mirror of
https://github.com/openai/codex.git
synced 2026-10-21 17:48:44 +00:00
Remove WebSocket transport from code-mode hosts (#40692)
## What changed - Accept only `http://` and `https://` gRPC endpoints for app server `--code-mode-host` connections. - Limit the standalone code-mode host to stdio and gRPC listeners. - Remove the WebSocket session provider, dual-WebSocket negotiation, transport lanes, and related dependencies and tests. - Keep coverage for shared and prewarmed gRPC hosts and reject WebSocket host URLs in CLI parsing tests. GitOrigin-RevId: 8792604e79ffd54ce776074a4dc3f7d55e63752a
This commit is contained in:
committed by
copyberry
parent
9be8d6e1c3
commit
725b3a44f8
@@ -6,4 +6,3 @@ pub use grpc_session::GrpcCodeModeSessionProvider;
|
||||
pub use remote_session::DisabledCodeModeSessionProvider;
|
||||
pub use remote_session::ProcessOwnedCodeModeSession;
|
||||
pub use remote_session::ProcessOwnedCodeModeSessionProvider;
|
||||
pub use remote_session::WebSocketCodeModeSessionProvider;
|
||||
|
||||
@@ -18,8 +18,6 @@ use codex_code_mode_protocol::StartedCell;
|
||||
use codex_code_mode_protocol::WaitOutcome;
|
||||
use codex_code_mode_protocol::WaitRequest;
|
||||
use codex_code_mode_protocol::host::SessionId;
|
||||
use codex_http_client::HttpClientFactory;
|
||||
use codex_http_client::OutboundProxyPolicy;
|
||||
use codex_install_context::InstallContext;
|
||||
use tokio::sync::Semaphore;
|
||||
use tokio::sync::watch;
|
||||
@@ -43,11 +41,6 @@ pub struct ProcessOwnedCodeModeSessionProvider {
|
||||
#[derive(Default)]
|
||||
pub struct DisabledCodeModeSessionProvider;
|
||||
|
||||
/// Creates code-mode sessions backed by one shared remote WebSocket connection.
|
||||
pub struct WebSocketCodeModeSessionProvider {
|
||||
host: Arc<OwnedCodeModeHost>,
|
||||
}
|
||||
|
||||
impl ProcessOwnedCodeModeSessionProvider {
|
||||
pub fn with_host_program(host_program: PathBuf) -> Self {
|
||||
Self {
|
||||
@@ -68,9 +61,7 @@ impl Default for ProcessOwnedCodeModeSessionProvider {
|
||||
|
||||
impl CodeModeSessionProvider for ProcessOwnedCodeModeSessionProvider {
|
||||
fn availability(&self) -> Result<(), String> {
|
||||
let HostEndpoint::Process(host_program) = &self.host.endpoint else {
|
||||
unreachable!("a process-owned provider always has a process endpoint");
|
||||
};
|
||||
let host_program = &self.host.host_program;
|
||||
if host_program.is_file() {
|
||||
Ok(())
|
||||
} else {
|
||||
@@ -119,49 +110,6 @@ impl CodeModeSessionProvider for DisabledCodeModeSessionProvider {
|
||||
}
|
||||
}
|
||||
|
||||
impl WebSocketCodeModeSessionProvider {
|
||||
pub fn new(websocket_url: String) -> Self {
|
||||
Self::with_http_client_factory(
|
||||
websocket_url,
|
||||
HttpClientFactory::new(OutboundProxyPolicy::ReqwestDefault),
|
||||
)
|
||||
}
|
||||
|
||||
/// Creates a remote host using the application's effective proxy and TLS policy.
|
||||
pub fn with_http_client_factory(
|
||||
websocket_url: String,
|
||||
http_client_factory: HttpClientFactory,
|
||||
) -> Self {
|
||||
Self {
|
||||
host: Arc::new(OwnedCodeModeHost::websocket(
|
||||
websocket_url,
|
||||
http_client_factory,
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl CodeModeSessionProvider for WebSocketCodeModeSessionProvider {
|
||||
fn create_session<'a>(
|
||||
&'a self,
|
||||
delegate: Arc<dyn CodeModeSessionDelegate>,
|
||||
) -> CodeModeSessionProviderFuture<'a> {
|
||||
self.create_session_with_limits(delegate, CodeModeSessionCellExecutionLimits::default())
|
||||
}
|
||||
|
||||
fn create_session_with_limits<'a>(
|
||||
&'a self,
|
||||
delegate: Arc<dyn CodeModeSessionDelegate>,
|
||||
limits: CodeModeSessionCellExecutionLimits,
|
||||
) -> CodeModeSessionProviderFuture<'a> {
|
||||
Box::pin(create_host_session(
|
||||
delegate,
|
||||
Arc::clone(&self.host),
|
||||
limits,
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
async fn create_host_session(
|
||||
delegate: Arc<dyn CodeModeSessionDelegate>,
|
||||
host: Arc<OwnedCodeModeHost>,
|
||||
@@ -172,16 +120,8 @@ async fn create_host_session(
|
||||
Ok(Arc::new(session))
|
||||
}
|
||||
|
||||
enum HostEndpoint {
|
||||
Process(PathBuf),
|
||||
WebSocket {
|
||||
websocket_url: String,
|
||||
http_client_factory: HttpClientFactory,
|
||||
},
|
||||
}
|
||||
|
||||
struct OwnedCodeModeHost {
|
||||
endpoint: HostEndpoint,
|
||||
host_program: PathBuf,
|
||||
connection: StdMutex<Option<Arc<Connection>>>,
|
||||
connect_permit: Semaphore,
|
||||
connection_generation: AtomicU64,
|
||||
@@ -192,21 +132,7 @@ struct OwnedCodeModeHost {
|
||||
impl OwnedCodeModeHost {
|
||||
fn new(host_program: PathBuf) -> Self {
|
||||
Self {
|
||||
endpoint: HostEndpoint::Process(host_program),
|
||||
connection: StdMutex::new(None),
|
||||
connect_permit: Semaphore::new(/*permits*/ 1),
|
||||
connection_generation: AtomicU64::new(0),
|
||||
last_connection_error: StdMutex::new(None),
|
||||
next_session_id: AtomicU64::new(1),
|
||||
}
|
||||
}
|
||||
|
||||
fn websocket(websocket_url: String, http_client_factory: HttpClientFactory) -> Self {
|
||||
Self {
|
||||
endpoint: HostEndpoint::WebSocket {
|
||||
websocket_url,
|
||||
http_client_factory,
|
||||
},
|
||||
host_program,
|
||||
connection: StdMutex::new(None),
|
||||
connect_permit: Semaphore::new(/*permits*/ 1),
|
||||
connection_generation: AtomicU64::new(0),
|
||||
@@ -238,13 +164,7 @@ impl OwnedCodeModeHost {
|
||||
{
|
||||
return Err(ConnectionError::Other(error.clone()));
|
||||
}
|
||||
let connection = match &self.endpoint {
|
||||
HostEndpoint::Process(host_program) => Connection::spawn(host_program).await,
|
||||
HostEndpoint::WebSocket {
|
||||
websocket_url,
|
||||
http_client_factory,
|
||||
} => Connection::connect_websocket(websocket_url, http_client_factory).await,
|
||||
};
|
||||
let connection = Connection::spawn(&self.host_program).await;
|
||||
let new_connection = match connection {
|
||||
Ok(connection) => connection,
|
||||
Err(error) => {
|
||||
@@ -312,7 +232,7 @@ struct SessionInner {
|
||||
retired_cleanups: StdMutex<Vec<SessionCleanup>>,
|
||||
}
|
||||
|
||||
/// A logical code-mode session assigned to a process or WebSocket host.
|
||||
/// A logical code-mode session assigned to a process host.
|
||||
pub struct ProcessOwnedCodeModeSession {
|
||||
inner: Arc<SessionInner>,
|
||||
}
|
||||
|
||||
@@ -20,32 +20,24 @@ use codex_code_mode_protocol::host::Capability;
|
||||
use codex_code_mode_protocol::host::CapabilitySet;
|
||||
use codex_code_mode_protocol::host::ClientHello;
|
||||
use codex_code_mode_protocol::host::ClientToHost;
|
||||
use codex_code_mode_protocol::host::DUAL_WEBSOCKET_CAPABILITY;
|
||||
use codex_code_mode_protocol::host::EncodedFrame;
|
||||
use codex_code_mode_protocol::host::FramedReader;
|
||||
use codex_code_mode_protocol::host::FramedWriter;
|
||||
use codex_code_mode_protocol::host::HostToClient;
|
||||
use codex_code_mode_protocol::host::MAX_FRAME_BYTES;
|
||||
use codex_code_mode_protocol::host::MAX_PENDING_DELEGATE_CALLS;
|
||||
use codex_code_mode_protocol::host::ProtocolVersion;
|
||||
use codex_code_mode_protocol::host::RequestId;
|
||||
use codex_code_mode_protocol::host::SESSION_RESOURCE_LIMITS_CAPABILITY;
|
||||
use codex_code_mode_protocol::host::SupportedProtocolVersions;
|
||||
use codex_code_mode_protocol::host::TransportLane;
|
||||
use codex_http_client::HttpClientFactory;
|
||||
use codex_protocol::shell_environment::scrub_non_inheritable_env_vars;
|
||||
use codex_websocket_client::WebSocketConnector;
|
||||
use futures::StreamExt;
|
||||
use tokio::io::AsyncBufReadExt;
|
||||
use tokio::io::BufReader;
|
||||
use tokio::process::Child;
|
||||
use tokio::process::ChildStdin;
|
||||
use tokio::process::ChildStdout;
|
||||
use tokio::process::Command;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::task::JoinHandle;
|
||||
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
|
||||
use tokio_tungstenite::tungstenite::http::Uri;
|
||||
use tokio_tungstenite::tungstenite::protocol::WebSocketConfig;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
use tracing::debug;
|
||||
use tracing::warn;
|
||||
@@ -57,19 +49,14 @@ use self::driver::DriverLifecycle;
|
||||
pub(super) use self::driver::RemoteSession;
|
||||
pub(super) use self::driver::SessionCleanup;
|
||||
use self::reader::drive_reader;
|
||||
use self::transport::ConnectionReader;
|
||||
use self::transport::ConnectionWriter;
|
||||
|
||||
mod driver;
|
||||
mod reader;
|
||||
mod transport;
|
||||
|
||||
const IPC_CHANNEL_CAPACITY: usize = 128;
|
||||
const HOST_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
const LOCAL_HOST_STARTUP_TIMEOUT: Duration = Duration::from_secs(30);
|
||||
// TODO(anp) make this timeout configurable if 60 seconds is insufficient.
|
||||
const DEFAULT_HOST_WAIT_TRANSPORT_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
const MAX_WEBSOCKET_FRAME_BYTES: usize = MAX_FRAME_BYTES + std::mem::size_of::<u32>();
|
||||
// Host spawn errors become model-visible tool output. Bound configured paths
|
||||
// while preserving the executable-bearing suffix needed to diagnose failures.
|
||||
const MAX_DISPLAYED_HOST_PROGRAM_BYTES: usize = 512;
|
||||
@@ -80,7 +67,6 @@ pub(super) enum ConnectionError {
|
||||
host_program: PathBuf,
|
||||
error: io::Error,
|
||||
},
|
||||
BulkConnectionUnavailable(String),
|
||||
Other(String),
|
||||
}
|
||||
|
||||
@@ -111,9 +97,7 @@ impl fmt::Display for ConnectionError {
|
||||
&host_program[suffix_start..]
|
||||
)
|
||||
}
|
||||
Self::BulkConnectionUnavailable(message) | Self::Other(message) => {
|
||||
formatter.write_str(message)
|
||||
}
|
||||
Self::Other(message) => formatter.write_str(message),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,7 +117,7 @@ struct CallerCancellation {
|
||||
}
|
||||
|
||||
struct ConnectionSupervisor {
|
||||
owner: ConnectionOwner,
|
||||
child: Child,
|
||||
event_tx: mpsc::Sender<DriverEvent>,
|
||||
cancellation: CancellationToken,
|
||||
alive: Arc<AtomicBool>,
|
||||
@@ -143,50 +127,6 @@ struct ConnectionSupervisor {
|
||||
writer_task: JoinHandle<Result<(), String>>,
|
||||
}
|
||||
|
||||
enum ConnectionOwner {
|
||||
Process(Box<Child>),
|
||||
WebSocket,
|
||||
}
|
||||
|
||||
struct BulkConnectionOptions {
|
||||
websocket_url: String,
|
||||
http_client_factory: HttpClientFactory,
|
||||
}
|
||||
|
||||
impl BulkConnectionOptions {
|
||||
async fn connect(
|
||||
&self,
|
||||
token: &str,
|
||||
) -> Result<(ConnectionReader, ConnectionWriter), ConnectionError> {
|
||||
let control_uri = self.websocket_url.parse::<Uri>().map_err(|error| {
|
||||
ConnectionError::Other(format!(
|
||||
"failed to build code-mode host bulk websocket URL: {error}"
|
||||
))
|
||||
})?;
|
||||
let bulk_path = format!("{}/bulk/{token}", control_uri.path().trim_end_matches('/'));
|
||||
let bulk_path_and_query = match control_uri.query() {
|
||||
Some(query) => format!("{bulk_path}?{query}"),
|
||||
None => bulk_path,
|
||||
};
|
||||
let mut bulk_uri_parts = control_uri.into_parts();
|
||||
bulk_uri_parts.path_and_query = Some(bulk_path_and_query.parse().map_err(|error| {
|
||||
ConnectionError::Other(format!(
|
||||
"failed to build code-mode host bulk websocket path: {error}"
|
||||
))
|
||||
})?);
|
||||
let bulk_url = Uri::from_parts(bulk_uri_parts)
|
||||
.map_err(|error| {
|
||||
ConnectionError::Other(format!(
|
||||
"failed to build code-mode host bulk websocket URL: {error}"
|
||||
))
|
||||
})?
|
||||
.to_string();
|
||||
connect_websocket_transport(&bulk_url, &self.http_client_factory)
|
||||
.await
|
||||
.map_err(|error| ConnectionError::BulkConnectionUnavailable(error.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
impl CallerCancellation {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
@@ -253,52 +193,19 @@ impl Connection {
|
||||
.take()
|
||||
.ok_or_else(|| ConnectionError::Other("spawned code-mode host has no stdout".into()))?;
|
||||
|
||||
Self::establish(
|
||||
ConnectionReader::Stdio(FramedReader::new(stdout)),
|
||||
ConnectionWriter::Stdio(FramedWriter::new(stdin)),
|
||||
ConnectionOwner::Process(Box::new(child)),
|
||||
/*bulk_connection_options*/ None,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
pub(super) async fn connect_websocket(
|
||||
websocket_url: &str,
|
||||
http_client_factory: &HttpClientFactory,
|
||||
) -> Result<Self, ConnectionError> {
|
||||
let (reader, writer) =
|
||||
connect_websocket_transport(websocket_url, http_client_factory).await?;
|
||||
|
||||
Self::establish(
|
||||
reader,
|
||||
writer,
|
||||
ConnectionOwner::WebSocket,
|
||||
Some(BulkConnectionOptions {
|
||||
websocket_url: websocket_url.to_string(),
|
||||
http_client_factory: http_client_factory.clone(),
|
||||
}),
|
||||
)
|
||||
.await
|
||||
Self::establish(FramedReader::new(stdout), FramedWriter::new(stdin), child).await
|
||||
}
|
||||
|
||||
async fn establish(
|
||||
mut reader: ConnectionReader,
|
||||
mut writer: ConnectionWriter,
|
||||
mut owner: ConnectionOwner,
|
||||
bulk_connection_options: Option<BulkConnectionOptions>,
|
||||
mut reader: FramedReader<ChildStdout>,
|
||||
mut writer: FramedWriter<ChildStdin>,
|
||||
mut child: Child,
|
||||
) -> Result<Self, ConnectionError> {
|
||||
let handshake = async {
|
||||
let dual_capability =
|
||||
Capability::new(DUAL_WEBSOCKET_CAPABILITY).map_err(|error| error.to_string())?;
|
||||
let session_limits_capability = Capability::new(SESSION_RESOURCE_LIMITS_CAPABILITY)
|
||||
.map_err(|error| error.to_string())?;
|
||||
let optional_capabilities = if bulk_connection_options.is_some() {
|
||||
CapabilitySet::try_new([dual_capability.clone(), session_limits_capability])
|
||||
.map_err(|error| error.to_string())?
|
||||
} else {
|
||||
CapabilitySet::try_new([session_limits_capability])
|
||||
.map_err(|error| error.to_string())?
|
||||
};
|
||||
let optional_capabilities = CapabilitySet::try_new([session_limits_capability])
|
||||
.map_err(|error| error.to_string())?;
|
||||
let hello = ClientHello::new(
|
||||
SupportedProtocolVersions::try_new([ProtocolVersion::V1])
|
||||
.map_err(|err| err.to_string())?,
|
||||
@@ -318,24 +225,7 @@ impl Connection {
|
||||
Some(HostToClient::HostHello(hello))
|
||||
if hello.selected_version() == ProtocolVersion::V1 =>
|
||||
{
|
||||
let capabilities = hello.capabilities().clone();
|
||||
let bulk_token = if capabilities.contains(&dual_capability) {
|
||||
hello
|
||||
.bulk_connection_token()
|
||||
.map(str::to_string)
|
||||
.ok_or_else(|| {
|
||||
"code-mode host advertised dual websockets without a pairing token"
|
||||
.to_string()
|
||||
})
|
||||
.map(Some)?
|
||||
} else if hello.bulk_connection_token().is_some() {
|
||||
return Err(
|
||||
"code-mode host returned an unexpected bulk pairing token".to_string()
|
||||
);
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Ok((capabilities, bulk_token))
|
||||
Ok(hello.capabilities().clone())
|
||||
}
|
||||
Some(HostToClient::HandshakeRejected { reason }) => {
|
||||
Err(format!("code-mode host rejected the handshake: {reason:?}"))
|
||||
@@ -346,99 +236,42 @@ impl Connection {
|
||||
None => Err("code-mode host exited during handshake".to_string()),
|
||||
}
|
||||
};
|
||||
let handshake_timeout = match &owner {
|
||||
ConnectionOwner::Process(_) => LOCAL_HOST_STARTUP_TIMEOUT,
|
||||
ConnectionOwner::WebSocket => HOST_HANDSHAKE_TIMEOUT,
|
||||
};
|
||||
let handshake_result = match tokio::time::timeout(handshake_timeout, handshake).await {
|
||||
Ok(result) => result,
|
||||
Err(_) => {
|
||||
let _ = writer.close().await;
|
||||
owner.close().await;
|
||||
return Err(ConnectionError::Other(
|
||||
"timed out negotiating with the code-mode host".into(),
|
||||
));
|
||||
}
|
||||
};
|
||||
let (capabilities, bulk_token) = match handshake_result {
|
||||
let handshake_result =
|
||||
match tokio::time::timeout(LOCAL_HOST_STARTUP_TIMEOUT, handshake).await {
|
||||
Ok(result) => result,
|
||||
Err(_) => {
|
||||
kill_and_reap(&mut child).await;
|
||||
return Err(ConnectionError::Other(
|
||||
"timed out negotiating with the code-mode host".into(),
|
||||
));
|
||||
}
|
||||
};
|
||||
let capabilities = match handshake_result {
|
||||
Ok(negotiated) => negotiated,
|
||||
Err(err) => {
|
||||
let _ = writer.close().await;
|
||||
owner.close().await;
|
||||
kill_and_reap(&mut child).await;
|
||||
return Err(ConnectionError::Other(err));
|
||||
}
|
||||
};
|
||||
let (bulk_reader, bulk_writer) = if let Some(token) = bulk_token {
|
||||
let Some(options) = bulk_connection_options else {
|
||||
let _ = writer.close().await;
|
||||
owner.close().await;
|
||||
return Err(ConnectionError::Other(
|
||||
"code-mode host negotiated an unsupported bulk websocket".to_string(),
|
||||
));
|
||||
};
|
||||
match options.connect(&token).await {
|
||||
Ok((reader, writer)) => (Some(reader), Some(writer)),
|
||||
Err(error) => {
|
||||
let _ = writer.close().await;
|
||||
owner.close().await;
|
||||
return Err(error);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
|
||||
let (command_tx, command_rx) = mpsc::channel(IPC_CHANNEL_CAPACITY);
|
||||
let (event_tx, event_rx) = mpsc::channel(IPC_CHANNEL_CAPACITY);
|
||||
let (outgoing_tx, outgoing_rx) = mpsc::channel::<EncodedFrame>(IPC_CHANNEL_CAPACITY);
|
||||
let (bulk_tx, bulk_rx) = if bulk_writer.is_some() {
|
||||
let (sender, receiver) = mpsc::channel::<EncodedFrame>(MAX_PENDING_DELEGATE_CALLS);
|
||||
(Some(sender), Some(receiver))
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
let dual_websocket = bulk_writer.is_some();
|
||||
let cancellation = CancellationToken::new();
|
||||
let alive = Arc::new(AtomicBool::new(true));
|
||||
let failure = Arc::new(std::sync::Mutex::new(None));
|
||||
|
||||
let writer_cancellation = cancellation.clone();
|
||||
let writer_task = tokio::spawn(async move {
|
||||
if let (Some(bulk_writer), Some(bulk_rx)) = (bulk_writer, bulk_rx) {
|
||||
tokio::try_join!(
|
||||
drive_writer(writer, outgoing_rx, writer_cancellation.clone()),
|
||||
drive_writer(bulk_writer, bulk_rx, writer_cancellation)
|
||||
)?;
|
||||
Ok(())
|
||||
} else {
|
||||
drive_writer(writer, outgoing_rx, writer_cancellation).await
|
||||
}
|
||||
});
|
||||
let writer_task =
|
||||
tokio::spawn(
|
||||
async move { drive_writer(writer, outgoing_rx, writer_cancellation).await },
|
||||
);
|
||||
|
||||
let reader_events = event_tx.clone();
|
||||
let reader_cancellation = cancellation.clone();
|
||||
let reader_task = tokio::spawn(async move {
|
||||
let lane = dual_websocket.then_some(TransportLane::Control);
|
||||
if let Some(bulk_reader) = bulk_reader {
|
||||
tokio::try_join!(
|
||||
drive_reader(
|
||||
reader,
|
||||
reader_events.clone(),
|
||||
reader_cancellation.clone(),
|
||||
lane,
|
||||
),
|
||||
drive_reader(
|
||||
bulk_reader,
|
||||
reader_events,
|
||||
reader_cancellation,
|
||||
Some(TransportLane::Bulk),
|
||||
)
|
||||
)?;
|
||||
Ok(())
|
||||
} else {
|
||||
drive_reader(reader, reader_events, reader_cancellation, lane).await
|
||||
}
|
||||
});
|
||||
let reader_task =
|
||||
tokio::spawn(
|
||||
async move { drive_reader(reader, reader_events, reader_cancellation).await },
|
||||
);
|
||||
|
||||
let (driver, execute_claim_tx) = ConnectionDriver::new(
|
||||
command_rx,
|
||||
@@ -451,14 +284,10 @@ impl Connection {
|
||||
cancellation: cancellation.clone(),
|
||||
},
|
||||
);
|
||||
let driver = match bulk_tx {
|
||||
Some(sender) => driver.with_bulk_sender(sender),
|
||||
None => driver,
|
||||
};
|
||||
let driver_task = tokio::spawn(driver.run());
|
||||
tokio::spawn(
|
||||
ConnectionSupervisor {
|
||||
owner,
|
||||
child,
|
||||
event_tx,
|
||||
cancellation: cancellation.clone(),
|
||||
alive: Arc::clone(&alive),
|
||||
@@ -653,65 +482,21 @@ impl Connection {
|
||||
}
|
||||
}
|
||||
|
||||
async fn connect_websocket_transport(
|
||||
websocket_url: &str,
|
||||
http_client_factory: &HttpClientFactory,
|
||||
) -> Result<(ConnectionReader, ConnectionWriter), ConnectionError> {
|
||||
let request = websocket_url.into_client_request().map_err(|error| {
|
||||
ConnectionError::Other(format!(
|
||||
"failed to build code-mode host websocket request: {error}"
|
||||
))
|
||||
})?;
|
||||
let connector = WebSocketConnector::new(http_client_factory)
|
||||
.map_err(|error| {
|
||||
ConnectionError::Other(format!(
|
||||
"failed to configure code-mode host websocket TLS: {error}"
|
||||
))
|
||||
})?
|
||||
.with_tcp_nodelay();
|
||||
let websocket_config = WebSocketConfig::default()
|
||||
.max_frame_size(Some(MAX_WEBSOCKET_FRAME_BYTES))
|
||||
.max_message_size(Some(MAX_WEBSOCKET_FRAME_BYTES));
|
||||
let (websocket, _) = tokio::time::timeout(
|
||||
HOST_HANDSHAKE_TIMEOUT,
|
||||
connector.connect(request, websocket_config),
|
||||
)
|
||||
.await
|
||||
.map_err(|_| {
|
||||
ConnectionError::Other("timed out connecting to the code-mode host websocket".into())
|
||||
})?
|
||||
.map_err(|error| {
|
||||
ConnectionError::Other(format!(
|
||||
"failed to connect to the code-mode host websocket: {error}"
|
||||
))
|
||||
})?;
|
||||
let (writer, reader) = websocket.split();
|
||||
Ok((
|
||||
ConnectionReader::WebSocket(reader),
|
||||
ConnectionWriter::WebSocket(writer),
|
||||
))
|
||||
}
|
||||
|
||||
async fn drive_writer(
|
||||
mut writer: ConnectionWriter,
|
||||
mut writer: FramedWriter<ChildStdin>,
|
||||
mut outgoing: mpsc::Receiver<EncodedFrame>,
|
||||
cancellation: CancellationToken,
|
||||
) -> Result<(), String> {
|
||||
loop {
|
||||
tokio::select! {
|
||||
_ = cancellation.cancelled() => {
|
||||
return writer
|
||||
.close()
|
||||
.await
|
||||
.map_err(|error| format!("failed to close code-mode host connection: {error}"));
|
||||
}
|
||||
_ = cancellation.cancelled() => return Ok(()),
|
||||
frame = outgoing.recv() => {
|
||||
let Some(frame) = frame else {
|
||||
return Err("code-mode host outgoing stream closed".to_string());
|
||||
};
|
||||
tokio::select! {
|
||||
_ = cancellation.cancelled() => return Ok(()),
|
||||
result = writer.write_frame(frame) => {
|
||||
result = writer.write_frame(&frame) => {
|
||||
result.map_err(|error| {
|
||||
format!("failed to write code-mode host message: {error}")
|
||||
})?;
|
||||
@@ -735,7 +520,7 @@ impl Drop for Connection {
|
||||
|
||||
impl ConnectionSupervisor {
|
||||
async fn run(mut self) {
|
||||
let mut owner_exited = false;
|
||||
let mut child_exited = false;
|
||||
let reason = tokio::select! {
|
||||
biased;
|
||||
_ = self.cancellation.cancelled() => failure_message(&self.failure),
|
||||
@@ -745,35 +530,19 @@ impl ConnectionSupervisor {
|
||||
},
|
||||
result = &mut self.reader_task => task_failure("reader", result),
|
||||
result = &mut self.writer_task => task_failure("writer", result),
|
||||
reason = self.owner.wait() => {
|
||||
owner_exited = true;
|
||||
reason
|
||||
result = self.child.wait() => {
|
||||
child_exited = true;
|
||||
match result {
|
||||
Ok(status) => format!("code-mode host exited with status {status}"),
|
||||
Err(error) => format!("failed waiting for code-mode host: {error}"),
|
||||
}
|
||||
}
|
||||
};
|
||||
mark_connection_dead(&self.alive, &self.failure, reason.clone());
|
||||
let _ = self.event_tx.try_send(DriverEvent::Failed(reason));
|
||||
self.cancellation.cancel();
|
||||
if !owner_exited {
|
||||
self.owner.close().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectionOwner {
|
||||
async fn wait(&mut self) -> String {
|
||||
match self {
|
||||
Self::Process(child) => match child.wait().await {
|
||||
Ok(status) => format!("code-mode host exited with status {status}"),
|
||||
Err(error) => format!("failed waiting for code-mode host: {error}"),
|
||||
},
|
||||
Self::WebSocket => std::future::pending().await,
|
||||
}
|
||||
}
|
||||
|
||||
async fn close(&mut self) {
|
||||
match self {
|
||||
Self::Process(child) => kill_and_reap(child).await,
|
||||
Self::WebSocket => {}
|
||||
if !child_exited {
|
||||
kill_and_reap(&mut self.child).await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ use codex_code_mode_protocol::CodeModeSessionDelegate;
|
||||
use codex_code_mode_protocol::host::EncodedFrame;
|
||||
use codex_code_mode_protocol::host::HostToClient;
|
||||
use codex_code_mode_protocol::host::RequestId;
|
||||
use codex_code_mode_protocol::host::TransportLane;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
@@ -42,7 +41,6 @@ pub(super) struct ConnectionDriver {
|
||||
event_tx: mpsc::Sender<DriverEvent>,
|
||||
execute_claim_rx: mpsc::UnboundedReceiver<RequestId>,
|
||||
outgoing_tx: mpsc::Sender<EncodedFrame>,
|
||||
bulk_tx: Option<mpsc::Sender<EncodedFrame>>,
|
||||
requests: RequestTracker,
|
||||
deferred_host_messages: VecDeque<HostToClient>,
|
||||
sessions: SessionRegistry,
|
||||
@@ -69,7 +67,6 @@ impl ConnectionDriver {
|
||||
event_tx: event_tx.clone(),
|
||||
execute_claim_rx,
|
||||
outgoing_tx,
|
||||
bulk_tx: None,
|
||||
requests: RequestTracker::new(),
|
||||
deferred_host_messages: VecDeque::new(),
|
||||
sessions: SessionRegistry::new(),
|
||||
@@ -137,17 +134,8 @@ impl ConnectionDriver {
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn with_bulk_sender(mut self, sender: mpsc::Sender<EncodedFrame>) -> Self {
|
||||
self.bulk_tx = Some(sender);
|
||||
self
|
||||
}
|
||||
|
||||
fn queue_frame(&mut self, frame: EncodedFrame, lane: TransportLane) -> bool {
|
||||
let sender = match lane {
|
||||
TransportLane::Control => &self.outgoing_tx,
|
||||
TransportLane::Bulk => self.bulk_tx.as_ref().unwrap_or(&self.outgoing_tx),
|
||||
};
|
||||
match sender.try_send(frame) {
|
||||
fn queue_frame(&mut self, frame: EncodedFrame) -> bool {
|
||||
match self.outgoing_tx.try_send(frame) {
|
||||
Ok(()) => true,
|
||||
Err(mpsc::error::TrySendError::Full(_)) => {
|
||||
self.fail("code-mode host outgoing queue is full".to_string());
|
||||
|
||||
@@ -127,8 +127,7 @@ impl ConnectionDriver {
|
||||
},
|
||||
&self.event_tx,
|
||||
);
|
||||
let lane = message.transport_lane();
|
||||
self.queue_frame(frame, lane)
|
||||
self.queue_frame(frame)
|
||||
}
|
||||
|
||||
fn execute(
|
||||
@@ -187,8 +186,7 @@ impl ConnectionDriver {
|
||||
},
|
||||
&self.event_tx,
|
||||
);
|
||||
let lane = message.transport_lane();
|
||||
self.queue_frame(frame, lane)
|
||||
self.queue_frame(frame)
|
||||
}
|
||||
|
||||
fn wait(
|
||||
@@ -317,7 +315,6 @@ impl ConnectionDriver {
|
||||
};
|
||||
self.requests
|
||||
.insert_pending(request_id, pending, &self.event_tx);
|
||||
let lane = message.transport_lane();
|
||||
self.queue_frame(frame, lane)
|
||||
self.queue_frame(frame)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,8 +318,7 @@ impl ConnectionDriver {
|
||||
}
|
||||
}
|
||||
};
|
||||
let lane = message.transport_lane();
|
||||
self.queue_frame(frame, lane)
|
||||
self.queue_frame(frame)
|
||||
}
|
||||
|
||||
pub(super) fn close_cell(&mut self, session_id: SessionId, cell_id: WireCellId) -> bool {
|
||||
|
||||
@@ -374,8 +374,7 @@ impl ConnectionDriver {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
let lane = message.transport_lane();
|
||||
self.queue_frame(frame, lane)
|
||||
self.queue_frame(frame)
|
||||
}
|
||||
|
||||
fn shutdown_abandoned_session(&mut self, session: RemoteSession) -> bool {
|
||||
|
||||
@@ -17,7 +17,6 @@ use codex_code_mode_protocol::host::CapabilitySet;
|
||||
use codex_code_mode_protocol::host::ClientToHost;
|
||||
use codex_code_mode_protocol::host::DelegateRequest;
|
||||
use codex_code_mode_protocol::host::DelegateRequestId;
|
||||
use codex_code_mode_protocol::host::DelegateResponse;
|
||||
use codex_code_mode_protocol::host::EncodedFrame;
|
||||
use codex_code_mode_protocol::host::HostRequest;
|
||||
use codex_code_mode_protocol::host::HostResponse;
|
||||
@@ -248,11 +247,6 @@ struct RecordingDelegate {
|
||||
|
||||
struct PanickingDelegate;
|
||||
|
||||
struct LargeResultBurstDelegate {
|
||||
started: AtomicUsize,
|
||||
release: CancellationToken,
|
||||
}
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
enum HeldDelegateEvent {
|
||||
Started,
|
||||
@@ -342,35 +336,6 @@ impl CodeModeSessionDelegate for PanickingDelegate {
|
||||
fn cell_closed(&self, _cell_id: &CellId) {}
|
||||
}
|
||||
|
||||
impl CodeModeSessionDelegate for LargeResultBurstDelegate {
|
||||
fn invoke_tool<'a>(
|
||||
&'a self,
|
||||
_invocation: CodeModeNestedToolCall,
|
||||
cancellation_token: CancellationToken,
|
||||
) -> ToolInvocationFuture<'a> {
|
||||
self.started.fetch_add(1, Ordering::Release);
|
||||
let release = self.release.clone();
|
||||
Box::pin(async move {
|
||||
tokio::select! {
|
||||
_ = cancellation_token.cancelled() => Err("cancelled".to_string()),
|
||||
_ = release.cancelled() => Ok("x".repeat(256 * 1024).into()),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn notify<'a>(
|
||||
&'a self,
|
||||
_call_id: String,
|
||||
_cell_id: CellId,
|
||||
_text: String,
|
||||
_cancellation_token: CancellationToken,
|
||||
) -> NotificationFuture<'a> {
|
||||
Box::pin(async { Ok(()) })
|
||||
}
|
||||
|
||||
fn cell_closed(&self, _cell_id: &CellId) {}
|
||||
}
|
||||
|
||||
impl CodeModeSessionDelegate for RecordingDelegate {
|
||||
fn invoke_tool<'a>(
|
||||
&'a self,
|
||||
@@ -692,97 +657,6 @@ async fn delegate_cancel_is_best_effort_and_sends_no_late_response() {
|
||||
assert_eq!(delegate.notifications.load(Ordering::Relaxed), 0);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn concurrent_large_delegate_results_do_not_disconnect_a_backpressured_bulk_lane() {
|
||||
const CONCURRENT_RESULTS: usize = 129;
|
||||
|
||||
let (command_tx, command_rx) = mpsc::channel(/*max_capacity*/ 16);
|
||||
let (event_tx, event_rx) = mpsc::channel(/*max_capacity*/ 16);
|
||||
let (outgoing_tx, outgoing_rx) = mpsc::channel(/*max_capacity*/ 16);
|
||||
let (bulk_tx, mut bulk_rx) = mpsc::channel(MAX_PENDING_DELEGATE_CALLS);
|
||||
let cancellation = CancellationToken::new();
|
||||
let alive = Arc::new(AtomicBool::new(true));
|
||||
let failure = Arc::new(StdMutex::new(None));
|
||||
let (driver, execute_claim_tx) = ConnectionDriver::new(
|
||||
command_rx,
|
||||
event_rx,
|
||||
event_tx.clone(),
|
||||
outgoing_tx,
|
||||
DriverLifecycle {
|
||||
alive: Arc::clone(&alive),
|
||||
failure: Arc::clone(&failure),
|
||||
cancellation: cancellation.clone(),
|
||||
},
|
||||
);
|
||||
let driver_task = tokio::spawn(driver.with_bulk_sender(bulk_tx).run());
|
||||
let mut harness = DriverHarness {
|
||||
command_tx,
|
||||
event_tx,
|
||||
execute_claim_tx,
|
||||
outgoing_rx,
|
||||
cancellation,
|
||||
alive,
|
||||
failure,
|
||||
driver_task,
|
||||
};
|
||||
let session = remote_session();
|
||||
let delegate = Arc::new(LargeResultBurstDelegate {
|
||||
started: AtomicUsize::new(0),
|
||||
release: CancellationToken::new(),
|
||||
});
|
||||
harness.open(session.clone(), delegate.clone()).await;
|
||||
let _started = harness
|
||||
.start_cell(session.clone(), /*request_id*/ 2, "1")
|
||||
.await;
|
||||
|
||||
for value in 1..=CONCURRENT_RESULTS {
|
||||
harness
|
||||
.start_tool_delegate(&session, DelegateRequestId::new(value as i64))
|
||||
.await;
|
||||
}
|
||||
tokio::time::timeout(Duration::from_secs(10), async {
|
||||
while delegate.started.load(Ordering::Acquire) < CONCURRENT_RESULTS {
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("concurrent delegate calls should all start");
|
||||
|
||||
delegate.release.cancel();
|
||||
tokio::time::timeout(Duration::from_secs(10), async {
|
||||
while bulk_rx.len() < CONCURRENT_RESULTS {
|
||||
assert!(
|
||||
harness.alive.load(Ordering::Acquire),
|
||||
"bulk queue disconnected before accepting all concurrent tool results"
|
||||
);
|
||||
tokio::task::yield_now().await;
|
||||
}
|
||||
})
|
||||
.await
|
||||
.expect("concurrent large results should queue behind the blocked bulk writer");
|
||||
|
||||
let _unrelated = harness.start_cell(session, /*request_id*/ 3, "2").await;
|
||||
assert!(harness.alive.load(Ordering::Acquire));
|
||||
|
||||
for _ in 0..CONCURRENT_RESULTS {
|
||||
let frame = bulk_rx.recv().await.expect("queued bulk delegate result");
|
||||
let message = EncodedFrame::decode_framed::<ClientToHost>(&frame.into_framed_bytes())
|
||||
.expect("decode queued delegate result");
|
||||
let ClientToHost::DelegateResponse {
|
||||
result:
|
||||
WireResult::Ok {
|
||||
value: DelegateResponse::ToolResult { result },
|
||||
},
|
||||
..
|
||||
} = message
|
||||
else {
|
||||
panic!("expected a successful large delegate result");
|
||||
};
|
||||
assert_eq!(result.as_str().map(str::len), Some(256 * 1024));
|
||||
}
|
||||
assert!(harness.alive.load(Ordering::Acquire));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delegate_limit_returns_an_error_without_disconnecting() {
|
||||
let mut harness = DriverHarness::start();
|
||||
|
||||
@@ -1,15 +1,14 @@
|
||||
use codex_code_mode_protocol::host::TransportLane;
|
||||
use codex_code_mode_protocol::host::FramedReader;
|
||||
use tokio::process::ChildStdout;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use super::driver::DriverEvent;
|
||||
use super::transport::ConnectionReader;
|
||||
|
||||
pub(super) async fn drive_reader(
|
||||
mut reader: ConnectionReader,
|
||||
mut reader: FramedReader<ChildStdout>,
|
||||
events: mpsc::Sender<DriverEvent>,
|
||||
cancellation: CancellationToken,
|
||||
lane: Option<TransportLane>,
|
||||
) -> Result<(), String> {
|
||||
loop {
|
||||
let message = tokio::select! {
|
||||
@@ -21,11 +20,6 @@ pub(super) async fn drive_reader(
|
||||
Ok(None) => return Err("code-mode host closed its stdout".to_string()),
|
||||
Err(err) => return Err(format!("failed to read code-mode host message: {err}")),
|
||||
};
|
||||
if let Some(lane) = lane
|
||||
&& !message.allows_transport_lane(lane)
|
||||
{
|
||||
return Err("code-mode host sent a message on the wrong websocket lane".to_string());
|
||||
}
|
||||
events
|
||||
.send(DriverEvent::HostMessage(message))
|
||||
.await
|
||||
|
||||
@@ -1,103 +0,0 @@
|
||||
use std::io;
|
||||
use std::time::Duration;
|
||||
|
||||
use codex_code_mode_protocol::host::ClientToHost;
|
||||
use codex_code_mode_protocol::host::EncodedFrame;
|
||||
use codex_code_mode_protocol::host::FramedReader;
|
||||
use codex_code_mode_protocol::host::FramedWriter;
|
||||
use codex_code_mode_protocol::host::HostToClient;
|
||||
use codex_websocket_client::WebSocketConnection;
|
||||
use futures::SinkExt;
|
||||
use futures::StreamExt;
|
||||
use futures::stream::SplitSink;
|
||||
use futures::stream::SplitStream;
|
||||
use tokio::process::ChildStdin;
|
||||
use tokio::process::ChildStdout;
|
||||
use tokio_tungstenite::tungstenite::Message;
|
||||
|
||||
const WEBSOCKET_CLOSE_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
pub(super) enum ConnectionReader {
|
||||
Stdio(FramedReader<ChildStdout>),
|
||||
WebSocket(SplitStream<WebSocketConnection>),
|
||||
}
|
||||
|
||||
pub(super) enum ConnectionWriter {
|
||||
Stdio(FramedWriter<ChildStdin>),
|
||||
WebSocket(SplitSink<WebSocketConnection, Message>),
|
||||
}
|
||||
|
||||
impl ConnectionReader {
|
||||
pub(super) async fn read(&mut self) -> io::Result<Option<HostToClient>> {
|
||||
match self {
|
||||
Self::Stdio(reader) => reader.read().await,
|
||||
Self::WebSocket(reader) => loop {
|
||||
match reader.next().await {
|
||||
Some(Ok(Message::Binary(frame))) => {
|
||||
return EncodedFrame::decode_framed(&frame).map(Some);
|
||||
}
|
||||
Some(Ok(Message::Ping(_) | Message::Pong(_))) => {}
|
||||
Some(Ok(Message::Close(_))) | None => return Ok(None),
|
||||
Some(Ok(Message::Text(_))) => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"code-mode host websocket messages must be binary framed messages",
|
||||
));
|
||||
}
|
||||
Some(Ok(Message::Frame(_))) => {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::InvalidData,
|
||||
"code-mode host websocket returned an unexpected raw frame",
|
||||
));
|
||||
}
|
||||
Some(Err(error)) => {
|
||||
return Err(io::Error::other(format!(
|
||||
"failed to read code-mode host websocket message: {error}"
|
||||
)));
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ConnectionWriter {
|
||||
pub(super) async fn write(&mut self, message: &ClientToHost) -> io::Result<()> {
|
||||
self.write_frame(EncodedFrame::encode(message)?).await
|
||||
}
|
||||
|
||||
pub(super) async fn write_frame(&mut self, frame: EncodedFrame) -> io::Result<()> {
|
||||
match self {
|
||||
Self::Stdio(writer) => writer.write_frame(&frame).await,
|
||||
Self::WebSocket(writer) => writer
|
||||
.send(Message::Binary(frame.into_framed_bytes().into()))
|
||||
.await
|
||||
.map_err(|error| {
|
||||
io::Error::other(format!(
|
||||
"failed to write code-mode host websocket message: {error}"
|
||||
))
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn close(&mut self) -> io::Result<()> {
|
||||
match self {
|
||||
Self::Stdio(_) => Ok(()),
|
||||
Self::WebSocket(writer) => {
|
||||
tokio::time::timeout(WEBSOCKET_CLOSE_TIMEOUT, writer.close())
|
||||
.await
|
||||
.map_err(|_| {
|
||||
io::Error::new(
|
||||
io::ErrorKind::TimedOut,
|
||||
"timed out closing code-mode host websocket connection",
|
||||
)
|
||||
})?
|
||||
.map_err(|error| {
|
||||
io::Error::other(format!(
|
||||
"failed to close code-mode host websocket connection: {error}"
|
||||
))
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,43 +1,12 @@
|
||||
use std::io;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use codex_code_mode_protocol::CodeModeSessionCellExecutionLimits;
|
||||
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::Capability;
|
||||
use codex_code_mode_protocol::host::CapabilitySet;
|
||||
use codex_code_mode_protocol::host::ClientToHost;
|
||||
use codex_code_mode_protocol::host::DUAL_WEBSOCKET_CAPABILITY;
|
||||
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::SESSION_RESOURCE_LIMITS_CAPABILITY;
|
||||
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::io::AsyncReadExt;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
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::connection::ConnectionError;
|
||||
use crate::NoopCodeModeSessionDelegate;
|
||||
|
||||
@@ -86,274 +55,6 @@ fn missing_host_error_preserves_utf8_boundaries_when_truncating_the_path() {
|
||||
assert!(displayed_path.len() <= 512);
|
||||
}
|
||||
|
||||
#[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(hello) => {
|
||||
let capability = Capability::new(SESSION_RESOURCE_LIMITS_CAPABILITY)
|
||||
.expect("session-limit capability");
|
||||
assert!(hello.optional_capabilities().contains(&capability));
|
||||
assert_eq!(hello.required_capabilities(), &CapabilitySet::empty());
|
||||
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 error = provider
|
||||
.create_session_with_limits(
|
||||
Arc::new(NoopCodeModeSessionDelegate),
|
||||
CodeModeSessionCellExecutionLimits {
|
||||
max_yield_time_ms: Some(250),
|
||||
max_heap_size_bytes: None,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.err()
|
||||
.expect("legacy host should reject a limited session");
|
||||
assert_eq!(
|
||||
error,
|
||||
format!(
|
||||
"code-mode host does not support session resource limits: missing `{SESSION_RESOURCE_LIMITS_CAPABILITY}` capability"
|
||||
)
|
||||
);
|
||||
let second_session = provider
|
||||
.create_session(Arc::new(NoopCodeModeSessionDelegate))
|
||||
.await
|
||||
.expect("rejecting limited sessions should preserve the shared legacy-host connection");
|
||||
second_session
|
||||
.shutdown()
|
||||
.await
|
||||
.expect("second unlimited session should shut down");
|
||||
drop(second_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 websocket_provider_fails_when_a_negotiated_bulk_connection_is_unavailable() {
|
||||
let listener = TcpListener::bind("127.0.0.1:0")
|
||||
.await
|
||||
.expect("websocket test listener should bind");
|
||||
let websocket_url = format!(
|
||||
"ws://{}/?access_token=shared-token",
|
||||
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 the first connection");
|
||||
let mut control = accept_async(stream)
|
||||
.await
|
||||
.expect("first control websocket should connect");
|
||||
let frame = control
|
||||
.next()
|
||||
.await
|
||||
.expect("first client hello")
|
||||
.expect("first websocket frame")
|
||||
.into_data();
|
||||
let ClientToHost::ClientHello(hello) =
|
||||
EncodedFrame::decode_framed(&frame).expect("decode first client hello")
|
||||
else {
|
||||
panic!("expected first client hello");
|
||||
};
|
||||
let capability =
|
||||
Capability::new(DUAL_WEBSOCKET_CAPABILITY).expect("dual websocket capability");
|
||||
assert!(hello.optional_capabilities().contains(&capability));
|
||||
let session_limits_capability =
|
||||
Capability::new(SESSION_RESOURCE_LIMITS_CAPABILITY).expect("session-limit capability");
|
||||
assert!(
|
||||
hello
|
||||
.optional_capabilities()
|
||||
.contains(&session_limits_capability)
|
||||
);
|
||||
let hello = HostToClient::HostHello(
|
||||
HostHello::new(
|
||||
ProtocolVersion::V1,
|
||||
CapabilitySet::try_new([capability.clone()]).expect("host capabilities"),
|
||||
)
|
||||
.with_bulk_connection_token("fallback-token".to_string()),
|
||||
);
|
||||
let frame = EncodedFrame::encode(&hello).expect("encode dual host hello");
|
||||
control
|
||||
.send(Message::Binary(frame.into_framed_bytes().into()))
|
||||
.await
|
||||
.expect("send dual host hello");
|
||||
|
||||
let (mut bulk, _) = listener
|
||||
.accept()
|
||||
.await
|
||||
.expect("websocket test host should accept the bulk connection");
|
||||
let mut request = [0_u8; 1024];
|
||||
let request_len = bulk
|
||||
.read(&mut request)
|
||||
.await
|
||||
.expect("read bulk websocket handshake");
|
||||
let request = std::str::from_utf8(&request[..request_len]).expect("bulk HTTP request");
|
||||
assert!(
|
||||
request.starts_with("GET /bulk/fallback-token?access_token=shared-token HTTP/1.1\r\n")
|
||||
);
|
||||
bulk.write_all(b"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\nConnection: close\r\n\r\n")
|
||||
.await
|
||||
.expect("reject unavailable bulk websocket");
|
||||
drop(bulk);
|
||||
drop(control);
|
||||
});
|
||||
|
||||
let provider = WebSocketCodeModeSessionProvider::new(websocket_url);
|
||||
let error = match provider
|
||||
.create_session(Arc::new(NoopCodeModeSessionDelegate))
|
||||
.await
|
||||
{
|
||||
Ok(_) => panic!("provider should reject an unavailable negotiated bulk websocket"),
|
||||
Err(error) => error,
|
||||
};
|
||||
assert!(
|
||||
error.contains("404"),
|
||||
"unexpected negotiated bulk websocket error: {error}"
|
||||
);
|
||||
drop(provider);
|
||||
timeout(Duration::from_secs(5), server)
|
||||
.await
|
||||
.expect("negotiated bulk websocket test host should disconnect promptly")
|
||||
.expect("negotiated bulk websocket test host task should succeed");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn provider_returns_missing_host_error() {
|
||||
let provider = ProcessOwnedCodeModeSessionProvider::with_host_program(
|
||||
|
||||
Reference in New Issue
Block a user