Expose the Codex version to commands and turn metadata (#42395)

## What changed

- Set `CODEX_VERSION` to the running package version in user shell and unified
  exec environments, overriding stale shell-policy or snapshot values.
- Add the reserved `codex_version` field to MCP turn metadata.

## Testing

- Cover version propagation through shell policies, shell snapshots, unified
  exec, and MCP metadata.

GitOrigin-RevId: 7846c70d668176530b1f467293b749dc9ddd79a0
This commit is contained in:
jif
2026-09-03 00:17:19 +00:00
committed by copyberry
parent 91608236ea
commit 9bb1ea035f
11 changed files with 74 additions and 17 deletions
+11 -2
View File
@@ -13,6 +13,8 @@ use std::collections::HashMap;
pub use codex_protocol::shell_environment::CODEX_SESSION_ID_ENV_VAR;
pub use codex_protocol::shell_environment::CODEX_THREAD_ID_ENV_VAR;
pub(crate) const CODEX_VERSION_ENV_VAR: &str = "CODEX_VERSION";
/// Informational name of the active permission profile. Child processes can
/// overwrite this value, so it must not be treated as proof of enforcement.
pub const CODEX_PERMISSION_PROFILE_ENV_VAR: &str = "CODEX_PERMISSION_PROFILE";
@@ -35,9 +37,16 @@ pub fn create_env(
shell_environment::create_env(policy, thread_id.as_deref())
}
/// Exposes the shared root-session identity to model-reachable shell commands.
pub(crate) fn inject_session_id_env(env: &mut HashMap<String, String>, session_id: SessionId) {
/// Exposes the shared root-session identity and harness version to shell commands.
pub(crate) fn inject_session_env(env: &mut HashMap<String, String>, session_id: SessionId) {
env.insert(CODEX_SESSION_ID_ENV_VAR.to_string(), session_id.to_string());
if cfg!(windows) {
env.retain(|key, _| !key.eq_ignore_ascii_case(CODEX_VERSION_ENV_VAR));
}
env.insert(
CODEX_VERSION_ENV_VAR.to_string(),
env!("CARGO_PKG_VERSION").to_string(),
);
}
/// Injects the selected named permission profile into a shell tool's environment.
+2 -2
View File
@@ -15,7 +15,7 @@ use crate::exec::StdoutStream;
use crate::exec::execute_exec_request;
use crate::exec_env::create_env;
use crate::exec_env::inject_apply_patch_env;
use crate::exec_env::inject_session_id_env;
use crate::exec_env::inject_session_env;
use crate::sandboxing::ExecRequest;
use crate::session::TurnInput;
use crate::session::turn_context::TurnContext;
@@ -166,7 +166,7 @@ pub(crate) async fn execute_user_shell_command(
let shell_snapshot_location = turn_environment.shell_snapshot(&cwd);
let shell_environment_policy = turn_environment.shell_environment_policy();
let mut exec_env_map = create_env(shell_environment_policy, Some(session.thread_id));
inject_session_id_env(&mut exec_env_map, session.session_id());
inject_session_env(&mut exec_env_map, session.session_id());
inject_apply_patch_env(&mut exec_env_map, &turn_context.config.features);
if exec_env_map.contains_key(PROXY_ACTIVE_ENV_KEY) {
strip_managed_proxy_env(&mut exec_env_map);
+2
View File
@@ -7,6 +7,7 @@ small and focused and reuses the orchestrator for approvals + sandbox + retry.
use crate::exec_env::CODEX_PERMISSION_PROFILE_ENV_VAR;
use crate::exec_env::CODEX_SESSION_ID_ENV_VAR;
use crate::exec_env::CODEX_THREAD_ID_ENV_VAR;
use crate::exec_env::CODEX_VERSION_ENV_VAR;
use crate::sandboxing::SandboxPermissions;
use crate::shell::Shell;
use crate::shell::ShellType;
@@ -264,6 +265,7 @@ pub(crate) fn maybe_wrap_shell_lc_with_snapshot(
for key in [
CODEX_SESSION_ID_ENV_VAR,
CODEX_THREAD_ID_ENV_VAR,
CODEX_VERSION_ENV_VAR,
CODEX_PERMISSION_PROFILE_ENV_VAR,
CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS_ENV_VAR,
PLUGIN_METRICS_OUTPUT_ENV_VAR,
+16 -6
View File
@@ -494,12 +494,12 @@ fn maybe_wrap_shell_lc_with_snapshot_restores_explicit_override_precedence() {
}
#[test]
fn maybe_wrap_shell_lc_with_snapshot_restores_codex_thread_id_from_env() {
fn maybe_wrap_shell_lc_with_snapshot_restores_codex_metadata_from_env() {
let dir = tempdir().expect("create temp dir");
let snapshot_path = dir.path().join("snapshot.sh");
std::fs::write(
&snapshot_path,
"# Snapshot file\nexport CODEX_THREAD_ID='parent-thread'\n",
"# Snapshot file\nexport CODEX_THREAD_ID='parent-thread'\nexport CODEX_VERSION='old-version'\n",
)
.expect("write snapshot");
let (session_shell, shell_snapshot) =
@@ -507,24 +507,34 @@ fn maybe_wrap_shell_lc_with_snapshot_restores_codex_thread_id_from_env() {
let command = vec![
"/bin/bash".to_string(),
"-lc".to_string(),
"printf '%s' \"$CODEX_THREAD_ID\"".to_string(),
"printf '%s|%s' \"$CODEX_THREAD_ID\" \"$CODEX_VERSION\"".to_string(),
];
let env = HashMap::from([
("CODEX_THREAD_ID".to_string(), "nested-thread".to_string()),
(
"CODEX_VERSION".to_string(),
env!("CARGO_PKG_VERSION").to_string(),
),
]);
let rewritten = maybe_wrap_shell_lc_with_snapshot(
&command,
&session_shell,
Some(&shell_snapshot),
&HashMap::new(),
&HashMap::from([("CODEX_THREAD_ID".to_string(), "nested-thread".to_string())]),
&env,
&RuntimePathPrepends::default(),
);
let output = Command::new(&rewritten[0])
.args(&rewritten[1..])
.env("CODEX_THREAD_ID", "nested-thread")
.envs(env)
.output()
.expect("run rewritten command");
assert!(output.status.success(), "command failed: {output:?}");
assert_eq!(String::from_utf8_lossy(&output.stdout), "nested-thread");
assert_eq!(
String::from_utf8_lossy(&output.stdout),
concat!("nested-thread|", env!("CARGO_PKG_VERSION")),
);
}
#[test]
+5
View File
@@ -45,6 +45,7 @@ use codex_utils_git_discovery::GitRootDiscovery;
const MEMORY_GIT_METADATA_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(1);
const MODEL_KEY: &str = "model";
const CODEX_VERSION_KEY: &str = "codex_version";
const REASONING_EFFORT_KEY: &str = "reasoning_effort";
const USER_INPUT_REQUESTED_DURING_TURN_KEY: &str = "user_input_requested_during_turn";
const WORKSPACE_KIND_KEY: &str = "workspace_kind";
@@ -246,6 +247,10 @@ impl TurnMetadataState {
MODEL_KEY.to_string(),
Value::String(context.model.to_string()),
);
metadata.insert(
CODEX_VERSION_KEY.to_string(),
Value::String(env!("CARGO_PKG_VERSION").to_string()),
);
match context.reasoning_effort {
Some(reasoning_effort) => {
metadata.insert(
+2
View File
@@ -739,6 +739,7 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields(
("workspace_kind".to_string(), "projectless".to_string()),
("source".to_string(), "client-source".to_string()),
("model".to_string(), "client-supplied".to_string()),
("codex_version".to_string(), "client-supplied".to_string()),
(
"reasoning_effort".to_string(),
"client-supplied".to_string(),
@@ -932,6 +933,7 @@ fn turn_metadata_state_merges_client_metadata_without_replacing_reserved_fields(
.current_meta_value_for_mcp_request(test_mcp_turn_metadata_context())
.expect("turn metadata should be present");
assert_eq!(meta["model"].as_str(), Some("gpt-5.4"));
assert_eq!(meta["codex_version"], env!("CARGO_PKG_VERSION"));
assert_eq!(meta["reasoning_effort"].as_str(), Some("high"));
assert!(meta.get(LEGACY_CODE_MODE_TOOL_NAMES_KEY).is_none());
assert!(meta.get(TOOL_NAMESPACES_INFO_KEY).is_none());
@@ -17,10 +17,11 @@ use super::oneshot::Completion;
use crate::codex_thread::BackgroundTerminalInfo;
use crate::exec_env::CODEX_PERMISSION_PROFILE_ENV_VAR;
use crate::exec_env::CODEX_THREAD_ID_ENV_VAR;
use crate::exec_env::CODEX_VERSION_ENV_VAR;
use crate::exec_env::create_env;
use crate::exec_env::inject_apply_patch_env;
use crate::exec_env::inject_permission_profile_env;
use crate::exec_env::inject_session_id_env;
use crate::exec_env::inject_session_env;
use crate::exec_policy::ExecApprovalRequest;
use crate::guardian::GuardianReviewContext;
use crate::plugins::metrics::finish_and_track_measurements;
@@ -138,6 +139,7 @@ pub(super) fn exec_env_policy_from_shell_policy(
.collect::<Vec<_>>();
exclude.extend([
CODEX_PERMISSION_PROFILE_ENV_VAR.to_string(),
CODEX_VERSION_ENV_VAR.to_string(),
codex_apply_patch::CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS_ENV_VAR.to_string(),
PLUGIN_METRICS_OUTPUT_ENV_VAR.to_string(),
]);
@@ -145,6 +147,7 @@ pub(super) fn exec_env_policy_from_shell_policy(
r#set.retain(|key, _| {
![
CODEX_PERMISSION_PROFILE_ENV_VAR,
CODEX_VERSION_ENV_VAR,
codex_apply_patch::CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS_ENV_VAR,
PLUGIN_METRICS_OUTPUT_ENV_VAR,
]
@@ -176,6 +179,7 @@ fn env_overlay_for_exec_server(
&& (matches!(
key.as_str(),
CODEX_PERMISSION_PROFILE_ENV_VAR
| CODEX_VERSION_ENV_VAR
| codex_apply_patch::CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS_ENV_VAR
) || local_policy_env.get(*key) != Some(*value))
})
@@ -1362,7 +1366,7 @@ impl UnifiedExecProcessManager {
CODEX_THREAD_ID_ENV_VAR.to_string(),
context.session.thread_id.to_string(),
);
inject_session_id_env(&mut env, context.session.session_id());
inject_session_env(&mut env, context.session.session_id());
inject_apply_patch_env(&mut env, &turn.config.features);
let active_permission_profile = request.turn_environment.active_permission_profile();
inject_permission_profile_env(&mut env, active_permission_profile.as_ref());
@@ -53,6 +53,10 @@ fn env_overlay_for_exec_server_keeps_runtime_changes_only() {
("HOME".to_string(), "/client-home".to_string()),
("PATH".to_string(), "/client-path".to_string()),
("SHELL_SET".to_string(), "policy".to_string()),
(
CODEX_VERSION_ENV_VAR.to_string(),
"client-version".to_string(),
),
(
CODEX_PERMISSION_PROFILE_ENV_VAR.to_string(),
"current-profile".to_string(),
@@ -68,6 +72,10 @@ fn env_overlay_for_exec_server_keeps_runtime_changes_only() {
("OpenAI_Federation_Rule_Id".to_string(), "rule".to_string()),
("SHELL_SET".to_string(), "policy".to_string()),
("CODEX_THREAD_ID".to_string(), "thread-1".to_string()),
(
CODEX_VERSION_ENV_VAR.to_string(),
"client-version".to_string(),
),
(
CODEX_PERMISSION_PROFILE_ENV_VAR.to_string(),
"current-profile".to_string(),
@@ -87,6 +95,10 @@ fn env_overlay_for_exec_server_keeps_runtime_changes_only() {
HashMap::from([
("PATH".to_string(), "/sandbox-path".to_string()),
("CODEX_THREAD_ID".to_string(), "thread-1".to_string()),
(
CODEX_VERSION_ENV_VAR.to_string(),
"client-version".to_string()
),
(
CODEX_PERMISSION_PROFILE_ENV_VAR.to_string(),
"current-profile".to_string(),
@@ -107,6 +119,7 @@ fn env_overlay_for_exec_server_keeps_runtime_changes_only() {
fn exec_env_policy_excludes_non_inheritable_and_runtime_variables() {
let policy = ShellEnvironmentPolicy {
r#set: HashMap::from([
("codex_version".to_string(), "stale-version".to_string()),
(
"codex_permission_profile".to_string(),
"stale-profile".to_string(),
@@ -135,6 +148,7 @@ fn exec_env_policy_excludes_non_inheritable_and_runtime_variables() {
ignore_default_excludes: policy.ignore_default_excludes,
exclude: vec![
CODEX_PERMISSION_PROFILE_ENV_VAR.to_string(),
CODEX_VERSION_ENV_VAR.to_string(),
codex_apply_patch::CODEX_APPLY_PATCH_PRESERVE_LINE_ENDINGS_ENV_VAR.to_string(),
PLUGIN_METRICS_OUTPUT_ENV_VAR.to_string(),
],
@@ -23,7 +23,7 @@ use crate::config::NetworkProxySpec;
use crate::exec_env::CODEX_THREAD_ID_ENV_VAR;
use crate::exec_env::inject_apply_patch_env;
use crate::exec_env::inject_permission_profile_env;
use crate::exec_env::inject_session_id_env;
use crate::exec_env::inject_session_env;
use crate::session::session::Session;
use crate::session::step_context::StepContext;
use crate::shell::ShellType;
@@ -114,7 +114,7 @@ impl Session {
CODEX_THREAD_ID_ENV_VAR.to_string(),
session.thread_id().to_string(),
);
inject_session_id_env(&mut env, session.session_id());
inject_session_env(&mut env, session.session_id());
inject_apply_patch_env(&mut env, &config.features);
inject_permission_profile_env(
&mut env,
@@ -527,6 +527,10 @@ async fn approved_mcp_tool_call_metadata_records_prior_user_input_request(
let mcp_turn_metadata = apps_tool_call
.pointer("/params/_meta/x-codex-turn-metadata")
.expect("MCP tools/call turn metadata");
assert_eq!(
mcp_turn_metadata["codex_version"],
env!("CARGO_PKG_VERSION")
);
assert_eq!(
(
mcp_turn_metadata.get("root_turn_id"),
+10 -3
View File
@@ -376,6 +376,10 @@ async fn exec_command_uses_installed_environment_shell_policy_with_explicit_over
r#set: HashMap::from([
("KEEP".to_string(), "preserved".to_string()),
("DROP".to_string(), "filtered".to_string()),
(
"CODEX_VERSION".to_string(),
"configured-version".to_string(),
),
]),
..Default::default()
};
@@ -428,10 +432,10 @@ async fn exec_command_uses_installed_environment_shell_policy_with_explicit_over
let call_id = "exec-command-environment-shell-policy";
let command = match core_test_support::test_target_os() {
core_test_support::TestTargetOs::Linux | core_test_support::TestTargetOs::MacOs => {
r#"printf '%s:%s:%s' "$KEEP" "${DROP:-missing}" "${OWNER_ONLY:-missing}""#
r#"printf '%s:%s:%s:%s' "$KEEP" "${DROP:-missing}" "${OWNER_ONLY:-missing}" "$CODEX_VERSION""#
}
core_test_support::TestTargetOs::Windows => {
r#"if (Test-Path Env:DROP) { $drop = $env:DROP } else { $drop = 'missing' }; if (Test-Path Env:OWNER_ONLY) { $owner = $env:OWNER_ONLY } else { $owner = 'missing' }; Write-Output "${env:KEEP}:${drop}:${owner}""#
r#"if (Test-Path Env:DROP) { $drop = $env:DROP } else { $drop = 'missing' }; if (Test-Path Env:OWNER_ONLY) { $owner = $env:OWNER_ONLY } else { $owner = 'missing' }; Write-Output "${env:KEEP}:${drop}:${owner}:${env:CODEX_VERSION}""#
}
};
let arguments = json!({
@@ -462,7 +466,10 @@ async fn exec_command_uses_installed_environment_shell_policy_with_explicit_over
)
.await?;
let output = parse_unified_exec_output(&harness.function_call_stdout(call_id).await)?;
assert_eq!(output.output.trim(), "preserved:missing:missing");
assert_eq!(
output.output.trim(),
concat!("preserved:missing:missing:", env!("CARGO_PKG_VERSION")),
);
Ok(())
}