Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions codex-rs/core/tests/suite/shell_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ use std::time::Duration;

use anyhow::Result;
use codex_protocol::models::PermissionProfile;
use codex_protocol::shell_environment::CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR;
use core_test_support::TestTargetOs;
use core_test_support::assert_regex_match;
use core_test_support::responses::ev_assistant_message;
use core_test_support::responses::ev_completed;
Expand All @@ -11,9 +13,11 @@ use core_test_support::responses::mount_sse_sequence;
use core_test_support::responses::sse;
use core_test_support::skip_if_host_windows;
use core_test_support::skip_if_no_network;
use core_test_support::skip_if_wine_exec;
use core_test_support::test_codex::TestCodexBuilder;
use core_test_support::test_codex::TestCodexHarness;
use core_test_support::test_codex::test_codex;
use core_test_support::test_target_os;
use pretty_assertions::assert_eq;
use serde_json::json;
use test_case::test_case;
Expand Down Expand Up @@ -126,6 +130,41 @@ async fn shell_command_works() -> anyhow::Result<()> {
Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn shell_command_does_not_expose_configured_noise_auth_token() -> Result<()> {
skip_if_no_network!(Ok(()));
skip_if_wine_exec!(Ok(()), "shell_command is unavailable for Wine executors");

let builder = test_codex().with_model("gpt-5.4").with_config(|config| {
config.permissions.shell_environment_policy.r#set.insert(
CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR.to_string(),
"configured-noise-token".to_string(),
);
config.permissions.shell_environment_policy.r#set.insert(
CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR.to_ascii_lowercase(),
"case-variant-noise-token".to_string(),
);
});
let harness = TestCodexHarness::with_auto_env_builder(builder).await?;
let command = match test_target_os() {
TestTargetOs::Linux | TestTargetOs::MacOs => {
"if [ -n \"${CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN:-}\" ] || [ -n \"${codex_exec_server_noise_auth_token:-}\" ]; then echo leaked; else echo unset; fi"
}
TestTargetOs::Windows => {
"if ($env:CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN) { Write-Output leaked } else { Write-Output unset }"
}
};
let call_id = "shell-command-noise-auth-token";
mount_shell_responses(&harness, call_id, command, /*login*/ None).await;
harness
.submit("check the remote execution auth token")
.await?;

assert_shell_command_output(&harness.function_call_stdout(call_id).await, "unset")?;

