mirror of
https://github.com/openai/codex.git
synced 2026-10-30 19:18:49 +00:00
Expose originating tool call metadata in command environments (#52119)
## Why Command processes need thread and tool call labels for correlation, without stale values from inherited environments or cached shell snapshots. ## What changed - Export the originating `exec_command` call ID as `CODEX_TOOL_CALL_ID`; later `write_stdin` calls leave it unchanged. - Apply executor metadata as `CODEX_THREAD_ID` and `CODEX_TOOL_CALL_ID` after environment policies, overlays, and snapshot replay. Clear omitted fields when metadata is present; without metadata, preserve thread context and remove the call ID. - Exclude both IDs from newly captured snapshots and omit the call ID from capture and validation helpers. - Preserve empty call IDs, omit NUL-containing IDs, and handle case-insensitive environment names on Windows. Document that these labels support correlation, not authorization. ## Testing Add regression coverage for metadata precedence, missing and invalid IDs, snapshot capture and replay, local and remote execution with pipes and TTYs, sandboxed remote execution, and preservation of the originating call ID across later input. GitOrigin-RevId: 1027212b5d495d8e7cfac0f498c3134e060aa897
This commit is contained in:
committed by
copyberry
parent
2fdf047c96
commit
9b738582b1
@@ -14,6 +14,7 @@ use codex_exec_server::Environment;
|
||||
use codex_exec_server::ExecBackend;
|
||||
#[cfg(unix)]
|
||||
use codex_exec_server::ExecEnvPolicy;
|
||||
use codex_exec_server::ExecMetadata;
|
||||
use codex_exec_server::ExecOutputStream;
|
||||
use codex_exec_server::ExecParams;
|
||||
use codex_exec_server::ExecProcess;
|
||||
@@ -373,6 +374,123 @@ async fn shell_snapshot_v2_filters_profile_exports_and_stays_in_memory(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test_case(false, false, false; "local_pipe")]
|
||||
#[test_case(false, true, false; "local_tty")]
|
||||
#[test_case(true, false, false; "remote_pipe")]
|
||||
#[test_case(true, true, false; "remote_tty")]
|
||||
#[test_case(true, false, true; "remote_sandbox")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[serial_test::serial(remote_exec_server)]
|
||||
async fn exec_metadata_overrides_policy_and_cached_snapshot(
|
||||
use_remote: bool,
|
||||
tty: bool,
|
||||
use_sandbox: bool,
|
||||
) -> Result<()> {
|
||||
if use_sandbox
|
||||
&& let Some(warning) = codex_sandboxing::system_bwrap_warning(
|
||||
&PermissionProfile::workspace_write(),
|
||||
&std::env::current_dir()?,
|
||||
)
|
||||
{
|
||||
eprintln!("skipping sandbox test: {warning}");
|
||||
return Ok(());
|
||||
}
|
||||
let context = create_process_context(use_remote).await?;
|
||||
let home = TempDir::new()?;
|
||||
let cwd = PathUri::from_host_native_path(home.path())?;
|
||||
std::fs::write(
|
||||
home.path().join(".bashrc"),
|
||||
"printf '%s|%s\n' \"${CODEX_THREAD_ID-unset}\" \"${CODEX_TOOL_CALL_ID-unset}\" >> \"$HOME/capture-env\"\nexport CODEX_THREAD_ID=snapshot-thread\nexport CODEX_TOOL_CALL_ID=snapshot-call\n",
|
||||
)?;
|
||||
let policy = ExecEnvPolicy {
|
||||
inherit: ShellEnvironmentPolicyInherit::None,
|
||||
ignore_default_excludes: true,
|
||||
exclude: Vec::new(),
|
||||
r#set: HashMap::from([
|
||||
("HOME".into(), home.path().to_string_lossy().into_owned()),
|
||||
("PATH".into(), "/usr/bin:/bin".into()),
|
||||
("CODEX_THREAD_ID".into(), "policy-thread".into()),
|
||||
("CODEX_TOOL_CALL_ID".into(), "policy-call".into()),
|
||||
]),
|
||||
include_only: Vec::new(),
|
||||
};
|
||||
let thread_id = codex_protocol::ThreadId::from_string("019d25f0-6728-7ce2-908f-c3c187323e5b")?;
|
||||
let later_thread_id =
|
||||
codex_protocol::ThreadId::from_string("019d25f0-6728-7ce2-908f-c3c187323e5c")?;
|
||||
|
||||
for (attempt, call_id, expected) in [
|
||||
(0, Some(""), ""),
|
||||
(1, Some("call-λ-'\";$()"), "call-λ-'\";$()"),
|
||||
(2, None, "unset"),
|
||||
(3, Some("exec-invalid\0call"), "unset"),
|
||||
] {
|
||||
let execution_thread_id = if attempt == 1 {
|
||||
later_thread_id
|
||||
} else {
|
||||
thread_id
|
||||
};
|
||||
let started = context
|
||||
.backend
|
||||
.start(ExecParams {
|
||||
metadata: Some(ExecMetadata {
|
||||
thread_id: Some(execution_thread_id),
|
||||
tool_call_id: call_id.map(str::to_string),
|
||||
}),
|
||||
process_id: ProcessId::from(format!("metadata-snapshot-{attempt}")),
|
||||
argv: vec![
|
||||
"/bin/bash".into(),
|
||||
"-lc".into(),
|
||||
"printf '%s|%s' \"$CODEX_THREAD_ID\" \"${CODEX_TOOL_CALL_ID-unset}\""
|
||||
.to_string(),
|
||||
],
|
||||
cwd: cwd.clone(),
|
||||
env_policy: Some(policy.clone()),
|
||||
shell_snapshot: Some(ShellSnapshotRequest {
|
||||
scope_id: "metadata-attachment".into(),
|
||||
shell: ShellInfo {
|
||||
name: "bash".into(),
|
||||
path: "/bin/bash".into(),
|
||||
},
|
||||
}),
|
||||
env: HashMap::from([
|
||||
("CODEX_THREAD_ID".into(), "overlay-thread".into()),
|
||||
("CODEX_TOOL_CALL_ID".into(), "overlay-call".into()),
|
||||
]),
|
||||
tty,
|
||||
pipe_stdin: false,
|
||||
arg0: None,
|
||||
sandbox: use_sandbox.then(|| {
|
||||
FileSystemSandboxContext::from_permission_profile(
|
||||
PermissionProfile::workspace_write(),
|
||||
cwd.clone(),
|
||||
)
|
||||
}),
|
||||
enforce_managed_network: false,
|
||||
managed_network: None,
|
||||
network_proxy: None,
|
||||
})
|
||||
.await?;
|
||||
let (stdout, stderr, status, closed) =
|
||||
collect_process_output_from_events(started.process).await?;
|
||||
assert_eq!(
|
||||
(stdout, stderr, status, closed),
|
||||
(
|
||||
format!("{execution_thread_id}|{expected}"),
|
||||
String::new(),
|
||||
Some(0),
|
||||
true,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(home.path().join("capture-env"))?,
|
||||
"019d25f0-6728-7ce2-908f-c3c187323e5b|unset\n"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
#[serial_test::serial(remote_exec_server)]
|
||||
@@ -715,16 +833,43 @@ async fn remote_sandboxed_process_preserves_custom_arg0() -> Result<()> {
|
||||
|
||||
async fn assert_exec_process_starts_and_exits(use_remote: bool) -> Result<()> {
|
||||
let context = create_process_context(use_remote).await?;
|
||||
let thread_id = codex_protocol::ThreadId::from_string("019d25f0-6728-7ce2-908f-c3c187323e5b")?;
|
||||
let call_id = "call-λ-'\";$()";
|
||||
#[cfg(unix)]
|
||||
let argv = vec![
|
||||
"/bin/sh".into(),
|
||||
"-c".into(),
|
||||
"printf '%s|%s|%s' \"$CODEX_THREAD_ID\" \"$CODEX_TOOL_CALL_ID\" \"$CUSTOM_ENV_VAR\""
|
||||
.to_string(),
|
||||
];
|
||||
#[cfg(windows)]
|
||||
let argv = vec![
|
||||
"powershell.exe".into(),
|
||||
"-NoProfile".into(),
|
||||
"-Command".into(),
|
||||
"[Console]::OutputEncoding = [Text.UTF8Encoding]::new($false); [Console]::Write($env:CODEX_THREAD_ID + '|' + $env:CODEX_TOOL_CALL_ID + '|' + $env:CUSTOM_ENV_VAR)".into(),
|
||||
];
|
||||
let mut env = std::env::vars().collect::<HashMap<_, _>>();
|
||||
env.extend([
|
||||
("CODEX_THREAD_ID".into(), "stale-thread".into()),
|
||||
("Codex_Thread_Id".into(), "mixed-case-thread".into()),
|
||||
("CODEX_TOOL_CALL_ID".into(), "stale-call".into()),
|
||||
("Codex_Tool_Call_Id".into(), "mixed-case-call".into()),
|
||||
("CUSTOM_ENV_VAR".into(), "caller-owned".into()),
|
||||
]);
|
||||
let session = context
|
||||
.backend
|
||||
.start(ExecParams {
|
||||
metadata: Default::default(),
|
||||
metadata: Some(ExecMetadata {
|
||||
thread_id: Some(thread_id),
|
||||
tool_call_id: Some(call_id.to_string()),
|
||||
}),
|
||||
process_id: ProcessId::from("proc-1"),
|
||||
argv: vec!["true".to_string()],
|
||||
argv,
|
||||
cwd: PathUri::from_host_native_path(std::env::current_dir()?)?,
|
||||
shell_snapshot: None,
|
||||
env_policy: /*env_policy*/ None,
|
||||
env: Default::default(),
|
||||
env,
|
||||
tty: false,
|
||||
pipe_stdin: false,
|
||||
arg0: None,
|
||||
@@ -736,6 +881,17 @@ async fn assert_exec_process_starts_and_exits(use_remote: bool) -> Result<()> {
|
||||
.await?;
|
||||
assert_eq!(session.process.process_id().as_str(), "proc-1");
|
||||
let wake_rx = session.process.subscribe_wake();
|
||||
let (stdout, stderr, status, closed) =
|
||||
collect_process_output_from_events(Arc::clone(&session.process)).await?;
|
||||
assert_eq!(
|
||||
(stdout, stderr, status, closed),
|
||||
(
|
||||
format!("{thread_id}|{call_id}|caller-owned"),
|
||||
String::new(),
|
||||
Some(0),
|
||||
true,
|
||||
)
|
||||
);
|
||||
let (_, exit_code, closed) =
|
||||
collect_process_output_from_reads(session.process, wake_rx).await?;
|
||||
|
||||
@@ -1776,7 +1932,6 @@ async fn remote_exec_process_recovers_after_transport_disconnect() -> Result<()>
|
||||
|
||||
#[test_case(false ; "local")]
|
||||
#[test_case(true ; "remote")]
|
||||
#[cfg_attr(not(unix), ignore = "Unix-only exec-server process test")]
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
// Serialize tests that launch a real exec-server process through the full CLI.
|
||||
#[serial_test::serial(remote_exec_server)]
|
||||
|
||||
Reference in New Issue
Block a user