Expose a stable executor build identity in environment metadata (#43513)

## Why

Executor compatibility checks need a build identity that distinguishes commits
and compiler targets independently of the package release version.

## What changed

- Add optional `providerId` to exec-server environment metadata, cached at startup
  and returned by initialization and `environment/info`.
- Derive the ID as SHA-256 of `git:<lowercase commit>:<target>`. It identifies a
  standard build configuration, not exact executable bytes, and is omitted when
  the commit stamp or target is unavailable or invalid.
- Embed the compiler target in `BuildInfo`, preserve compatibility with historical
  metadata without a target, and stamp Cargo release builds with `STABLE_GIT_COMMIT`.
- Group Bazel build-script argument files under one `--arg-file` flag to reduce
  Windows command-line length while preserving file order.

## Testing

Add deterministic build-ID vectors across targets, commit normalization and invalid
input coverage, and historical metadata compatibility tests. Extend exec-server
coverage for metadata caching and ensure runtime environment overrides cannot
replace the executor's build identity.

GitOrigin-RevId: 125a18c23de7ad006571940ba305836376c983f4
This commit is contained in:
felixxia-oai
2026-09-07 16:55:06 +00:00
committed by copyberry
parent 6750f5bd13
commit dbe2f6d528
20 changed files with 213 additions and 14 deletions
+2
View File
@@ -255,6 +255,8 @@ jobs:
fi
build_args+=(--bin "$binary")
done
STABLE_GIT_COMMIT="$(git rev-parse HEAD)"
export STABLE_GIT_COMMIT
cargo build --target "$target" --release --timings "${build_args[@]}"
- name: Upload Cargo timings
+2
View File
@@ -200,6 +200,8 @@ rules_rust.patch(
# Skip transient native-Windows linker outputs while consolidating
# dependency search paths.
"//patches:rules_rust_windows_process_wrapper_skip_temp_outputs.patch",
# Group build-script argument files to avoid Windows command-line limits.
"//patches:rules_rust_group_build_script_arg_files.patch",
],
strip = 1,
)
+1
View File
@@ -2533,6 +2533,7 @@ dependencies = [
"semver",
"serde",
"serde_json",
"sha2 0.10.9",
"tempfile",
]
+1
View File
@@ -16,6 +16,7 @@ workspace = true
codex-install-context = { workspace = true }
semver = { workspace = true, features = ["serde"] }
serde = { workspace = true, features = ["derive"] }
sha2 = { workspace = true }
[dev-dependencies]
pretty_assertions = { workspace = true }
+8
View File
@@ -0,0 +1,8 @@
//! Embed the compilation target, including its architecture and ABI.
fn main() -> Result<(), std::env::VarError> {
let target = std::env::var("TARGET")?;
println!("cargo:rustc-env=CODEX_BUILD_TARGET={target}");
println!("cargo:rerun-if-changed=build.rs");
Ok(())
}
+71 -7
View File
@@ -6,6 +6,7 @@ use semver::Version;
use tempfile::tempdir;
use crate::BuildInfo;
use crate::build_id;
const BUILD_COMMIT: &str = "0123456789abcdef0123456789abcdef01234567";
@@ -34,6 +35,7 @@ fn packaged_runtime_uses_manifest_version() {
BuildInfo {
version: Version::parse("1.2.3-alpha.4").expect("valid release version"),
build_commit: BUILD_COMMIT.to_string(),
target: Some(env!("CODEX_BUILD_TARGET").to_string()),
},
);
}
@@ -52,6 +54,7 @@ fn unpackaged_runtime_uses_build_commit() {
BuildInfo {
version: Version::new(0, 0, 0),
build_commit: BUILD_COMMIT.to_string(),
target: Some(env!("CODEX_BUILD_TARGET").to_string()),
},
);
}
@@ -78,6 +81,7 @@ fn legacy_package_without_version_uses_build_commit() {
BuildInfo {
version: Version::new(0, 0, 0),
build_commit: BUILD_COMMIT.to_string(),
target: Some(env!("CODEX_BUILD_TARGET").to_string()),
},
);
}
@@ -107,27 +111,87 @@ fn invalid_package_version_uses_build_commit() {
BuildInfo {
version: Version::new(0, 0, 0),
build_commit: BUILD_COMMIT.to_string(),
target: Some(env!("CODEX_BUILD_TARGET").to_string()),
},
);
}
/// Serializing build information preserves both its release version and commit.
/// Serializing build information preserves release version, commit, and target.
#[test]
fn build_info_serialization_preserves_build_provenance() {
let build_info = BuildInfo {
version: Version::parse("1.2.3-alpha.4").expect("valid release version"),
build_commit: BUILD_COMMIT.to_string(),
target: Some("x86_64-pc-windows-msvc".to_string()),
};
let serialized = serde_json::json!({
"version": "1.2.3-alpha.4",
"build_commit": BUILD_COMMIT,
"target": "x86_64-pc-windows-msvc",
});
assert_eq!(
serde_json::to_string(&build_info).expect("serialize build information"),
format!("{{\"version\":\"1.2.3-alpha.4\",\"build_commit\":\"{BUILD_COMMIT}\"}}"),
serde_json::to_value(&build_info).expect("serialize build information"),
serialized,
);
assert_eq!(
serde_json::from_str::<BuildInfo>(&format!(
"{{\"version\":\"1.2.3-alpha.4\",\"build_commit\":\"{BUILD_COMMIT}\"}}"
))
.expect("deserialize build information"),
serde_json::from_value::<BuildInfo>(serialized).expect("deserialize build information"),
build_info,
);
}
#[test]
fn historical_build_info_does_not_infer_the_current_target() {
let legacy = serde_json::json!({ "version": "1.2.3", "build_commit": BUILD_COMMIT });
for info in [
serde_json::from_value::<BuildInfo>(legacy).expect("deserialize legacy build information"),
BuildInfo::from_version("1.2.3"),
BuildInfo::from_version(BUILD_COMMIT),
] {
assert_eq!(info.target(), None);
}
}
#[test]
fn build_id_is_stable_and_uses_the_supplied_commit_and_target() {
// These vectors also cover targets different from the machine running CI.
for (target, digest) in [
(
"aarch64-apple-darwin",
"90138d7ee35f3f61cd61eb55e8d64105f856055641af175388b102dffa772594",
),
(
"x86_64-unknown-linux-gnu",
"89f3a373036537bde64861b3ad8b1c9494924b0174622e47acda695619630d09",
),
(
"x86_64-unknown-linux-musl",
"fb4f62da3e84f6864dcec8ede7bc66f1c96ecaeaf55f8a786b85df994057c8ac",
),
] {
let expected = Some(format!("sha256:{digest}"));
assert_eq!(build_id(BUILD_COMMIT, target), expected);
assert_eq!(
build_id(&BUILD_COMMIT.to_ascii_uppercase(), target),
expected
);
assert_ne!(
build_id("1123456789abcdef0123456789abcdef01234567", target),
expected
);
}
}
#[test]
fn build_id_requires_a_valid_stamp_and_target() {
for commit in [
"",
"dev",
"unknown",
"0123456",
"zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz",
] {
assert_eq!(build_id(commit, "x86_64-unknown-linux-musl"), None);
}
assert_eq!(build_id(BUILD_COMMIT, ""), None);
}
+33 -1
View File
@@ -1,4 +1,4 @@
//! Resolve the release identity of the current Codex runtime.
//! Resolve the packaged release version, compiled commit, and target of the current runtime.
use std::fmt;
use std::sync::OnceLock;
@@ -7,6 +7,8 @@ use codex_install_context::InstallContext;
use semver::Version;
use serde::Deserialize;
use serde::Serialize;
use sha2::Digest;
use sha2::Sha256;
static BUILD_INFO: OnceLock<BuildInfo> = OnceLock::new();
@@ -14,6 +16,7 @@ static BUILD_INFO: OnceLock<BuildInfo> = OnceLock::new();
///
/// The environment lookup intentionally expands at the macro call site so Git
/// changes invalidate only final binary actions, not this shared library.
/// Cargo builds must supply STABLE_GIT_COMMIT in the build environment.
#[macro_export]
macro_rules! initialize {
() => {
@@ -26,6 +29,8 @@ macro_rules! initialize {
pub struct BuildInfo {
version: Version,
build_commit: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
target: Option<String>,
}
impl BuildInfo {
@@ -59,10 +64,12 @@ impl BuildInfo {
"unknown".to_string()
},
version: parsed_version,
target: None,
},
Err(_) => Self {
version: Version::new(0, 0, 0),
build_commit: version,
target: None,
},
}
}
@@ -93,21 +100,46 @@ impl BuildInfo {
&self.build_commit
}
/// Return the compiler target triple, or `None` for historical metadata without a target.
pub fn target(&self) -> Option<&str> {
self.target.as_deref()
}
fn resolve(install_context: &InstallContext, build_commit: &'static str) -> Self {
if let Some(manifest) = install_context.package_manifest() {
return Self {
version: manifest.version,
build_commit: build_commit.to_owned(),
target: Some(env!("CODEX_BUILD_TARGET").to_owned()),
};
}
Self {
version: Version::new(0, 0, 0),
build_commit: build_commit.to_owned(),
target: Some(env!("CODEX_BUILD_TARGET").to_owned()),
}
}
}
/// Derive a standard build's opaque ID from its stamped commit and compiler target.
///
/// Explicit inputs let CI identify cross-compiled builds without running them.
/// Runtime callers must use the running executable's stamp and target. The stable
/// encoding is SHA-256 of `git:<full lowercase commit>:<target>`, excluding version.
/// This identifies a standard build configuration, not exact executable bytes.
pub fn build_id(build_commit: &str, target: &str) -> Option<String> {
if build_commit.len() != 40
|| !build_commit.bytes().all(|byte| byte.is_ascii_hexdigit())
|| target.is_empty()
{
return None;
}
let identity = format!("git:{}:{target}", build_commit.to_ascii_lowercase());
let digest = Sha256::digest(identity.as_bytes());
Some(format!("sha256:{digest:x}"))
}
impl fmt::Display for BuildInfo {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.is_source_build() {
+4 -1
View File
@@ -244,11 +244,14 @@ metrics_exporter = {{ otlp-http = {{ endpoint = "{collector_url}/v1/metrics", pr
.await
.context("remote harness did not connect")???;
let environment_info = client.environment_info().await?;
let expected_info = EnvironmentInfo {
executor_version: "1.2.3-alpha.4".to_string(),
// The build identity belongs to the spawned CLI, not this test process.
provider_id: environment_info.provider_id.clone(),
..EnvironmentInfo::local()
};
assert_eq!(client.environment_info().await?, expected_info);
assert_eq!(environment_info, expected_info);
std::fs::remove_file(&manifest)?;
assert_eq!(client.force_environment_info().await?, expected_info);
@@ -99,6 +99,12 @@ pub struct EnvironmentInfo {
/// `0.0.0` when unknown, including responses from legacy executors.
#[serde(default = "unknown_executor_version")]
pub executor_version: String,
/// Opaque executor build identity for looking up behavioral verification.
/// Derived from the compiled commit and target for standard builds;
/// absent for legacy or unstamped builds. This is not an artifact checksum
/// or a security attestation, and evidence must not be shared across build variants.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub provider_id: Option<String>,
/// Working directory inherited by the exec-server process.
#[serde(default)]
pub cwd: Option<PathUri>,
@@ -224,6 +230,7 @@ impl EnvironmentInfo {
Self {
shell: codex_shell_command::shell_detect::default_user_shell().into(),
executor_version: unknown_executor_version(),
provider_id: None,
cwd: cwd.and_then(|cwd| PathUri::from_host_native_path(cwd).ok()),
user_home_dir: PathUri::from_host_native_path("~").ok(),
platform_os: Some(std::env::consts::OS.to_string()),
@@ -1022,6 +1029,7 @@ mod tests {
path: "/bin/zsh".to_string(),
},
executor_version: "0.0.0".to_string(),
provider_id: None,
cwd: None,
user_home_dir: None,
platform_os: None,
@@ -1058,6 +1066,7 @@ mod tests {
let expected = serde_json::json!({
"shell": { "name": "powershell", "path": "powershell.exe" },
"executorVersion": "1.2.3-alpha.4",
"providerId": "sha256:e0a0cebe63ab8189ffe3eed378ccf6aa89ef15bc75e39dbbf1fc55951ec6888b",
"cwd": null,
"userHomeDir": "file:///C:/Users/remote",
"platformOs": "windows",
+7
View File
@@ -193,6 +193,7 @@ Response:
"environmentInfo": {
"shell": { "name": "bash", "path": "/bin/bash" },
"executorVersion": "1.2.3-alpha.4",
"providerId": "sha256:fb4f62da3e84f6864dcec8ede7bc66f1c96ecaeaf55f8a786b85df994057c8ac",
"cwd": "file:///workspace"
}
}
@@ -203,6 +204,12 @@ Response:
`executorVersion` is the executor's package release version, or `0.0.0` when unknown.
The executor caches optional `providerId` at startup using
`codex_build_info::build_id(commit, target)`, which CI can also call for an
explicit build target. This opaque compatibility key excludes package version
and requires no manifest. It identifies a standard build configuration, not exact
executable bytes. Unstamped and legacy executors may omit it.
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`.
+2 -1
View File
@@ -2543,8 +2543,9 @@ 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(),
provider_id: Some("sha256:fb4f62da3e84f6864dcec8ede7bc66f1c96ecaeaf55f8a786b85df994057c8ac".to_string()),
..EnvironmentInfo::local()
}); "with_executor_version")]
}); "with_executor_metadata")]
#[test_case::test_case(None; "legacy_server")]
#[tokio::test]
async fn environment_info_is_cached(
+2
View File
@@ -1,3 +1,4 @@
mod build_identity;
mod file_system_handler;
mod handler;
mod process_handler;
@@ -50,6 +51,7 @@ pub async fn run_main_with_telemetry(
http_client_factory: HttpClientFactory,
request_dispatch_mode: RequestDispatchMode,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
std::sync::LazyLock::force(&build_identity::PROVIDER_ID);
transport::run_transport(
listen_url,
runtime_paths,
@@ -0,0 +1,21 @@
//! Cache the running executor's build identity before accepting connections.
use std::sync::LazyLock;
use codex_build_info::BuildInfo;
use codex_build_info::build_id;
use crate::protocol::EnvironmentInfo;
pub(super) static PROVIDER_ID: LazyLock<Option<String>> = LazyLock::new(|| {
let info = BuildInfo::get();
info.target()
.and_then(|target| build_id(info.build_commit(), target))
});
pub(super) fn local_environment_info() -> EnvironmentInfo {
EnvironmentInfo {
provider_id: PROVIDER_ID.clone(),
..super::release_version::local_environment_info()
}
}
+3 -2
View File
@@ -67,6 +67,7 @@ use crate::rpc::RpcNotificationSender;
use crate::rpc::internal_error;
use crate::rpc::invalid_params;
use crate::rpc::invalid_request;
use crate::server::build_identity::local_environment_info;
use crate::server::file_system_handler::FileSystemHandler;
use crate::server::session_registry::SessionHandle;
use crate::server::session_registry::SessionRegistry;
@@ -162,7 +163,7 @@ impl ExecServerHandler {
.unwrap_or_else(std::sync::PoisonError::into_inner) = Some(session);
Ok(InitializeResponse {
session_id,
environment_info: Some(super::release_version::local_environment_info()),
environment_info: Some(local_environment_info()),
})
}
@@ -197,7 +198,7 @@ impl ExecServerHandler {
pub(crate) fn environment_info(&self) -> Result<EnvironmentInfo, JSONRPCErrorError> {
self.require_initialized_for("environment info")?;
Ok(super::release_version::local_environment_info())
Ok(local_environment_info())
}
pub(crate) async fn environment_config_read(
+4
View File
@@ -21,6 +21,8 @@ use ctor::ctor;
pub(crate) mod exec_server;
pub(crate) const TEST_BUILD_COMMIT: &str = "0123456789abcdef0123456789abcdef01234567";
pub(crate) const DELAYED_OUTPUT_AFTER_EXIT_PARENT_ARG: &str =
"--codex-test-delayed-output-after-exit-parent";
pub(crate) const SYSTEM_PROXY_REQUEST_URL_ENV: &str =
@@ -150,6 +152,8 @@ fn maybe_run_exec_server_from_test_binary(guard: Option<&TestBinaryDispatchGuard
if command != "exec-server" {
return;
}
// Initialize in the executor child, just as the real CLI does at startup.
codex_build_info::BuildInfo::initialize(TEST_BUILD_COMMIT);
let Some(flag) = args.next() else {
eprintln!("expected --listen");
+3 -1
View File
@@ -38,7 +38,9 @@ async fn remote_environment_fetches_info_from_exec_server() -> anyhow::Result<()
assert!(environment.is_remote());
let remote_info = environment.info().await?;
let local_info = Environment::default_for_tests().info().await?;
let mut local_info = Environment::default_for_tests().info().await?;
// Only the remote executor advertises its optional build identity.
local_info.provider_id = remote_info.provider_id.clone();
assert_eq!(remote_info, local_info);
server.shutdown().await?;
+19
View File
@@ -1,5 +1,8 @@
mod common;
use anyhow::Context;
use codex_build_info::BuildInfo;
use codex_build_info::build_id;
use codex_exec_server::EnvironmentInfo;
use codex_exec_server::InitializeParams;
use codex_exec_server::InitializeResponse;
@@ -7,6 +10,7 @@ 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::TEST_BUILD_COMMIT;
use common::exec_server::ExecServerHarness;
use common::exec_server::exec_server_with_env;
use pretty_assertions::assert_eq;
@@ -33,6 +37,15 @@ async fn exec_server_accepts_initialize(version: Option<&str>) -> anyhow::Result
let mut command = Command::new(&executable);
command.args(["exec-server", "--listen", "ws://127.0.0.1:0"]);
// Runtime environment variables cannot replace the executable's build stamp.
command.envs([
(
"STABLE_GIT_COMMIT",
"ffffffffffffffffffffffffffffffffffffffff",
),
("GITHUB_SHA", "ffffffffffffffffffffffffffffffffffffffff"),
("CODEX_BUILD_TARGET", "runtime-override"),
]);
let mut server = ExecServerHarness::start(command).await?;
// Updates after startup cannot change the advertised release version.
@@ -56,6 +69,12 @@ async fn exec_server_accepts_initialize(version: Option<&str>) -> anyhow::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();
let build_info = BuildInfo::get();
let target = build_info
.target()
.context("the test binary has a compiled target")?;
expected_environment.provider_id = build_id(TEST_BUILD_COMMIT, target);
assert!(expected_environment.provider_id.is_some());
assert_eq!(
initialize_response.environment_info,
Some(expected_environment.clone())
+8 -1
View File
@@ -134,7 +134,7 @@ async fn exec_server_runs_ordinary_requests_serially_by_default() -> anyhow::Res
})?,
)
.await?;
let _ = server
let response = server
.wait_for_event(|event| {
matches!(
event,
@@ -142,6 +142,10 @@ async fn exec_server_runs_ordinary_requests_serially_by_default() -> anyhow::Res
)
})
.await?;
let JSONRPCMessage::Response(JSONRPCResponse { result, .. }) = response else {
panic!("expected initialize response");
};
let initialization: InitializeResponse = serde_json::from_value(result)?;
server
.send_notification("initialized", serde_json::json!({}))
.await?;
@@ -213,6 +217,9 @@ async fn exec_server_runs_ordinary_requests_serially_by_default() -> anyhow::Res
};
assert_eq!(id, queued_environment_info_id);
let mut expected_environment_info = EnvironmentInfo::local();
expected_environment_info.provider_id = initialization
.environment_info
.and_then(|info| info.provider_id);
expected_environment_info.temporary_directories = Some(vec![PathUri::from_host_native_path(
temporary_directory.path(),
)?]);
+1
View File
@@ -5,6 +5,7 @@ exports_files([
"llvm_windows_arm64_powl.patch",
"llvm_windows_mingw_compat.patch",
"rules_rust_build_script_tools_transition.patch",
"rules_rust_group_build_script_arg_files.patch",
"rules_rust_windows_msvc_direct_link_args.patch",
"rules_rust_windows_process_wrapper_skip_temp_outputs.patch",
"rules_cc_rusty_v8_custom_libcxx.patch",
@@ -0,0 +1,12 @@
diff --git a/rust/private/rustc.bzl b/rust/private/rustc.bzl
--- a/rust/private/rustc.bzl
+++ b/rust/private/rustc.bzl
@@ -1125,5 +1125,7 @@
for build_env_file in build_env_files:
process_wrapper_flags.add("--env-file", build_env_file)
- process_wrapper_flags.add_all(build_flags_files, before_each = "--arg-file")
+ # The wrapper accepts multiple files per flag. Reduce native Windows
+ # command-line length without changing the files or their order.
+ process_wrapper_flags.add_all("--arg-file", build_flags_files)