Ok(())
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn shell_command_rejects_justification_without_sandbox_permissions() -> Result<()> {
skip_if_no_network!(Ok(()));
Expand Down
2 changes: 1 addition & 1 deletion codex-rs/exec-server/src/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use codex_http_client::HttpClientFactory;
use codex_http_client::OutboundProxyPolicy;
use codex_protocol::capabilities::CapabilityRootLocation;
use codex_protocol::capabilities::SelectedCapabilityRoot;
use codex_protocol::shell_environment::CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR;

use crate::CapabilityRootsDiscoverParams;
use crate::CapabilityRootsDiscoverResponse;
Expand Down Expand Up @@ -50,7 +51,6 @@ pub const CODEX_EXEC_SERVER_NOISE_REGISTRY_URL_ENV_VAR: &str =
"CODEX_EXEC_SERVER_NOISE_REGISTRY_URL";
pub const CODEX_EXEC_SERVER_NOISE_ENVIRONMENT_ID_ENV_VAR: &str =
"CODEX_EXEC_SERVER_NOISE_ENVIRONMENT_ID";
pub const CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR: &str = "CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN";
pub const CODEX_EXEC_SERVER_NOISE_CHATGPT_ACCOUNT_ID_ENV_VAR: &str =
"CODEX_EXEC_SERVER_NOISE_CHATGPT_ACCOUNT_ID";

Expand Down
2 changes: 1 addition & 1 deletion codex-rs/exec-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ pub use codex_file_system::WalkEntryKind;
pub use codex_file_system::WalkError;
pub use codex_file_system::WalkOptions;
pub use codex_file_system::WalkOutcome;
pub use environment::CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR;
pub use codex_protocol::shell_environment::CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR;
pub use environment::CODEX_EXEC_SERVER_NOISE_CHATGPT_ACCOUNT_ID_ENV_VAR;
pub use environment::CODEX_EXEC_SERVER_NOISE_ENVIRONMENT_ID_ENV_VAR;
pub use environment::CODEX_EXEC_SERVER_NOISE_REGISTRY_URL_ENV_VAR;
Expand Down
43 changes: 43 additions & 0 deletions codex-rs/hooks/src/engine/command_runner_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use codex_protocol::protocol::HookOutputEntry;
use codex_protocol::protocol::HookOutputEntryKind;
use codex_protocol::protocol::HookRunStatus;
use codex_protocol::protocol::HookSource;
use codex_protocol::shell_environment::CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR;
use codex_utils_absolute_path::AbsolutePathBuf;
use pretty_assertions::assert_eq;
use tempfile::TempDir;
Expand Down Expand Up @@ -113,6 +114,48 @@ async fn fast_exiting_hook_preserves_stdout_when_stdin_is_not_consumed() {
assert_eq!(result.error, None);
}

#[tokio::test]
async fn command_hook_does_not_expose_configured_noise_auth_token() {
let temp = tempdir().expect("create temp dir");
let source_path = AbsolutePathBuf::try_from(temp.path().join("hooks.json"))
.expect("absolute hook configuration path");
let command = if cfg!(windows) { "set" } else { "env" };
let env = HashMap::from([
(
CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR.to_ascii_lowercase(),
"configured-noise-token".to_string(),
),
("CODEX_HOOK_SAFE_ENV".to_string(), "visible".to_string()),
]);
let handler = ConfiguredHandler {
event_name: HookEventName::SessionStart,
matcher: None,
timeout_sec: 10,
status_message: None,
additional_context_limit: Default::default(),
source_path,
source: HookSource::User,
display_order: 0,
kind: ConfiguredHandlerKind::Command {
command: command.to_string(),
r#async: false,
env: env.clone(),
},
};
let (runtime, _result_receiver) = runtime();

let result = run_command(&runtime, &handler, command, &env, "{}", temp.path()).await;

assert_eq!(result.exit_code, Some(0), "stderr: {}", result.stderr);
assert!(result.stdout.contains("CODEX_HOOK_SAFE_ENV=visible"));
assert!(!result.stdout.lines().any(|line| {
line.split_once('=').is_some_and(|(name, _)| {
name.eq_ignore_ascii_case(CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR)
})
}));
assert_eq!(result.error, None);
}

const ASYNC_HOOK_TEST_TIMEOUT: Duration = Duration::from_secs(30);

fn runtime() -> (CommandHookRuntime, Receiver<HookCompletedEvent>) {
Expand Down
2 changes: 2 additions & 0 deletions codex-rs/protocol/src/shell_environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@ use std::collections::HashMap;

pub const CODEX_SESSION_ID_ENV_VAR: &str = "CODEX_SESSION_ID";
pub const CODEX_THREAD_ID_ENV_VAR: &str = "CODEX_THREAD_ID";
pub const CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR: &str = "CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN";
pub const OPENAI_FEDERATION_RULE_ID_ENV_VAR: &str = "OPENAI_FEDERATION_RULE_ID";
pub const OPENAI_IDENTITY_TOKEN_FILE_ENV_VAR: &str = "OPENAI_IDENTITY_TOKEN_FILE";
pub const OPENAI_WORKLOAD_IDENTITY_CONTEXT_ENV_VAR: &str = "OPENAI_WORKLOAD_IDENTITY_CONTEXT";

/// Environment variables that model-reachable child processes must not inherit.
pub const NON_INHERITABLE_ENV_VARS: &[&str] = &[
CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR,
OPENAI_FEDERATION_RULE_ID_ENV_VAR,
OPENAI_IDENTITY_TOKEN_FILE_ENV_VAR,
OPENAI_WORKLOAD_IDENTITY_CONTEXT_ENV_VAR,
Expand Down
16 changes: 16 additions & 0 deletions codex-rs/protocol/src/shell_environment_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ fn non_inheritable_environment_is_removed_after_policy_overrides() {
"OPENAI_WORKLOAD_IDENTITY_CONTEXT".to_string(),
r#"{"instance_id":"box-one"}"#.to_string(),
),
(
"codex_exec_server_noise_auth_token".to_string(),
"inherited-noise-token".to_string(),
),
];
let policy = ShellEnvironmentPolicy {
inherit: ShellEnvironmentPolicyInherit::All,
Expand All @@ -31,6 +35,10 @@ fn non_inheritable_environment_is_removed_after_policy_overrides() {
"OpenAI_Identity_Token_File".to_string(),
"/run/identity-token".to_string(),
),
(
"Codex_Exec_Server_Noise_Auth_Token".to_string(),
"configured-noise-token".to_string(),
),
]),
..Default::default()
};
Expand All @@ -52,6 +60,10 @@ fn command_scrubber_removes_names_from_real_child_environment() {
"OpenAI_Workload_Identity_Context",
r#"{"instance_id":"box-one"}"#,
)
.env(
"Codex_Exec_Server_Noise_Auth_Token",
"inherited-noise-token",
)
.output()
.expect("run inherited-environment test process");
assert!(
Expand All @@ -66,6 +78,10 @@ fn command_scrubber_removes_names_from_real_child_environment() {
let mut command = environment_command();
command
.env("openai_identity_token_file", "/run/identity-token")
.env(
CODEX_EXEC_SERVER_NOISE_AUTH_TOKEN_ENV_VAR,
"configured-noise-token",
)
.env("SAFE", "value");
scrub_non_inheritable_env_vars(&mut command);
let output = command.output().expect("read child environment");
Expand Down
Loading