mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Restore app-server websocket listener with auth guard (#22404)
## Why PR #21843 removed the TCP websocket app-server listener, but that also removed functionality that still needs to exist. Restoring it as-is would reopen the old remote exposure problem, so this keeps the restored listener while making remote and non-loopback usage require explicit auth. ## What Changed - Mostly reverts #21843 and reapplies the small merge-conflict resolutions needed on top of current main. - Restores ws://IP:PORT parsing, the app-server TCP websocket acceptor, websocket auth CLI flags, and the associated tests. - The only intentional behavior change from the restored code is that non-loopback websocket listeners now fail startup unless --ws-auth capability-token or --ws-auth signed-bearer-token is configured. Loopback listeners remain available for local and SSH-forwarding workflows. ## Reviewer Focus Please focus review on the small auth-enforcement delta layered on top of the revert: - codex-rs/app-server-transport/src/transport/websocket.rs: start_websocket_acceptor now rejects unauthenticated non-loopback websocket binds before accepting connections. - codex-rs/app-server-transport/src/transport/auth.rs: helper logic classifies unauthenticated non-loopback listeners. - codex-rs/app-server/tests/suite/v2/connection_handling_websocket.rs: tests cover unauthenticated ws://0.0.0.0 startup rejection and authenticated non-loopback capability-token startup. Everything else is intended to be revert/merge-conflict restoration rather than new product behavior. ## Verification - Manually verified that TUI remoting is restored and that auth is enforced for non-localhost urls.
This commit is contained in:
@@ -28,6 +28,7 @@ axum = { workspace = true, default-features = false, features = [
|
||||
"http1",
|
||||
"json",
|
||||
"tokio",
|
||||
"ws",
|
||||
] }
|
||||
codex-analytics = { workspace = true }
|
||||
codex-arg0 = { workspace = true }
|
||||
@@ -100,20 +101,22 @@ axum = { workspace = true, default-features = false, features = [
|
||||
"tokio",
|
||||
] }
|
||||
base64 = { workspace = true }
|
||||
codex-uds = { workspace = true }
|
||||
codex-model-provider-info = { workspace = true }
|
||||
codex-utils-cargo-bin = { workspace = true }
|
||||
core_test_support = { workspace = true }
|
||||
flate2 = { workspace = true }
|
||||
hmac = { workspace = true }
|
||||
opentelemetry = { workspace = true }
|
||||
opentelemetry_sdk = { workspace = true }
|
||||
pretty_assertions = { workspace = true }
|
||||
reqwest = { workspace = true, features = ["rustls-tls"] }
|
||||
rmcp = { workspace = true, default-features = false, features = [
|
||||
"elicitation",
|
||||
"server",
|
||||
"transport-streamable-http-server",
|
||||
] }
|
||||
serial_test = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
shlex = { workspace = true }
|
||||
tar = { workspace = true }
|
||||
tokio-tungstenite = { workspace = true }
|
||||
|
||||
@@ -24,9 +24,18 @@ Similar to [MCP](https://modelcontextprotocol.io/), `codex app-server` supports
|
||||
Supported transports:
|
||||
|
||||
- stdio (`--listen stdio://`, default): newline-delimited JSON (JSONL)
|
||||
- websocket (`--listen ws://IP:PORT`): one JSON-RPC message per websocket text frame (**experimental / unsupported**)
|
||||
- unix socket (`--listen unix://` or `--listen unix://PATH`): websocket connections over `$CODEX_HOME/app-server-control/app-server-control.sock` or a custom socket path, using the standard HTTP Upgrade handshake
|
||||
- off (`--listen off`): do not expose a local transport
|
||||
|
||||
When running with `--listen ws://IP:PORT`, the same listener also serves basic HTTP health probes:
|
||||
|
||||
- `GET /readyz` returns `200 OK` once the listener is accepting new connections.
|
||||
- `GET /healthz` returns `200 OK` when no `Origin` header is present.
|
||||
- Any request carrying an `Origin` header is rejected with `403 Forbidden`.
|
||||
|
||||
Websocket transport is currently experimental and unsupported. Do not rely on it for production workloads.
|
||||
|
||||
The unix socket transport is intended for local app-server control-plane clients. `codex app-server proxy`
|
||||
opens exactly one raw stream connection to `$CODEX_HOME/app-server-control/app-server-control.sock`
|
||||
by default, or to `--sock PATH` when provided, and proxies bytes between that socket and stdin/stdout.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
//! Tracing helpers shared by socket and in-process app-server entry points.
|
||||
//!
|
||||
//! The in-process path intentionally reuses the same span shape as JSON-RPC
|
||||
//! transports so request telemetry stays comparable across stdio, unix socket,
|
||||
//! transports so request telemetry stays comparable across stdio, websocket,
|
||||
//! and embedded callers. [`typed_request_span`] is the in-process counterpart
|
||||
//! of [`request_span`] and stamps `rpc.transport` as `"in-process"` while
|
||||
//! deriving client identity from the typed [`ClientRequest`] rather than
|
||||
@@ -86,6 +86,7 @@ fn transport_name(transport: &AppServerTransport) -> &'static str {
|
||||
match transport {
|
||||
AppServerTransport::Stdio => "stdio",
|
||||
AppServerTransport::UnixSocket { .. } => "unix_socket",
|
||||
AppServerTransport::WebSocket { .. } => "websocket",
|
||||
AppServerTransport::Off => "off",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,10 +31,12 @@ use crate::transport::ConnectionState;
|
||||
use crate::transport::OutboundConnectionState;
|
||||
use crate::transport::RemoteControlStartConfig;
|
||||
use crate::transport::TransportEvent;
|
||||
use crate::transport::auth::policy_from_settings;
|
||||
use crate::transport::route_outgoing_envelope;
|
||||
use crate::transport::start_control_socket_acceptor;
|
||||
use crate::transport::start_remote_control;
|
||||
use crate::transport::start_stdio_connection;
|
||||
use crate::transport::start_websocket_acceptor;
|
||||
use codex_analytics::AppServerRpcTransport;
|
||||
use codex_app_server_protocol::ConfigLayerSource;
|
||||
use codex_app_server_protocol::ConfigWarningNotification;
|
||||
@@ -101,6 +103,9 @@ pub use crate::error_code::INPUT_TOO_LARGE_ERROR_CODE;
|
||||
pub use crate::error_code::INVALID_PARAMS_ERROR_CODE;
|
||||
pub use crate::transport::AppServerTransport;
|
||||
pub use crate::transport::app_server_control_socket_path;
|
||||
pub use crate::transport::auth::AppServerWebsocketAuthArgs;
|
||||
pub use crate::transport::auth::AppServerWebsocketAuthSettings;
|
||||
pub use crate::transport::auth::WebsocketAuthCliMode;
|
||||
|
||||
const LOG_FORMAT_ENV_VAR: &str = "LOG_FORMAT";
|
||||
const OTEL_SERVICE_NAME: &str = "codex-app-server";
|
||||
@@ -378,6 +383,7 @@ pub async fn run_main(
|
||||
default_analytics_enabled,
|
||||
AppServerTransport::Stdio,
|
||||
SessionSource::VSCode,
|
||||
AppServerWebsocketAuthSettings::default(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
@@ -410,6 +416,7 @@ pub async fn run_main_with_transport(
|
||||
default_analytics_enabled: bool,
|
||||
transport: AppServerTransport,
|
||||
session_source: SessionSource,
|
||||
auth: AppServerWebsocketAuthSettings,
|
||||
) -> IoResult<()> {
|
||||
run_main_with_transport_options(
|
||||
arg0_paths,
|
||||
@@ -418,6 +425,7 @@ pub async fn run_main_with_transport(
|
||||
default_analytics_enabled,
|
||||
transport,
|
||||
session_source,
|
||||
auth,
|
||||
AppServerRuntimeOptions::default(),
|
||||
)
|
||||
.await
|
||||
@@ -431,6 +439,7 @@ pub async fn run_main_with_transport_options(
|
||||
default_analytics_enabled: bool,
|
||||
transport: AppServerTransport,
|
||||
session_source: SessionSource,
|
||||
auth: AppServerWebsocketAuthSettings,
|
||||
runtime_options: AppServerRuntimeOptions,
|
||||
) -> IoResult<()> {
|
||||
let (transport_event_tx, mut transport_event_rx) =
|
||||
@@ -670,6 +679,16 @@ pub async fn run_main_with_transport_options(
|
||||
.await?;
|
||||
transport_accept_handles.push(accept_handle);
|
||||
}
|
||||
AppServerTransport::WebSocket { bind_address } => {
|
||||
let accept_handle = start_websocket_acceptor(
|
||||
*bind_address,
|
||||
transport_event_tx.clone(),
|
||||
transport_shutdown_token.clone(),
|
||||
policy_from_settings(&auth)?,
|
||||
)
|
||||
.await?;
|
||||
transport_accept_handles.push(accept_handle);
|
||||
}
|
||||
AppServerTransport::Off => {}
|
||||
}
|
||||
|
||||
@@ -741,7 +760,7 @@ pub async fn run_main_with_transport_options(
|
||||
}
|
||||
OutboundControlEvent::DisconnectAll => {
|
||||
info!(
|
||||
"disconnecting {} outbound connection(s) for graceful restart",
|
||||
"disconnecting {} outbound websocket connection(s) for graceful restart",
|
||||
outbound_connections.len()
|
||||
);
|
||||
for connection_state in outbound_connections.values() {
|
||||
@@ -1068,9 +1087,9 @@ pub async fn run_main_with_transport_options(
|
||||
fn analytics_rpc_transport(transport: &AppServerTransport) -> AppServerRpcTransport {
|
||||
match transport {
|
||||
AppServerTransport::Stdio => AppServerRpcTransport::Stdio,
|
||||
AppServerTransport::UnixSocket { .. } | AppServerTransport::Off => {
|
||||
AppServerRpcTransport::Websocket
|
||||
}
|
||||
AppServerTransport::UnixSocket { .. }
|
||||
| AppServerTransport::WebSocket { .. }
|
||||
| AppServerTransport::Off => AppServerRpcTransport::Websocket,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use clap::Parser;
|
||||
use codex_app_server::AppServerRuntimeOptions;
|
||||
use codex_app_server::AppServerTransport;
|
||||
use codex_app_server::AppServerWebsocketAuthArgs;
|
||||
use codex_app_server::PluginStartupTasks;
|
||||
use codex_app_server::run_main_with_transport_options;
|
||||
use codex_arg0::Arg0DispatchPaths;
|
||||
@@ -18,7 +19,7 @@ const DISABLE_MANAGED_CONFIG_ENV_VAR: &str = "CODEX_APP_SERVER_DISABLE_MANAGED_C
|
||||
#[derive(Debug, Parser)]
|
||||
struct AppServerArgs {
|
||||
/// Transport endpoint URL. Supported values: `stdio://` (default),
|
||||
/// `unix://`, `unix://PATH`, `off`.
|
||||
/// `unix://`, `unix://PATH`, `ws://IP:PORT`, `off`.
|
||||
#[arg(
|
||||
long = "listen",
|
||||
value_name = "URL",
|
||||
@@ -35,6 +36,9 @@ struct AppServerArgs {
|
||||
)]
|
||||
session_source: SessionSource,
|
||||
|
||||
#[command(flatten)]
|
||||
auth: AppServerWebsocketAuthArgs,
|
||||
|
||||
/// Hidden debug-only test hook used by integration tests that spawn the
|
||||
/// production app-server binary.
|
||||
#[cfg(debug_assertions)]
|
||||
@@ -58,6 +62,7 @@ fn main() -> anyhow::Result<()> {
|
||||
};
|
||||
let transport = args.listen;
|
||||
let session_source = args.session_source;
|
||||
let auth = args.auth.try_into_settings()?;
|
||||
let mut runtime_options = AppServerRuntimeOptions::default();
|
||||
#[cfg(debug_assertions)]
|
||||
if args.disable_plugin_startup_tasks_for_tests {
|
||||
@@ -72,6 +77,7 @@ fn main() -> anyhow::Result<()> {
|
||||
/*default_analytics_enabled*/ false,
|
||||
transport,
|
||||
session_source,
|
||||
auth,
|
||||
runtime_options,
|
||||
)
|
||||
.await?;
|
||||
|
||||
@@ -21,9 +21,11 @@ pub(crate) use codex_app_server_transport::QueuedOutgoingMessage;
|
||||
pub(crate) use codex_app_server_transport::RemoteControlStartConfig;
|
||||
pub(crate) use codex_app_server_transport::TransportEvent;
|
||||
pub use codex_app_server_transport::app_server_control_socket_path;
|
||||
pub use codex_app_server_transport::auth;
|
||||
pub(crate) use codex_app_server_transport::start_control_socket_acceptor;
|
||||
pub(crate) use codex_app_server_transport::start_remote_control;
|
||||
pub(crate) use codex_app_server_transport::start_stdio_connection;
|
||||
pub(crate) use codex_app_server_transport::start_websocket_acceptor;
|
||||
|
||||
pub(crate) struct ConnectionState {
|
||||
pub(crate) outbound_initialized: Arc<AtomicBool>,
|
||||
|
||||
@@ -861,10 +861,10 @@ async fn command_exec_process_ids_are_connection_scoped_and_disconnect_terminate
|
||||
.as_nanos()
|
||||
);
|
||||
|
||||
let (mut process, socket_path, _socket_dir) = spawn_websocket_server(codex_home.path()).await?;
|
||||
let (mut process, bind_addr) = spawn_websocket_server(codex_home.path()).await?;
|
||||
|
||||
let mut ws1 = connect_websocket(&socket_path).await?;
|
||||
let mut ws2 = connect_websocket(&socket_path).await?;
|
||||
let mut ws1 = connect_websocket(bind_addr).await?;
|
||||
let mut ws2 = connect_websocket(bind_addr).await?;
|
||||
|
||||
send_initialize_request(&mut ws1, /*id*/ 1, "ws_client_one").await?;
|
||||
read_initialize_response(&mut ws1, /*request_id*/ 1).await?;
|
||||
@@ -929,7 +929,7 @@ async fn command_exec_process_ids_are_connection_scoped_and_disconnect_terminate
|
||||
process
|
||||
.kill()
|
||||
.await
|
||||
.context("failed to stop app-server process")?;
|
||||
.context("failed to stop websocket app-server process")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ use anyhow::bail;
|
||||
use app_test_support::DISABLE_PLUGIN_STARTUP_TASKS_ARG;
|
||||
use app_test_support::create_mock_responses_server_sequence_unchecked;
|
||||
use app_test_support::to_response;
|
||||
use base64::Engine;
|
||||
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
|
||||
use codex_app_server_protocol::ClientInfo;
|
||||
use codex_app_server_protocol::InitializeParams;
|
||||
use codex_app_server_protocol::JSONRPCError;
|
||||
@@ -16,14 +18,18 @@ use codex_app_server_protocol::ThreadLoadedListParams;
|
||||
use codex_app_server_protocol::ThreadLoadedListResponse;
|
||||
use codex_app_server_protocol::ThreadStartParams;
|
||||
use codex_app_server_protocol::ThreadStartResponse;
|
||||
use codex_uds::UnixStream;
|
||||
use futures::SinkExt;
|
||||
use futures::StreamExt;
|
||||
use hmac::Hmac;
|
||||
use hmac::Mac;
|
||||
use reqwest::StatusCode;
|
||||
use serde_json::json;
|
||||
use sha2::Sha256;
|
||||
use std::net::SocketAddr;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::process::Stdio;
|
||||
use tempfile::TempDir;
|
||||
use time::OffsetDateTime;
|
||||
use tokio::io::AsyncBufReadExt;
|
||||
use tokio::io::BufReader;
|
||||
use tokio::process::Child;
|
||||
@@ -32,29 +38,37 @@ use tokio::time::Duration;
|
||||
use tokio::time::Instant;
|
||||
use tokio::time::sleep;
|
||||
use tokio::time::timeout;
|
||||
use tokio_tungstenite::MaybeTlsStream;
|
||||
use tokio_tungstenite::WebSocketStream;
|
||||
use tokio_tungstenite::client_async;
|
||||
use tokio_tungstenite::connect_async;
|
||||
use tokio_tungstenite::tungstenite::Error as WsError;
|
||||
use tokio_tungstenite::tungstenite::Message as WebSocketMessage;
|
||||
use tokio_tungstenite::tungstenite::client::IntoClientRequest;
|
||||
use tokio_tungstenite::tungstenite::http::HeaderValue;
|
||||
use tokio_tungstenite::tungstenite::http::header::AUTHORIZATION;
|
||||
use tokio_tungstenite::tungstenite::http::header::ORIGIN;
|
||||
|
||||
// macOS and Windows CI can spend tens of seconds starting the app-server test
|
||||
// binary under Bazel before it accepts JSON-RPC over the control socket.
|
||||
// binary under Bazel before it accepts JSON-RPC or reports its websocket bind
|
||||
// address.
|
||||
#[cfg(any(target_os = "macos", windows))]
|
||||
pub(super) const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(60);
|
||||
#[cfg(not(any(target_os = "macos", windows)))]
|
||||
pub(super) const DEFAULT_READ_TIMEOUT: Duration = Duration::from_secs(10);
|
||||
|
||||
pub(super) type WsClient = WebSocketStream<UnixStream>;
|
||||
pub(super) type WsClient = WebSocketStream<MaybeTlsStream<tokio::net::TcpStream>>;
|
||||
type HmacSha256 = Hmac<Sha256>;
|
||||
|
||||
#[tokio::test]
|
||||
async fn unix_socket_transport_routes_per_connection_handshake_and_responses() -> Result<()> {
|
||||
async fn websocket_transport_routes_per_connection_handshake_and_responses() -> Result<()> {
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
|
||||
let (mut process, socket_path, _socket_dir) = spawn_websocket_server(codex_home.path()).await?;
|
||||
let (mut process, bind_addr) = spawn_websocket_server(codex_home.path()).await?;
|
||||
|
||||
let mut ws1 = connect_websocket(&socket_path).await?;
|
||||
let mut ws2 = connect_websocket(&socket_path).await?;
|
||||
let mut ws1 = connect_websocket(bind_addr).await?;
|
||||
let mut ws2 = connect_websocket(bind_addr).await?;
|
||||
|
||||
send_initialize_request(&mut ws1, /*id*/ 1, "ws_client_one").await?;
|
||||
let first_init = read_response_for_id(&mut ws1, /*id*/ 1).await?;
|
||||
@@ -86,20 +100,259 @@ async fn unix_socket_transport_routes_per_connection_handshake_and_responses() -
|
||||
process
|
||||
.kill()
|
||||
.await
|
||||
.context("failed to stop app-server process")?;
|
||||
.context("failed to stop websocket app-server process")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unix_socket_disconnect_keeps_last_subscribed_thread_loaded_until_idle_timeout()
|
||||
-> Result<()> {
|
||||
async fn websocket_transport_serves_health_endpoints_on_same_listener() -> Result<()> {
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
|
||||
let (mut process, socket_path, _socket_dir) = spawn_websocket_server(codex_home.path()).await?;
|
||||
let (mut process, bind_addr) = spawn_websocket_server(codex_home.path()).await?;
|
||||
let client = reqwest::Client::new();
|
||||
|
||||
let mut ws1 = connect_websocket(&socket_path).await?;
|
||||
let readyz = http_get(&client, bind_addr, "/readyz").await?;
|
||||
assert_eq!(readyz.status(), StatusCode::OK);
|
||||
|
||||
let healthz = http_get(&client, bind_addr, "/healthz").await?;
|
||||
assert_eq!(healthz.status(), StatusCode::OK);
|
||||
|
||||
let mut ws = connect_websocket(bind_addr).await?;
|
||||
send_initialize_request(&mut ws, /*id*/ 1, "ws_health_client").await?;
|
||||
let init = read_response_for_id(&mut ws, /*id*/ 1).await?;
|
||||
assert_eq!(init.id, RequestId::Integer(1));
|
||||
|
||||
process
|
||||
.kill()
|
||||
.await
|
||||
.context("failed to stop websocket app-server process")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn websocket_transport_rejects_browser_origin_without_auth() -> Result<()> {
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
|
||||
let (mut process, bind_addr) = spawn_websocket_server(codex_home.path()).await?;
|
||||
|
||||
let mut ws = connect_websocket(bind_addr).await?;
|
||||
send_initialize_request(&mut ws, /*id*/ 1, "ws_loopback_client").await?;
|
||||
let init = read_response_for_id(&mut ws, /*id*/ 1).await?;
|
||||
assert_eq!(init.id, RequestId::Integer(1));
|
||||
drop(ws);
|
||||
|
||||
assert_websocket_connect_rejected_with_headers(
|
||||
bind_addr,
|
||||
/*bearer_token*/ None,
|
||||
Some("https://evil.example"),
|
||||
StatusCode::FORBIDDEN,
|
||||
)
|
||||
.await?;
|
||||
|
||||
process
|
||||
.kill()
|
||||
.await
|
||||
.context("failed to stop websocket app-server process")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn websocket_transport_rejects_missing_and_invalid_capability_tokens() -> Result<()> {
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
let token_file = codex_home.path().join("app-server-token");
|
||||
std::fs::write(&token_file, "super-secret-token\n")?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
let auth_args = vec![
|
||||
"--ws-auth".to_string(),
|
||||
"capability-token".to_string(),
|
||||
"--ws-token-file".to_string(),
|
||||
token_file.display().to_string(),
|
||||
];
|
||||
|
||||
let (mut process, bind_addr) =
|
||||
spawn_websocket_server_with_args(codex_home.path(), "ws://0.0.0.0:0", &auth_args).await?;
|
||||
|
||||
assert_websocket_connect_rejected(bind_addr, /*bearer_token*/ None).await?;
|
||||
assert_websocket_connect_rejected(bind_addr, Some("wrong-token")).await?;
|
||||
|
||||
let mut ws = connect_websocket_with_bearer(bind_addr, Some("super-secret-token")).await?;
|
||||
send_initialize_request(&mut ws, /*id*/ 1, "ws_auth_client").await?;
|
||||
let init = read_response_for_id(&mut ws, /*id*/ 1).await?;
|
||||
assert_eq!(init.id, RequestId::Integer(1));
|
||||
|
||||
process
|
||||
.kill()
|
||||
.await
|
||||
.context("failed to stop websocket app-server process")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn websocket_transport_verifies_signed_short_lived_bearer_tokens() -> Result<()> {
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
let shared_secret_file = codex_home.path().join("app-server-signing-secret");
|
||||
let shared_secret = "0123456789abcdef0123456789abcdef";
|
||||
std::fs::write(&shared_secret_file, format!("{shared_secret}\n"))?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
let auth_args = vec![
|
||||
"--ws-auth".to_string(),
|
||||
"signed-bearer-token".to_string(),
|
||||
"--ws-shared-secret-file".to_string(),
|
||||
shared_secret_file.display().to_string(),
|
||||
"--ws-issuer".to_string(),
|
||||
"codex-enroller".to_string(),
|
||||
"--ws-audience".to_string(),
|
||||
"codex-app-server".to_string(),
|
||||
"--ws-max-clock-skew-seconds".to_string(),
|
||||
"1".to_string(),
|
||||
];
|
||||
|
||||
let (mut process, bind_addr) =
|
||||
spawn_websocket_server_with_args(codex_home.path(), "ws://127.0.0.1:0", &auth_args).await?;
|
||||
let expired_token = signed_bearer_token(
|
||||
shared_secret.as_bytes(),
|
||||
json!({
|
||||
"exp": OffsetDateTime::now_utc().unix_timestamp() - 30,
|
||||
"iss": "codex-enroller",
|
||||
"aud": "codex-app-server",
|
||||
}),
|
||||
)?;
|
||||
assert_websocket_connect_rejected(bind_addr, Some(expired_token.as_str())).await?;
|
||||
|
||||
let malformed_token = "not-a-jwt";
|
||||
assert_websocket_connect_rejected(bind_addr, Some(malformed_token)).await?;
|
||||
|
||||
let not_yet_valid_token = signed_bearer_token(
|
||||
shared_secret.as_bytes(),
|
||||
json!({
|
||||
"exp": OffsetDateTime::now_utc().unix_timestamp() + 60,
|
||||
"nbf": OffsetDateTime::now_utc().unix_timestamp() + 30,
|
||||
"iss": "codex-enroller",
|
||||
"aud": "codex-app-server",
|
||||
}),
|
||||
)?;
|
||||
assert_websocket_connect_rejected(bind_addr, Some(not_yet_valid_token.as_str())).await?;
|
||||
|
||||
let wrong_issuer_token = signed_bearer_token(
|
||||
shared_secret.as_bytes(),
|
||||
json!({
|
||||
"exp": OffsetDateTime::now_utc().unix_timestamp() + 60,
|
||||
"iss": "someone-else",
|
||||
"aud": "codex-app-server",
|
||||
}),
|
||||
)?;
|
||||
assert_websocket_connect_rejected(bind_addr, Some(wrong_issuer_token.as_str())).await?;
|
||||
|
||||
let wrong_audience_token = signed_bearer_token(
|
||||
shared_secret.as_bytes(),
|
||||
json!({
|
||||
"exp": OffsetDateTime::now_utc().unix_timestamp() + 60,
|
||||
"iss": "codex-enroller",
|
||||
"aud": "wrong-audience",
|
||||
}),
|
||||
)?;
|
||||
assert_websocket_connect_rejected(bind_addr, Some(wrong_audience_token.as_str())).await?;
|
||||
|
||||
let wrong_signature_token = signed_bearer_token(
|
||||
b"fedcba9876543210fedcba9876543210",
|
||||
json!({
|
||||
"exp": OffsetDateTime::now_utc().unix_timestamp() + 60,
|
||||
"iss": "codex-enroller",
|
||||
"aud": "codex-app-server",
|
||||
}),
|
||||
)?;
|
||||
assert_websocket_connect_rejected(bind_addr, Some(wrong_signature_token.as_str())).await?;
|
||||
|
||||
let valid_token = signed_bearer_token(
|
||||
shared_secret.as_bytes(),
|
||||
json!({
|
||||
"exp": OffsetDateTime::now_utc().unix_timestamp() + 60,
|
||||
"iss": "codex-enroller",
|
||||
"aud": "codex-app-server",
|
||||
}),
|
||||
)?;
|
||||
let mut ws = connect_websocket_with_bearer(bind_addr, Some(valid_token.as_str())).await?;
|
||||
send_initialize_request(&mut ws, /*id*/ 1, "ws_signed_auth_client").await?;
|
||||
let init = read_response_for_id(&mut ws, /*id*/ 1).await?;
|
||||
assert_eq!(init.id, RequestId::Integer(1));
|
||||
|
||||
process
|
||||
.kill()
|
||||
.await
|
||||
.context("failed to stop websocket app-server process")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn websocket_transport_rejects_short_signed_bearer_secret_configuration() -> Result<()> {
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
let shared_secret_file = codex_home.path().join("app-server-signing-secret");
|
||||
std::fs::write(&shared_secret_file, "too-short\n")?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
|
||||
let output = run_websocket_server_to_completion_with_args(
|
||||
codex_home.path(),
|
||||
"ws://127.0.0.1:0",
|
||||
&[
|
||||
"--ws-auth".to_string(),
|
||||
"signed-bearer-token".to_string(),
|
||||
"--ws-shared-secret-file".to_string(),
|
||||
shared_secret_file.display().to_string(),
|
||||
],
|
||||
)
|
||||
.await?;
|
||||
assert!(
|
||||
!output.status.success(),
|
||||
"short shared secret should fail websocket server startup"
|
||||
);
|
||||
let stderr = String::from_utf8(output.stderr).context("stderr should be valid utf-8")?;
|
||||
assert!(
|
||||
stderr.contains("must be at least 32 bytes"),
|
||||
"unexpected stderr: {stderr}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn websocket_transport_rejects_unauthenticated_non_loopback_startup() -> Result<()> {
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
|
||||
let output =
|
||||
run_websocket_server_to_completion_with_args(codex_home.path(), "ws://0.0.0.0:0", &[])
|
||||
.await?;
|
||||
assert!(
|
||||
!output.status.success(),
|
||||
"unauthenticated non-loopback listener should fail websocket server startup"
|
||||
);
|
||||
let stderr = String::from_utf8(output.stderr).context("stderr should be valid utf-8")?;
|
||||
assert!(
|
||||
stderr.contains("refusing to start non-loopback websocket listener"),
|
||||
"unexpected stderr: {stderr}"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn websocket_disconnect_keeps_last_subscribed_thread_loaded_until_idle_timeout() -> Result<()>
|
||||
{
|
||||
let server = create_mock_responses_server_sequence_unchecked(Vec::new()).await;
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
|
||||
let (mut process, bind_addr) = spawn_websocket_server(codex_home.path()).await?;
|
||||
|
||||
let mut ws1 = connect_websocket(bind_addr).await?;
|
||||
send_initialize_request(&mut ws1, /*id*/ 1, "ws_thread_owner").await?;
|
||||
read_response_for_id(&mut ws1, /*id*/ 1).await?;
|
||||
|
||||
@@ -109,7 +362,7 @@ async fn unix_socket_disconnect_keeps_last_subscribed_thread_loaded_until_idle_t
|
||||
ws1.close(None).await.context("failed to close websocket")?;
|
||||
drop(ws1);
|
||||
|
||||
let mut ws2 = connect_websocket(&socket_path).await?;
|
||||
let mut ws2 = connect_websocket(bind_addr).await?;
|
||||
send_initialize_request(&mut ws2, /*id*/ 4, "ws_reconnect_client").await?;
|
||||
read_response_for_id(&mut ws2, /*id*/ 4).await?;
|
||||
|
||||
@@ -118,29 +371,26 @@ async fn unix_socket_disconnect_keeps_last_subscribed_thread_loaded_until_idle_t
|
||||
process
|
||||
.kill()
|
||||
.await
|
||||
.context("failed to stop app-server process")?;
|
||||
.context("failed to stop websocket app-server process")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) async fn spawn_websocket_server(codex_home: &Path) -> Result<(Child, PathBuf, TempDir)> {
|
||||
pub(super) async fn spawn_websocket_server(codex_home: &Path) -> Result<(Child, SocketAddr)> {
|
||||
spawn_websocket_server_with_args(codex_home, "ws://127.0.0.1:0", &[]).await
|
||||
}
|
||||
|
||||
pub(super) async fn spawn_websocket_server_with_args(
|
||||
codex_home: &Path,
|
||||
listen_url: &str,
|
||||
extra_args: &[String],
|
||||
) -> Result<(Child, SocketAddr)> {
|
||||
let program = codex_utils_cargo_bin::cargo_bin("codex-app-server")
|
||||
.context("should find app-server binary")?;
|
||||
#[cfg(unix)]
|
||||
let socket_dir = tempfile::Builder::new()
|
||||
.prefix("cxs-")
|
||||
.tempdir_in("/tmp")
|
||||
.context("failed to create short app-server socket temp dir")?;
|
||||
#[cfg(not(unix))]
|
||||
let socket_dir = tempfile::Builder::new()
|
||||
.prefix("cxs-")
|
||||
.tempdir()
|
||||
.context("failed to create app-server socket temp dir")?;
|
||||
let socket_path = socket_dir.path().join("c.sock");
|
||||
let listen_url = format!("unix://{}", socket_path.display());
|
||||
let mut cmd = Command::new(program);
|
||||
cmd.arg("--listen")
|
||||
.arg(&listen_url)
|
||||
.arg(listen_url)
|
||||
.arg(DISABLE_PLUGIN_STARTUP_TASKS_ARG)
|
||||
.args(extra_args)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::piped())
|
||||
@@ -149,62 +399,194 @@ pub(super) async fn spawn_websocket_server(codex_home: &Path) -> Result<(Child,
|
||||
let mut process = cmd
|
||||
.kill_on_drop(true)
|
||||
.spawn()
|
||||
.context("failed to spawn app-server process")?;
|
||||
.context("failed to spawn websocket app-server process")?;
|
||||
|
||||
let stderr = process
|
||||
.stderr
|
||||
.take()
|
||||
.context("failed to capture app-server stderr")?;
|
||||
.context("failed to capture websocket app-server stderr")?;
|
||||
let mut stderr_reader = BufReader::new(stderr).lines();
|
||||
let deadline = Instant::now() + DEFAULT_READ_TIMEOUT;
|
||||
let bind_addr = loop {
|
||||
let line = timeout(
|
||||
deadline.saturating_duration_since(Instant::now()),
|
||||
stderr_reader.next_line(),
|
||||
)
|
||||
.await
|
||||
.context("timed out waiting for websocket app-server to report bound websocket address")?
|
||||
.context("failed to read websocket app-server stderr")?
|
||||
.context("websocket app-server exited before reporting bound websocket address")?;
|
||||
eprintln!("[websocket app-server stderr] {line}");
|
||||
|
||||
let stripped_line = {
|
||||
let mut stripped = String::with_capacity(line.len());
|
||||
let mut chars = line.chars().peekable();
|
||||
while let Some(ch) = chars.next() {
|
||||
if ch == '\u{1b}' && matches!(chars.peek(), Some(&'[')) {
|
||||
chars.next();
|
||||
for next in chars.by_ref() {
|
||||
if ('@'..='~').contains(&next) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
stripped.push(ch);
|
||||
}
|
||||
stripped
|
||||
};
|
||||
|
||||
if let Some(bind_addr) = stripped_line
|
||||
.split_whitespace()
|
||||
.find_map(|token| token.strip_prefix("ws://"))
|
||||
.and_then(|addr| addr.parse::<SocketAddr>().ok())
|
||||
{
|
||||
break bind_addr;
|
||||
}
|
||||
};
|
||||
|
||||
tokio::spawn(async move {
|
||||
while let Ok(Some(line)) = stderr_reader.next_line().await {
|
||||
eprintln!("[app-server stderr] {line}");
|
||||
eprintln!("[websocket app-server stderr] {line}");
|
||||
}
|
||||
});
|
||||
|
||||
Ok((process, bind_addr))
|
||||
}
|
||||
|
||||
pub(super) async fn connect_websocket(bind_addr: SocketAddr) -> Result<WsClient> {
|
||||
connect_websocket_with_bearer(bind_addr, /*bearer_token*/ None).await
|
||||
}
|
||||
|
||||
pub(super) async fn connect_websocket_with_bearer(
|
||||
bind_addr: SocketAddr,
|
||||
bearer_token: Option<&str>,
|
||||
) -> Result<WsClient> {
|
||||
let url = format!("ws://{}", connectable_bind_addr(bind_addr));
|
||||
let request = websocket_request(url.as_str(), bearer_token, /*origin*/ None)?;
|
||||
let deadline = Instant::now() + DEFAULT_READ_TIMEOUT;
|
||||
loop {
|
||||
if socket_path.exists() {
|
||||
return Ok((process, socket_path, socket_dir));
|
||||
match connect_async(request.clone()).await {
|
||||
Ok((stream, _response)) => return Ok(stream),
|
||||
Err(err) => {
|
||||
if Instant::now() >= deadline {
|
||||
bail!("failed to connect websocket to {url}: {err}");
|
||||
}
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
}
|
||||
}
|
||||
if let Some(status) = process.try_wait()? {
|
||||
bail!("app-server exited before creating control socket: {status}");
|
||||
}
|
||||
if Instant::now() >= deadline {
|
||||
bail!(
|
||||
"timed out waiting for app-server control socket at {}",
|
||||
socket_path.display()
|
||||
);
|
||||
}
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn connect_websocket(socket_path: &Path) -> Result<WsClient> {
|
||||
async fn assert_websocket_connect_rejected(
|
||||
bind_addr: SocketAddr,
|
||||
bearer_token: Option<&str>,
|
||||
) -> Result<()> {
|
||||
assert_websocket_connect_rejected_with_headers(
|
||||
bind_addr,
|
||||
bearer_token,
|
||||
/*origin*/ None,
|
||||
StatusCode::UNAUTHORIZED,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn assert_websocket_connect_rejected_with_headers(
|
||||
bind_addr: SocketAddr,
|
||||
bearer_token: Option<&str>,
|
||||
origin: Option<&str>,
|
||||
expected_status: StatusCode,
|
||||
) -> Result<()> {
|
||||
let url = format!("ws://{}", connectable_bind_addr(bind_addr));
|
||||
let request = websocket_request(url.as_str(), bearer_token, origin)?;
|
||||
|
||||
match connect_async(request).await {
|
||||
Ok((_stream, response)) => {
|
||||
bail!(
|
||||
"expected websocket handshake rejection, got {}",
|
||||
response.status()
|
||||
)
|
||||
}
|
||||
Err(WsError::Http(response)) => {
|
||||
assert_eq!(response.status(), expected_status);
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => bail!("expected http rejection during websocket handshake: {err}"),
|
||||
}
|
||||
}
|
||||
|
||||
async fn run_websocket_server_to_completion_with_args(
|
||||
codex_home: &Path,
|
||||
listen_url: &str,
|
||||
extra_args: &[String],
|
||||
) -> Result<std::process::Output> {
|
||||
let program = codex_utils_cargo_bin::cargo_bin("codex-app-server")
|
||||
.context("should find app-server binary")?;
|
||||
let mut cmd = Command::new(program);
|
||||
cmd.arg("--listen")
|
||||
.arg(listen_url)
|
||||
.arg(DISABLE_PLUGIN_STARTUP_TASKS_ARG)
|
||||
.args(extra_args)
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::null())
|
||||
.stderr(Stdio::piped())
|
||||
.env("CODEX_HOME", codex_home)
|
||||
.env("RUST_LOG", "warn");
|
||||
timeout(DEFAULT_READ_TIMEOUT, cmd.output())
|
||||
.await
|
||||
.context("timed out waiting for websocket app-server to exit")?
|
||||
.context("failed to run websocket app-server")
|
||||
}
|
||||
|
||||
async fn http_get(
|
||||
client: &reqwest::Client,
|
||||
bind_addr: SocketAddr,
|
||||
path: &str,
|
||||
) -> Result<reqwest::Response> {
|
||||
let connectable_bind_addr = connectable_bind_addr(bind_addr);
|
||||
let deadline = Instant::now() + DEFAULT_READ_TIMEOUT;
|
||||
loop {
|
||||
match UnixStream::connect(socket_path).await {
|
||||
Ok(stream) => match client_async("ws://localhost/rpc", stream).await {
|
||||
Ok((websocket, _response)) => return Ok(websocket),
|
||||
Err(err) => {
|
||||
if Instant::now() >= deadline {
|
||||
bail!(
|
||||
"failed to upgrade websocket over {}: {err}",
|
||||
socket_path.display()
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
match client
|
||||
.get(format!("http://{connectable_bind_addr}{path}"))
|
||||
.send()
|
||||
.await
|
||||
.with_context(|| format!("failed to GET http://{connectable_bind_addr}{path}"))
|
||||
{
|
||||
Ok(response) => return Ok(response),
|
||||
Err(err) => {
|
||||
if Instant::now() >= deadline {
|
||||
bail!("failed to connect to {}: {err}", socket_path.display());
|
||||
bail!("failed to GET http://{connectable_bind_addr}{path}: {err}");
|
||||
}
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
}
|
||||
}
|
||||
sleep(Duration::from_millis(50)).await;
|
||||
}
|
||||
}
|
||||
|
||||
fn websocket_request(
|
||||
url: &str,
|
||||
bearer_token: Option<&str>,
|
||||
origin: Option<&str>,
|
||||
) -> Result<tokio_tungstenite::tungstenite::http::Request<()>> {
|
||||
let mut request = url
|
||||
.into_client_request()
|
||||
.context("failed to create websocket request")?;
|
||||
if let Some(bearer_token) = bearer_token {
|
||||
request.headers_mut().insert(
|
||||
AUTHORIZATION,
|
||||
HeaderValue::from_str(&format!("Bearer {bearer_token}"))
|
||||
.context("invalid bearer token header")?,
|
||||
);
|
||||
}
|
||||
if let Some(origin) = origin {
|
||||
request.headers_mut().insert(
|
||||
ORIGIN,
|
||||
HeaderValue::from_str(origin).context("invalid origin header")?,
|
||||
);
|
||||
}
|
||||
Ok(request)
|
||||
}
|
||||
|
||||
pub(super) async fn send_initialize_request(
|
||||
stream: &mut WsClient,
|
||||
id: i64,
|
||||
@@ -467,3 +849,25 @@ stream_max_retries = 0
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
fn connectable_bind_addr(bind_addr: SocketAddr) -> SocketAddr {
|
||||
match bind_addr {
|
||||
SocketAddr::V4(addr) if addr.ip().is_unspecified() => {
|
||||
SocketAddr::from(([127, 0, 0, 1], addr.port()))
|
||||
}
|
||||
SocketAddr::V6(addr) if addr.ip().is_unspecified() => {
|
||||
SocketAddr::from(([0, 0, 0, 0, 0, 0, 0, 1], addr.port()))
|
||||
}
|
||||
_ => bind_addr,
|
||||
}
|
||||
}
|
||||
|
||||
fn signed_bearer_token(shared_secret: &[u8], claims: serde_json::Value) -> Result<String> {
|
||||
let header_segment = URL_SAFE_NO_PAD.encode(br#"{"alg":"HS256","typ":"JWT"}"#);
|
||||
let claims_segment = URL_SAFE_NO_PAD.encode(serde_json::to_vec(&claims)?);
|
||||
let payload = format!("{header_segment}.{claims_segment}");
|
||||
let mut mac = HmacSha256::new_from_slice(shared_secret).context("failed to create hmac")?;
|
||||
mac.update(payload.as_bytes());
|
||||
let signature = URL_SAFE_NO_PAD.encode(mac.finalize().into_bytes());
|
||||
Ok(format!("{payload}.{signature}"))
|
||||
}
|
||||
|
||||
@@ -32,13 +32,12 @@ use wiremock::matchers::method;
|
||||
use wiremock::matchers::path_regex;
|
||||
|
||||
#[tokio::test]
|
||||
async fn unix_socket_transport_ctrl_c_waits_for_running_turn_before_exit() -> Result<()> {
|
||||
async fn websocket_transport_ctrl_c_waits_for_running_turn_before_exit() -> Result<()> {
|
||||
let GracefulCtrlCFixture {
|
||||
_codex_home,
|
||||
_server,
|
||||
mut process,
|
||||
mut ws,
|
||||
..
|
||||
} = start_ctrl_c_restart_fixture(Duration::from_secs(3)).await?;
|
||||
|
||||
send_sigint(&process)?;
|
||||
@@ -58,13 +57,12 @@ async fn unix_socket_transport_ctrl_c_waits_for_running_turn_before_exit() -> Re
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unix_socket_transport_second_ctrl_c_forces_exit_while_turn_running() -> Result<()> {
|
||||
async fn websocket_transport_second_ctrl_c_forces_exit_while_turn_running() -> Result<()> {
|
||||
let GracefulCtrlCFixture {
|
||||
_codex_home,
|
||||
_server,
|
||||
mut process,
|
||||
mut ws,
|
||||
..
|
||||
} = start_ctrl_c_restart_fixture(Duration::from_secs(3)).await?;
|
||||
|
||||
send_sigint(&process)?;
|
||||
@@ -85,13 +83,12 @@ async fn unix_socket_transport_second_ctrl_c_forces_exit_while_turn_running() ->
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unix_socket_transport_sigterm_waits_for_running_turn_before_exit() -> Result<()> {
|
||||
async fn websocket_transport_sigterm_waits_for_running_turn_before_exit() -> Result<()> {
|
||||
let GracefulCtrlCFixture {
|
||||
_codex_home,
|
||||
_server,
|
||||
mut process,
|
||||
mut ws,
|
||||
..
|
||||
} = start_ctrl_c_restart_fixture(Duration::from_secs(3)).await?;
|
||||
|
||||
send_sigterm(&process)?;
|
||||
@@ -111,13 +108,12 @@ async fn unix_socket_transport_sigterm_waits_for_running_turn_before_exit() -> R
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn unix_socket_transport_second_sigterm_forces_exit_while_turn_running() -> Result<()> {
|
||||
async fn websocket_transport_second_sigterm_forces_exit_while_turn_running() -> Result<()> {
|
||||
let GracefulCtrlCFixture {
|
||||
_codex_home,
|
||||
_server,
|
||||
mut process,
|
||||
mut ws,
|
||||
..
|
||||
} = start_ctrl_c_restart_fixture(Duration::from_secs(3)).await?;
|
||||
|
||||
send_sigterm(&process)?;
|
||||
@@ -144,7 +140,6 @@ async fn websocket_transport_repeated_sighup_keeps_waiting_for_running_turn() ->
|
||||
_server,
|
||||
mut process,
|
||||
mut ws,
|
||||
..
|
||||
} = start_ctrl_c_restart_fixture(Duration::from_secs(3)).await?;
|
||||
|
||||
send_sighup(&process)?;
|
||||
@@ -168,7 +163,6 @@ async fn websocket_transport_repeated_sighup_keeps_waiting_for_running_turn() ->
|
||||
|
||||
struct GracefulCtrlCFixture {
|
||||
_codex_home: TempDir,
|
||||
_socket_dir: TempDir,
|
||||
_server: wiremock::MockServer,
|
||||
process: Child,
|
||||
ws: WsClient,
|
||||
@@ -187,8 +181,8 @@ async fn start_ctrl_c_restart_fixture(turn_delay: Duration) -> Result<GracefulCt
|
||||
let codex_home = TempDir::new()?;
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
|
||||
let (process, socket_path, socket_dir) = spawn_websocket_server(codex_home.path()).await?;
|
||||
let mut ws = connect_websocket(&socket_path).await?;
|
||||
let (process, bind_addr) = spawn_websocket_server(codex_home.path()).await?;
|
||||
let mut ws = connect_websocket(bind_addr).await?;
|
||||
|
||||
send_initialize_request(&mut ws, /*id*/ 1, "ws_graceful_shutdown").await?;
|
||||
let init_response = read_response_for_id(&mut ws, /*id*/ 1).await?;
|
||||
@@ -206,7 +200,6 @@ async fn start_ctrl_c_restart_fixture(turn_delay: Duration) -> Result<GracefulCt
|
||||
|
||||
Ok(GracefulCtrlCFixture {
|
||||
_codex_home: codex_home,
|
||||
_socket_dir: socket_dir,
|
||||
_server: server,
|
||||
process,
|
||||
ws,
|
||||
@@ -276,7 +269,9 @@ fn send_sighup(process: &Child) -> Result<()> {
|
||||
}
|
||||
|
||||
fn send_signal(process: &Child, signal: &str) -> Result<()> {
|
||||
let pid = process.id().context("app-server process has no pid")?;
|
||||
let pid = process
|
||||
.id()
|
||||
.context("websocket app-server process has no pid")?;
|
||||
let status = StdCommand::new("kill")
|
||||
.arg(signal)
|
||||
.arg(pid.to_string())
|
||||
@@ -304,7 +299,7 @@ async fn wait_for_process_exit_within(
|
||||
timeout(window, process.wait())
|
||||
.await
|
||||
.context(timeout_context)?
|
||||
.context("failed waiting for app-server process exit")
|
||||
.context("failed waiting for websocket app-server process exit")
|
||||
}
|
||||
|
||||
async fn expect_websocket_disconnect(stream: &mut WsClient) -> Result<()> {
|
||||
|
||||
@@ -36,11 +36,11 @@ async fn thread_name_updated_broadcasts_for_loaded_threads() -> Result<()> {
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
let conversation_id = create_rollout(codex_home.path(), "2025-01-05T12-00-00")?;
|
||||
|
||||
let (mut process, socket_path, _socket_dir) = spawn_websocket_server(codex_home.path()).await?;
|
||||
let (mut process, bind_addr) = spawn_websocket_server(codex_home.path()).await?;
|
||||
|
||||
let result = async {
|
||||
let mut ws1 = connect_websocket(&socket_path).await?;
|
||||
let mut ws2 = connect_websocket(&socket_path).await?;
|
||||
let mut ws1 = connect_websocket(bind_addr).await?;
|
||||
let mut ws2 = connect_websocket(bind_addr).await?;
|
||||
initialize_both_clients(&mut ws1, &mut ws2).await?;
|
||||
|
||||
send_request(
|
||||
@@ -91,7 +91,7 @@ async fn thread_name_updated_broadcasts_for_loaded_threads() -> Result<()> {
|
||||
process
|
||||
.kill()
|
||||
.await
|
||||
.context("failed to stop app-server process")?;
|
||||
.context("failed to stop websocket app-server process")?;
|
||||
result
|
||||
}
|
||||
|
||||
@@ -102,11 +102,11 @@ async fn thread_name_updated_broadcasts_for_not_loaded_threads() -> Result<()> {
|
||||
create_config_toml(codex_home.path(), &server.uri(), "never")?;
|
||||
let conversation_id = create_rollout(codex_home.path(), "2025-01-05T12-05-00")?;
|
||||
|
||||
let (mut process, socket_path, _socket_dir) = spawn_websocket_server(codex_home.path()).await?;
|
||||
let (mut process, bind_addr) = spawn_websocket_server(codex_home.path()).await?;
|
||||
|
||||
let result = async {
|
||||
let mut ws1 = connect_websocket(&socket_path).await?;
|
||||
let mut ws2 = connect_websocket(&socket_path).await?;
|
||||
let mut ws1 = connect_websocket(bind_addr).await?;
|
||||
let mut ws2 = connect_websocket(bind_addr).await?;
|
||||
initialize_both_clients(&mut ws1, &mut ws2).await?;
|
||||
|
||||
let renamed = "Stored rename";
|
||||
@@ -143,7 +143,7 @@ async fn thread_name_updated_broadcasts_for_not_loaded_threads() -> Result<()> {
|
||||
process
|
||||
.kill()
|
||||
.await
|
||||
.context("failed to stop app-server process")?;
|
||||
.context("failed to stop websocket app-server process")?;
|
||||
result
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user