mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
6e50b22e55
## Why The transport in [openai/codex#26242](https://github.com/openai/codex/pull/26242) needs to be used by every remote orchestrator-to-executor connection before JSON-RPC traffic starts. ## Changes - Generates one executor Noise identity when remote exec-server starts and registers its public key. - Creates a harness identity for each physical remote environment connection. - Fetches a fresh registry bundle before connecting and validates the authenticated harness key before completing the executor handshake. - Multiplexes encrypted logical streams over the existing executor WebSocket. - Adds bounded stream, handshake-failure, and reassembly state. - Adds safe lifecycle diagnostics without logging keys, authorizations, plaintext, or ciphertext. - Covers reconnects, replay rejection, validation failure, framing limits, and encrypted JSON-RPC tool traffic. ## Stack 1. [openai/codex#26242](https://github.com/openai/codex/pull/26242): Noise channel and relay transport 2. **[openai/codex#26245](https://github.com/openai/codex/pull/26245)**: remote registration and runtime activation ## Verification - `just test -p codex-exec-server` - `just fix -p codex-exec-server` - `just bazel-lock-check` - `cargo shear` --------- Co-authored-by: Codex <noreply@openai.com>
359 lines
14 KiB
Rust
359 lines
14 KiB
Rust
use std::sync::Arc;
|
|
use std::sync::atomic::AtomicUsize;
|
|
use std::sync::atomic::Ordering;
|
|
use std::time::Duration;
|
|
|
|
use anyhow::Result;
|
|
use futures::SinkExt;
|
|
use futures::StreamExt;
|
|
use pretty_assertions::assert_eq;
|
|
use tokio::net::TcpListener;
|
|
use tokio::sync::Notify;
|
|
use tokio::time::timeout;
|
|
use tokio_tungstenite::accept_async;
|
|
use tokio_tungstenite::connect_async;
|
|
use tokio_tungstenite::tungstenite::Message;
|
|
|
|
use super::HarnessKeyValidator;
|
|
use super::MAX_FAILED_NOISE_HANDSHAKES;
|
|
use super::MAX_HARNESS_KEY_AUTHORIZATION_BYTES;
|
|
use super::run_multiplexed_environment;
|
|
use crate::ExecServerError;
|
|
use crate::ExecServerRuntimePaths;
|
|
use crate::noise_channel::InitiatorHandshake;
|
|
use crate::noise_channel::NoiseChannelIdentity;
|
|
use crate::noise_channel::NoiseChannelPublicKey;
|
|
use crate::noise_channel::noise_channel_prologue;
|
|
use crate::relay::RelayFrameBodyKind;
|
|
use crate::relay::decode_relay_message_frame;
|
|
use crate::relay::encode_relay_message_frame;
|
|
use crate::relay_proto::RelayMessageFrame;
|
|
use crate::server::ConnectionProcessor;
|
|
|
|
const ENVIRONMENT_ID: &str = "environment-1";
|
|
const EXECUTOR_REGISTRATION_ID: &str = "registration-1";
|
|
|
|
#[derive(Clone)]
|
|
struct BlockingValidator {
|
|
calls: Arc<AtomicUsize>,
|
|
release: Arc<Notify>,
|
|
}
|
|
|
|
impl HarnessKeyValidator for BlockingValidator {
|
|
fn validate_harness_key(
|
|
&self,
|
|
_harness_public_key: &NoiseChannelPublicKey,
|
|
_authorization: &str,
|
|
) -> impl std::future::Future<Output = Result<(), ExecServerError>> + Send {
|
|
let calls = Arc::clone(&self.calls);
|
|
let release = Arc::clone(&self.release);
|
|
async move {
|
|
calls.fetch_add(1, Ordering::SeqCst);
|
|
release.notified().await;
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn pending_harness_key_validation_does_not_block_new_handshakes() -> Result<()> {
|
|
let listener = TcpListener::bind("127.0.0.1:0").await?;
|
|
let websocket_url = format!("ws://{}", listener.local_addr()?);
|
|
let harness_connection = tokio::spawn(connect_async(websocket_url));
|
|
let (socket, _peer_addr) = listener.accept().await?;
|
|
let environment_websocket = accept_async(socket).await?;
|
|
let (mut harness_websocket, _response) = harness_connection.await??;
|
|
|
|
let environment_identity = NoiseChannelIdentity::generate()?;
|
|
let harness_identity = NoiseChannelIdentity::generate()?;
|
|
let calls = Arc::new(AtomicUsize::new(0));
|
|
let environment_task = tokio::spawn(run_multiplexed_environment(
|
|
environment_websocket,
|
|
ConnectionProcessor::new(ExecServerRuntimePaths::new(
|
|
std::env::current_exe()?,
|
|
/*codex_linux_sandbox_exe*/ None,
|
|
)?),
|
|
ENVIRONMENT_ID.to_string(),
|
|
EXECUTOR_REGISTRATION_ID.to_string(),
|
|
environment_identity.clone(),
|
|
BlockingValidator {
|
|
calls: Arc::clone(&calls),
|
|
release: Arc::new(Notify::new()),
|
|
},
|
|
));
|
|
|
|
for stream_id in ["stream-1", "stream-2"] {
|
|
let prologue = noise_channel_prologue(ENVIRONMENT_ID, EXECUTOR_REGISTRATION_ID, stream_id);
|
|
let (_handshake, request) = InitiatorHandshake::start(
|
|
&harness_identity,
|
|
&environment_identity.public_key(),
|
|
&prologue,
|
|
b"authorization",
|
|
)?;
|
|
let frame = RelayMessageFrame::handshake(stream_id.to_string(), request);
|
|
harness_websocket
|
|
.send(Message::Binary(encode_relay_message_frame(&frame).into()))
|
|
.await?;
|
|
}
|
|
|
|
timeout(Duration::from_secs(1), async {
|
|
while calls.load(Ordering::SeqCst) != 2 {
|
|
tokio::task::yield_now().await;
|
|
}
|
|
})
|
|
.await?;
|
|
|
|
harness_websocket.close(None).await?;
|
|
timeout(Duration::from_secs(1), environment_task).await??;
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn duplicate_handshakes_exhaust_failure_budget() -> Result<()> {
|
|
let listener = TcpListener::bind("127.0.0.1:0").await?;
|
|
let websocket_url = format!("ws://{}", listener.local_addr()?);
|
|
let harness_connection = tokio::spawn(connect_async(websocket_url));
|
|
let (socket, _peer_addr) = listener.accept().await?;
|
|
let environment_websocket = accept_async(socket).await?;
|
|
let (mut harness_websocket, _response) = harness_connection.await??;
|
|
|
|
let environment_identity = NoiseChannelIdentity::generate()?;
|
|
let harness_identity = NoiseChannelIdentity::generate()?;
|
|
let calls = Arc::new(AtomicUsize::new(0));
|
|
let release = Arc::new(Notify::new());
|
|
let environment_task = tokio::spawn(run_multiplexed_environment(
|
|
environment_websocket,
|
|
ConnectionProcessor::new(ExecServerRuntimePaths::new(
|
|
std::env::current_exe()?,
|
|
/*codex_linux_sandbox_exe*/ None,
|
|
)?),
|
|
ENVIRONMENT_ID.to_string(),
|
|
EXECUTOR_REGISTRATION_ID.to_string(),
|
|
environment_identity.clone(),
|
|
BlockingValidator {
|
|
calls: Arc::clone(&calls),
|
|
release: Arc::clone(&release),
|
|
},
|
|
));
|
|
|
|
let stream_id = "stream-1";
|
|
let prologue = noise_channel_prologue(ENVIRONMENT_ID, EXECUTOR_REGISTRATION_ID, stream_id);
|
|
let (_handshake, request) = InitiatorHandshake::start(
|
|
&harness_identity,
|
|
&environment_identity.public_key(),
|
|
&prologue,
|
|
b"authorization",
|
|
)?;
|
|
let frame = RelayMessageFrame::handshake(stream_id.to_string(), request);
|
|
let encoded = encode_relay_message_frame(&frame);
|
|
harness_websocket
|
|
.send(Message::Binary(encoded.clone().into()))
|
|
.await?;
|
|
timeout(Duration::from_secs(1), async {
|
|
while calls.load(Ordering::SeqCst) != 1 {
|
|
tokio::task::yield_now().await;
|
|
}
|
|
})
|
|
.await?;
|
|
|
|
for attempt in 1..MAX_FAILED_NOISE_HANDSHAKES {
|
|
if attempt > 1 {
|
|
harness_websocket
|
|
.send(Message::Binary(encoded.clone().into()))
|
|
.await?;
|
|
timeout(Duration::from_secs(1), async {
|
|
while calls.load(Ordering::SeqCst) != attempt {
|
|
tokio::task::yield_now().await;
|
|
}
|
|
})
|
|
.await?;
|
|
}
|
|
harness_websocket
|
|
.send(Message::Binary(encoded.clone().into()))
|
|
.await?;
|
|
let payload = timeout(Duration::from_secs(1), async {
|
|
loop {
|
|
match harness_websocket.next().await {
|
|
Some(Ok(Message::Binary(payload))) => break Ok(payload),
|
|
Some(Ok(Message::Ping(_) | Message::Pong(_) | Message::Frame(_))) => {}
|
|
Some(Ok(message)) => anyhow::bail!("expected reset frame, got {message:?}"),
|
|
Some(Err(error)) => break Err(error.into()),
|
|
None => anyhow::bail!("environment closed before sending reset"),
|
|
}
|
|
}
|
|
})
|
|
.await??;
|
|
let reset = decode_relay_message_frame(payload.as_ref())?;
|
|
assert_eq!(reset.stream_id, stream_id);
|
|
assert_eq!(reset.validate()?, RelayFrameBodyKind::Reset);
|
|
}
|
|
|
|
harness_websocket
|
|
.send(Message::Binary(encoded.clone().into()))
|
|
.await?;
|
|
timeout(Duration::from_secs(1), async {
|
|
while calls.load(Ordering::SeqCst) != MAX_FAILED_NOISE_HANDSHAKES {
|
|
tokio::task::yield_now().await;
|
|
}
|
|
})
|
|
.await?;
|
|
harness_websocket
|
|
.send(Message::Binary(encoded.into()))
|
|
.await?;
|
|
timeout(Duration::from_secs(1), environment_task).await??;
|
|
release.notify_waiters();
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn oversized_harness_authorization_is_rejected_before_validation() -> Result<()> {
|
|
let listener = TcpListener::bind("127.0.0.1:0").await?;
|
|
let websocket_url = format!("ws://{}", listener.local_addr()?);
|
|
let harness_connection = tokio::spawn(connect_async(websocket_url));
|
|
let (socket, _peer_addr) = listener.accept().await?;
|
|
let environment_websocket = accept_async(socket).await?;
|
|
let (mut harness_websocket, _response) = harness_connection.await??;
|
|
|
|
let environment_identity = NoiseChannelIdentity::generate()?;
|
|
let harness_identity = NoiseChannelIdentity::generate()?;
|
|
let calls = Arc::new(AtomicUsize::new(0));
|
|
let environment_task = tokio::spawn(run_multiplexed_environment(
|
|
environment_websocket,
|
|
ConnectionProcessor::new(ExecServerRuntimePaths::new(
|
|
std::env::current_exe()?,
|
|
/*codex_linux_sandbox_exe*/ None,
|
|
)?),
|
|
ENVIRONMENT_ID.to_string(),
|
|
EXECUTOR_REGISTRATION_ID.to_string(),
|
|
environment_identity.clone(),
|
|
BlockingValidator {
|
|
calls: Arc::clone(&calls),
|
|
release: Arc::new(Notify::new()),
|
|
},
|
|
));
|
|
|
|
let stream_id = "stream-1";
|
|
let prologue = noise_channel_prologue(ENVIRONMENT_ID, EXECUTOR_REGISTRATION_ID, stream_id);
|
|
let oversized_authorization = vec![b'a'; MAX_HARNESS_KEY_AUTHORIZATION_BYTES + 1];
|
|
let (_handshake, request) = InitiatorHandshake::start(
|
|
&harness_identity,
|
|
&environment_identity.public_key(),
|
|
&prologue,
|
|
&oversized_authorization,
|
|
)?;
|
|
let frame = RelayMessageFrame::handshake(stream_id.to_string(), request);
|
|
harness_websocket
|
|
.send(Message::Binary(encode_relay_message_frame(&frame).into()))
|
|
.await?;
|
|
|
|
let Message::Binary(payload) = timeout(Duration::from_secs(1), harness_websocket.next())
|
|
.await?
|
|
.ok_or_else(|| anyhow::anyhow!("environment closed before sending reset"))??
|
|
else {
|
|
anyhow::bail!("expected binary reset frame");
|
|
};
|
|
let reset = decode_relay_message_frame(payload.as_ref())?;
|
|
assert_eq!(reset.validate()?, RelayFrameBodyKind::Reset);
|
|
assert_eq!(calls.load(Ordering::SeqCst), 0);
|
|
|
|
harness_websocket.close(None).await?;
|
|
timeout(Duration::from_secs(1), environment_task).await??;
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn repeated_malformed_handshakes_close_the_physical_relay() -> Result<()> {
|
|
let listener = TcpListener::bind("127.0.0.1:0").await?;
|
|
let websocket_url = format!("ws://{}", listener.local_addr()?);
|
|
let harness_connection = tokio::spawn(connect_async(websocket_url));
|
|
let (socket, _peer_addr) = listener.accept().await?;
|
|
let environment_websocket = accept_async(socket).await?;
|
|
let (mut harness_websocket, _response) = harness_connection.await??;
|
|
|
|
let environment_identity = NoiseChannelIdentity::generate()?;
|
|
let harness_identity = NoiseChannelIdentity::generate()?;
|
|
let environment_task = tokio::spawn(run_multiplexed_environment(
|
|
environment_websocket,
|
|
ConnectionProcessor::new(ExecServerRuntimePaths::new(
|
|
std::env::current_exe()?,
|
|
/*codex_linux_sandbox_exe*/ None,
|
|
)?),
|
|
ENVIRONMENT_ID.to_string(),
|
|
EXECUTOR_REGISTRATION_ID.to_string(),
|
|
environment_identity.clone(),
|
|
BlockingValidator {
|
|
calls: Arc::new(AtomicUsize::new(0)),
|
|
release: Arc::new(Notify::new()),
|
|
},
|
|
));
|
|
|
|
for attempt in 0..MAX_FAILED_NOISE_HANDSHAKES {
|
|
let stream_id = format!("malformed-{attempt}");
|
|
let prologue = noise_channel_prologue(ENVIRONMENT_ID, EXECUTOR_REGISTRATION_ID, &stream_id);
|
|
let (_handshake, mut request) = InitiatorHandshake::start(
|
|
&harness_identity,
|
|
&environment_identity.public_key(),
|
|
&prologue,
|
|
b"authorization",
|
|
)?;
|
|
let last_byte = request.last_mut().expect("handshake request is not empty");
|
|
*last_byte ^= 1;
|
|
let frame = RelayMessageFrame::handshake(stream_id, request);
|
|
harness_websocket
|
|
.send(Message::Binary(encode_relay_message_frame(&frame).into()))
|
|
.await?;
|
|
}
|
|
|
|
timeout(Duration::from_secs(1), environment_task).await??;
|
|
Ok(())
|
|
}
|
|
|
|
#[tokio::test]
|
|
async fn repeated_early_data_during_validation_closes_the_physical_relay() -> Result<()> {
|
|
let listener = TcpListener::bind("127.0.0.1:0").await?;
|
|
let websocket_url = format!("ws://{}", listener.local_addr()?);
|
|
let harness_connection = tokio::spawn(connect_async(websocket_url));
|
|
let (socket, _peer_addr) = listener.accept().await?;
|
|
let environment_websocket = accept_async(socket).await?;
|
|
let (mut harness_websocket, _response) = harness_connection.await??;
|
|
|
|
let environment_identity = NoiseChannelIdentity::generate()?;
|
|
let harness_identity = NoiseChannelIdentity::generate()?;
|
|
let environment_task = tokio::spawn(run_multiplexed_environment(
|
|
environment_websocket,
|
|
ConnectionProcessor::new(ExecServerRuntimePaths::new(
|
|
std::env::current_exe()?,
|
|
/*codex_linux_sandbox_exe*/ None,
|
|
)?),
|
|
ENVIRONMENT_ID.to_string(),
|
|
EXECUTOR_REGISTRATION_ID.to_string(),
|
|
environment_identity.clone(),
|
|
BlockingValidator {
|
|
calls: Arc::new(AtomicUsize::new(0)),
|
|
release: Arc::new(Notify::new()),
|
|
},
|
|
));
|
|
|
|
for attempt in 0..MAX_FAILED_NOISE_HANDSHAKES {
|
|
let stream_id = format!("early-data-{attempt}");
|
|
let prologue = noise_channel_prologue(ENVIRONMENT_ID, EXECUTOR_REGISTRATION_ID, &stream_id);
|
|
let (_handshake, request) = InitiatorHandshake::start(
|
|
&harness_identity,
|
|
&environment_identity.public_key(),
|
|
&prologue,
|
|
b"authorization",
|
|
)?;
|
|
for frame in [
|
|
RelayMessageFrame::handshake(stream_id.clone(), request),
|
|
RelayMessageFrame::data(stream_id, /*seq*/ 0, vec![0]),
|
|
] {
|
|
harness_websocket
|
|
.send(Message::Binary(encode_relay_message_frame(&frame).into()))
|
|
.await?;
|
|
}
|
|
}
|
|
|
|
timeout(Duration::from_secs(1), environment_task).await??;
|
|
Ok(())
|
|
}
|