Compare commits

...

2 Commits

Author SHA1 Message Date
Ilan Bigio
693a388c51 Ensure executor injects CODEX_SESSION_ID 2025-10-20 14:46:27 -07:00
Ilan Bigio
9fb734e52c Add coverage for CODEX_SESSION_ID propagation 2025-10-17 20:07:38 -07:00
3 changed files with 130 additions and 3 deletions

View File

@@ -245,6 +245,8 @@ impl Codex {
use crate::state::SessionState;
pub(crate) const CODEX_SESSION_ID_ENV_VAR: &str = "CODEX_SESSION_ID";
/// Context for an initialized model agent
///
/// A session has at most 1 running task at a time, and can be interrupted by user input.
@@ -555,6 +557,10 @@ impl Session {
self.tx_event.clone()
}
pub(crate) fn conversation_id(&self) -> ConversationId {
self.conversation_id
}
fn next_internal_sub_id(&self) -> String {
let id = self
.next_internal_sub_id
@@ -2561,6 +2567,7 @@ mod tests {
use codex_app_server_protocol::AuthMode;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
use std::collections::HashMap;
use std::time::Duration;
use tokio::time::sleep;
@@ -2614,6 +2621,16 @@ mod tests {
assert_eq!(expected, actual);
}
#[test]
fn session_conversation_id_accessor_exposes_inner_value() {
let (session, _turn_context) = make_session_and_context();
let expected = session.conversation_id.to_string();
let actual = session.conversation_id().to_string();
assert_eq!(actual, expected);
}
#[test]
fn prefers_structured_content_when_present() {
let ctr = CallToolResult {
@@ -3347,4 +3364,80 @@ mod tests {
pretty_assertions::assert_eq!(exec_output.metadata, ResponseExecMetadata { exit_code: 0 });
assert!(exec_output.output.contains("hi"));
}
#[tokio::test]
async fn handle_container_exec_exposes_session_id_to_commands() {
use crate::exec::ExecParams;
use crate::protocol::SandboxPolicy;
use crate::turn_diff_tracker::TurnDiffTracker;
let (session, mut turn_context_raw) = make_session_and_context();
turn_context_raw.sandbox_policy = SandboxPolicy::DangerFullAccess;
let session = Arc::new(session);
let turn_context = Arc::new(turn_context_raw);
let conversation_id = session.conversation_id().to_string();
let mut env = HashMap::new();
env.insert(
CODEX_SESSION_ID_ENV_VAR.to_string(),
"incorrect".to_string(),
);
let command = if cfg!(windows) {
vec![
"cmd.exe".to_string(),
"/C".to_string(),
"echo %CODEX_SESSION_ID%".to_string(),
]
} else {
vec![
"/bin/sh".to_string(),
"-c".to_string(),
"printf %s \"$CODEX_SESSION_ID\"".to_string(),
]
};
let params = ExecParams {
command,
cwd: turn_context.cwd.clone(),
timeout_ms: Some(5_000),
env,
with_escalated_permissions: Some(false),
justification: None,
};
let turn_diff_tracker = Arc::new(tokio::sync::Mutex::new(TurnDiffTracker::new()));
let output = handle_container_exec_with_params(
"shell",
params,
Arc::clone(&session),
Arc::clone(&turn_context),
Arc::clone(&turn_diff_tracker),
"test-sub".to_string(),
"test-call".to_string(),
)
.await
.expect("command should succeed");
#[derive(Deserialize, PartialEq, Eq, Debug)]
struct ResponseExecMetadata {
exit_code: i32,
}
#[derive(Deserialize)]
struct ResponseExecOutput {
output: String,
metadata: ResponseExecMetadata,
}
let exec_output: ResponseExecOutput =
serde_json::from_str(&output).expect("valid exec output json");
assert_eq!(exec_output.metadata, ResponseExecMetadata { exit_code: 0 });
let observed = exec_output.output.trim_matches(['\r', '\n']);
assert_eq!(observed, conversation_id);
}
}

View File

@@ -1,3 +1,4 @@
use crate::codex::CODEX_SESSION_ID_ENV_VAR;
use crate::config_types::EnvironmentVariablePattern;
use crate::config_types::ShellEnvironmentPolicy;
use crate::config_types::ShellEnvironmentPolicyInherit;
@@ -19,18 +20,25 @@ fn populate_env<I>(vars: I, policy: &ShellEnvironmentPolicy) -> HashMap<String,
where
I: IntoIterator<Item = (String, String)>,
{
let vars: Vec<(String, String)> = vars.into_iter().collect();
let session_id = vars
.iter()
.find(|(key, _)| key == CODEX_SESSION_ID_ENV_VAR)
.map(|(_, value)| value.clone());
// Step 1 determine the starting set of variables based on the
// `inherit` strategy.
let mut env_map: HashMap<String, String> = match policy.inherit {
ShellEnvironmentPolicyInherit::All => vars.into_iter().collect(),
ShellEnvironmentPolicyInherit::All => vars.iter().cloned().collect(),
ShellEnvironmentPolicyInherit::None => HashMap::new(),
ShellEnvironmentPolicyInherit::Core => {
const CORE_VARS: &[&str] = &[
"HOME", "LOGNAME", "PATH", "SHELL", "USER", "USERNAME", "TMPDIR", "TEMP", "TMP",
];
let allow: HashSet<&str> = CORE_VARS.iter().copied().collect();
vars.into_iter()
.filter(|(k, _)| allow.contains(k.as_str()))
vars.iter()
.filter(|&(k, _)| allow.contains(k.as_str()))
.cloned()
.collect()
}
};
@@ -65,6 +73,10 @@ where
env_map.retain(|k, _| matches_any(k, &policy.include_only));
}
if let Some(session_id) = session_id {
env_map.insert(CODEX_SESSION_ID_ENV_VAR.to_string(), session_id);
}
env_map
}
@@ -191,4 +203,20 @@ mod tests {
};
assert_eq!(result, expected);
}
#[test]
fn test_codex_session_id_preserved() {
let vars = make_vars(&[
("PATH", "/usr/bin"),
(CODEX_SESSION_ID_ENV_VAR, "session-123"),
]);
let policy = ShellEnvironmentPolicy::default();
let result = populate_env(vars, &policy);
assert_eq!(
result.get(CODEX_SESSION_ID_ENV_VAR),
Some(&"session-123".to_string())
);
}
}

View File

@@ -7,6 +7,7 @@ use std::time::Duration;
use super::backends::ExecutionMode;
use super::backends::backend_for_mode;
use super::cache::ApprovalCache;
use crate::codex::CODEX_SESSION_ID_ENV_VAR;
use crate::codex::Session;
use crate::error::CodexErr;
use crate::error::SandboxErr;
@@ -110,6 +111,11 @@ impl Executor {
.prepare(request.params, &request.mode, &config)
.map_err(ExecError::from)?;
request.params.env.insert(
CODEX_SESSION_ID_ENV_VAR.to_string(),
session.conversation_id().to_string(),
);
// Step 3: Decide sandbox placement, prompting for approval when needed.
let sandbox_decision = select_sandbox(
&request,