mirror of
https://github.com/openai/codex.git
synced 2026-10-19 17:28:46 +00:00
Report the exec-server release version in environment info (#42270)
## Why Clients need the executor's release version to make version-based compatibility decisions. ## What changed - Add `executorVersion` to `EnvironmentInfo` and return it from both `initialize` and `environment/info`. - Resolve the package release version at process startup so the advertised value remains stable for the server's lifetime. - Use `0.0.0` when the version is unknown or omitted by a legacy executor. ## Testing - Cover packaged and source-build initialization, startup caching, protocol serialization, and client-side metadata caching. GitOrigin-RevId: a0c7f0b701a8e7e50345cd5fb1b5c5d2d38ff3b1
This commit is contained in:
@@ -17,6 +17,7 @@ base64 = { workspace = true }
|
||||
bytes = { workspace = true }
|
||||
clatter = { workspace = true }
|
||||
codex-api = { workspace = true }
|
||||
codex-build-info = { workspace = true }
|
||||
codex-config = { workspace = true }
|
||||
codex-http-client = { workspace = true }
|
||||
codex-exec-server-protocol = { workspace = true }
|
||||
|
||||
@@ -157,6 +157,7 @@ Response:
|
||||
"sessionId": "00000000-0000-4000-8000-000000000001",
|
||||
"environmentInfo": {
|
||||
"shell": { "name": "bash", "path": "/bin/bash" },
|
||||
"executorVersion": "1.2.3-alpha.4",
|
||||
"cwd": "file:///workspace"
|
||||
}
|
||||
}
|
||||
@@ -165,6 +166,8 @@ Response:
|
||||
`environmentInfo` contains the same executor metadata returned by
|
||||
`environment/info`, so clients can use it without a second request.
|
||||
|
||||
`executorVersion` is the executor's package release version, or `0.0.0` when unknown.
|
||||
|
||||
Rust clients cache this metadata for the client's lifetime, including session
|
||||
resumption. If initialization omits it, the first metadata request fetches and
|
||||
caches `environment/info`.
|
||||
|
||||
@@ -2428,6 +2428,10 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test_case::test_case(Some(EnvironmentInfo::local()); "from_initialize")]
|
||||
#[test_case::test_case(Some(EnvironmentInfo {
|
||||
executor_version: "1.2.3-alpha.4".to_string(),
|
||||
..EnvironmentInfo::local()
|
||||
}); "with_executor_version")]
|
||||
#[test_case::test_case(None; "legacy_server")]
|
||||
#[tokio::test]
|
||||
async fn environment_info_is_cached(
|
||||
|
||||
@@ -3,6 +3,7 @@ mod handler;
|
||||
mod process_handler;
|
||||
mod processor;
|
||||
mod registry;
|
||||
mod release_version;
|
||||
mod request_dispatcher;
|
||||
mod session_registry;
|
||||
mod transport;
|
||||
|
||||
@@ -158,7 +158,7 @@ impl ExecServerHandler {
|
||||
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(session);
|
||||
Ok(InitializeResponse {
|
||||
session_id,
|
||||
environment_info: Some(EnvironmentInfo::local()),
|
||||
environment_info: Some(super::release_version::local_environment_info()),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ impl ExecServerHandler {
|
||||
|
||||
pub(crate) fn environment_info(&self) -> Result<EnvironmentInfo, JSONRPCErrorError> {
|
||||
self.require_initialized_for("environment info")?;
|
||||
Ok(EnvironmentInfo::local())
|
||||
Ok(super::release_version::local_environment_info())
|
||||
}
|
||||
|
||||
pub(crate) async fn environment_config_read(
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
|
||||
use codex_build_info::BuildInfo;
|
||||
use codex_exec_server_protocol::JSONRPCMessage;
|
||||
use tokio::sync::mpsc;
|
||||
use tracing::debug;
|
||||
@@ -53,6 +54,8 @@ impl ConnectionProcessor {
|
||||
http_client_factory: HttpClientFactory,
|
||||
request_dispatch_mode: RequestDispatchMode,
|
||||
) -> Self {
|
||||
// Library callers may bypass CLI startup. Capture the version before serving clients.
|
||||
let _ = BuildInfo::get();
|
||||
Self {
|
||||
session_registry: SessionRegistry::new(telemetry.clone()),
|
||||
runtime_paths,
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
//! Include the startup-cached package release version in executor metadata.
|
||||
|
||||
use codex_build_info::BuildInfo;
|
||||
|
||||
use crate::protocol::EnvironmentInfo;
|
||||
|
||||
pub(super) fn local_environment_info() -> EnvironmentInfo {
|
||||
EnvironmentInfo {
|
||||
executor_version: BuildInfo::get().version().to_string(),
|
||||
..EnvironmentInfo::local()
|
||||
}
|
||||
}
|
||||
@@ -33,7 +33,6 @@ const EVENT_TIMEOUT: Duration = Duration::from_secs(5);
|
||||
|
||||
pub(crate) struct ExecServerHarness {
|
||||
codex_home: TempDir,
|
||||
_helper_paths: TestCodexHelperPaths,
|
||||
child: Child,
|
||||
websocket_url: String,
|
||||
websocket: tokio_tungstenite::WebSocketStream<
|
||||
@@ -89,31 +88,34 @@ where
|
||||
V: AsRef<std::ffi::OsStr>,
|
||||
{
|
||||
let helper_paths = test_codex_helper_paths()?;
|
||||
let codex_home = TempDir::new()?;
|
||||
let mut child = Command::new(&helper_paths.codex_exe);
|
||||
child.args(["exec-server", "--listen", "ws://127.0.0.1:0"]);
|
||||
child.args(args);
|
||||
child.stdin(Stdio::null());
|
||||
child.stdout(Stdio::piped());
|
||||
child.stderr(Stdio::inherit());
|
||||
child.kill_on_drop(true);
|
||||
child.env("CODEX_HOME", codex_home.path());
|
||||
child.envs(env);
|
||||
let mut child = child.spawn()?;
|
||||
|
||||
let websocket_url = read_listen_url_from_stdout(&mut child).await?;
|
||||
let (websocket, _) = connect_websocket_when_ready(&websocket_url).await?;
|
||||
Ok(ExecServerHarness {
|
||||
codex_home,
|
||||
_helper_paths: helper_paths,
|
||||
child,
|
||||
websocket_url,
|
||||
websocket,
|
||||
next_request_id: 1,
|
||||
})
|
||||
ExecServerHarness::start(child).await
|
||||
}
|
||||
|
||||
impl ExecServerHarness {
|
||||
pub(crate) async fn start(mut command: Command) -> anyhow::Result<Self> {
|
||||
let codex_home = TempDir::new()?;
|
||||
command.stdin(Stdio::null());
|
||||
command.stdout(Stdio::piped());
|
||||
command.stderr(Stdio::inherit());
|
||||
command.kill_on_drop(true);
|
||||
command.env("CODEX_HOME", codex_home.path());
|
||||
let mut child = command.spawn()?;
|
||||
|
||||
let websocket_url = read_listen_url_from_stdout(&mut child).await?;
|
||||
let (websocket, _) = connect_websocket_when_ready(&websocket_url).await?;
|
||||
Ok(Self {
|
||||
codex_home,
|
||||
child,
|
||||
websocket_url,
|
||||
websocket,
|
||||
next_request_id: 1,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn codex_home(&self) -> &std::path::Path {
|
||||
self.codex_home.path()
|
||||
}
|
||||
|
||||
@@ -7,14 +7,36 @@ use codex_exec_server_protocol::JSONRPCError;
|
||||
use codex_exec_server_protocol::JSONRPCErrorError;
|
||||
use codex_exec_server_protocol::JSONRPCMessage;
|
||||
use codex_exec_server_protocol::JSONRPCResponse;
|
||||
use common::exec_server::exec_server;
|
||||
use common::exec_server::ExecServerHarness;
|
||||
use common::exec_server::exec_server_with_env;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
use tokio::process::Command;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[test_case::test_case(Some("1.2.3-alpha.4"); "packaged")]
|
||||
#[test_case::test_case(None; "without_manifest")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn exec_server_accepts_initialize() -> anyhow::Result<()> {
|
||||
let mut server = exec_server().await?;
|
||||
async fn exec_server_accepts_initialize(version: Option<&str>) -> anyhow::Result<()> {
|
||||
let package = TempDir::new()?;
|
||||
let bin_dir = package.path().join("bin");
|
||||
std::fs::create_dir(&bin_dir)?;
|
||||
let executable = bin_dir.join(format!("codex{}", std::env::consts::EXE_SUFFIX));
|
||||
std::fs::copy(std::env::current_exe()?, &executable)?;
|
||||
let manifest = package.path().join("codex-package.json");
|
||||
if let Some(version) = version {
|
||||
std::fs::write(
|
||||
&manifest,
|
||||
serde_json::to_vec(&serde_json::json!({ "version": version }))?,
|
||||
)?;
|
||||
}
|
||||
|
||||
let mut command = Command::new(&executable);
|
||||
command.args(["exec-server", "--listen", "ws://127.0.0.1:0"]);
|
||||
let mut server = ExecServerHarness::start(command).await?;
|
||||
|
||||
// Updates after startup cannot change the advertised release version.
|
||||
std::fs::write(&manifest, r#"{"version":"9.9.9"}"#)?;
|
||||
let initialize_id = server
|
||||
.send_request(
|
||||
"initialize",
|
||||
@@ -32,9 +54,28 @@ async fn exec_server_accepts_initialize() -> anyhow::Result<()> {
|
||||
assert_eq!(id, initialize_id);
|
||||
let initialize_response: InitializeResponse = serde_json::from_value(result)?;
|
||||
Uuid::parse_str(&initialize_response.session_id)?;
|
||||
let mut expected_environment = EnvironmentInfo::local();
|
||||
expected_environment.executor_version = version.unwrap_or("0.0.0").to_string();
|
||||
assert_eq!(
|
||||
initialize_response.environment_info,
|
||||
Some(EnvironmentInfo::local())
|
||||
Some(expected_environment.clone())
|
||||
);
|
||||
|
||||
server
|
||||
.send_notification("initialized", serde_json::json!({}))
|
||||
.await?;
|
||||
std::fs::remove_file(&manifest)?;
|
||||
let environment_id = server
|
||||
.send_request("environment/info", serde_json::json!({}))
|
||||
.await?;
|
||||
let JSONRPCMessage::Response(JSONRPCResponse { id, result }) = server.next_event().await?
|
||||
else {
|
||||
panic!("expected environment info response");
|
||||
};
|
||||
assert_eq!(id, environment_id);
|
||||
assert_eq!(
|
||||
serde_json::from_value::<EnvironmentInfo>(result)?,
|
||||
expected_environment
|
||||
);
|
||||
|
||||
server.shutdown().await?;
|
||||
|
||||
Reference in New Issue
Block a user