mirror of
https://github.com/openai/codex.git
synced 2026-10-20 17:38:45 +00:00
Support remote credential masking with Zsh shell snapshots (#54432)
## Why Remote credential masking previously supported only Bash commands without login startup or shell snapshots, leaving Zsh commands with unmasked credentials. ## What changed - Enable masking for Zsh login commands using shell snapshots on Linux and macOS. - Capture trusted startup profiles per launch, bind the credential broker to the resulting environment, and replace credentials and supported aliases with placeholders before snapshot replay. - Preserve explicit environment overrides, credential unsets, and launcher-managed proxy settings during replay. - Warn and fall back to unmasked startup when a snapshot cannot safely replay credentials; stop execution if protected snapshot restoration fails. ## Testing Extend remote execution tests for inherited and profile-defined credentials, overrides, destination changes, and unsupported-profile fallback. Add a Zsh scenario snapshot showing a credential placeholder in command output. GitOrigin-RevId: 83193d6cbc05f3deb0d6d0b9ae80974b353ce900
This commit is contained in:
@@ -10,6 +10,8 @@ mod exports;
|
||||
mod literals;
|
||||
#[path = "shell_snapshot_render.rs"]
|
||||
mod render;
|
||||
#[path = "shell_snapshot_restore.rs"]
|
||||
mod restore;
|
||||
|
||||
pub use capture::CapturedSnapshot;
|
||||
pub use capture::CapturedStartupEnvironment;
|
||||
@@ -19,6 +21,9 @@ pub use capture::snapshot_source_capture_script;
|
||||
pub use credentials::PreparedSnapshot;
|
||||
pub use credentials::SnapshotCredentialEnvironment;
|
||||
pub use credentials::prepare_snapshot_credentials;
|
||||
pub use restore::SNAPSHOT_BROKERED_UNSET_ENV_PREFIX;
|
||||
pub use restore::SNAPSHOT_BROKERED_VALUE_ENV_PREFIX;
|
||||
pub use restore::build_brokered_credential_exports;
|
||||
|
||||
use capture::BASH_SH_SNAPSHOT_HEADER;
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ use super::render;
|
||||
use super::render::Export;
|
||||
use super::render::Value;
|
||||
use super::render::ValuePart;
|
||||
use crate::shell_detect::ShellType;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// Environment views used to render protected credential references into a shell snapshot.
|
||||
@@ -106,7 +107,9 @@ pub fn prepare_snapshot_credentials(
|
||||
let next_credential = credential_keys
|
||||
.iter()
|
||||
.filter_map(|credential_key| {
|
||||
let dummy = discovered.get(*credential_key)?;
|
||||
let dummy = discovered
|
||||
.get(*credential_key)
|
||||
.filter(|dummy| !dummy.is_empty())?;
|
||||
let real = real_credential_value(credential_key)?;
|
||||
let allowed_key = allowed_brokered_keys
|
||||
.iter()
|
||||
@@ -296,11 +299,24 @@ pub fn prepare_snapshot_credentials(
|
||||
return Some(Export::Captured(line));
|
||||
}
|
||||
return Some(Export::Assignment {
|
||||
declaration,
|
||||
declaration: if captured.shell_type == ShellType::Zsh
|
||||
&& configured.contains_key(key)
|
||||
{
|
||||
format!("export {key}").into()
|
||||
} else {
|
||||
declaration.into()
|
||||
},
|
||||
value: assignment,
|
||||
});
|
||||
}
|
||||
|
||||
// Explicit scalar overrides must not inherit captured coercion attributes.
|
||||
if captured.shell_type == ShellType::Zsh
|
||||
&& configured.contains_key(key)
|
||||
&& matches!(export.value, ExportValue::Plain)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
Some(Export::Captured(line))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
use super::capture::CapturedSnapshot;
|
||||
use codex_protocol::config_types::ShellEnvironmentPolicy;
|
||||
use codex_protocol::shell_environment::create_env_from_vars;
|
||||
use std::borrow::Cow;
|
||||
|
||||
impl CapturedSnapshot<'_> {
|
||||
/// Render native functions, options, and aliases; the executor restores environment separately.
|
||||
@@ -45,7 +46,7 @@ pub(super) enum ValuePart {
|
||||
pub(super) enum Export<'a> {
|
||||
Captured(&'a str),
|
||||
Assignment {
|
||||
declaration: &'a str,
|
||||
declaration: Cow<'a, str>,
|
||||
value: Value,
|
||||
},
|
||||
Array {
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
//! Restore brokered values after shell startup and snapshot replay.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub const SNAPSHOT_BROKERED_VALUE_ENV_PREFIX: &str = "CODEX_NETWORK_PROXY_SNAPSHOT_BROKERED_VALUE_";
|
||||
pub const SNAPSHOT_BROKERED_UNSET_ENV_PREFIX: &str = "CODEX_NETWORK_PROXY_SNAPSHOT_BROKERED_UNSET_";
|
||||
|
||||
/// Restore protected credential copies and unsets after shell startup.
|
||||
/// Keep the copies for another replay stage, or remove them before the user command.
|
||||
pub fn build_brokered_credential_exports(
|
||||
env: &HashMap<String, String>,
|
||||
remove_copies: bool,
|
||||
) -> String {
|
||||
let mut value_copies = env
|
||||
.keys()
|
||||
.filter_map(|copy_key| {
|
||||
let key = copy_key.strip_prefix(SNAPSHOT_BROKERED_VALUE_ENV_PREFIX)?;
|
||||
is_valid_shell_variable_name(key).then_some((key, copy_key))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
value_copies.sort_unstable();
|
||||
let value_exports = value_copies
|
||||
.into_iter()
|
||||
.map(|(key, copy_key)| {
|
||||
let restore = format!(
|
||||
"if [ -z \"${{{copy_key}+x}}\" ]; then exit 1; fi\nif [ -n \"${{{key}+x}}\" ] && [ -n \"${{{key}}}\" ] && [ \"${{{key}}}\" != \"${{{copy_key}}}\" ]; then export {key}=\"${{{copy_key}}}\" || exit 1; fi\nif [ -n \"${{{key}+x}}\" ] && [ -n \"${{{key}}}\" ] && [ \"${{{key}}}\" != \"${{{copy_key}}}\" ]; then exit 1; fi"
|
||||
);
|
||||
if remove_copies {
|
||||
format!("{restore}\nunset {copy_key} || exit 1")
|
||||
} else {
|
||||
restore
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let mut unset_markers = env
|
||||
.keys()
|
||||
.filter_map(|marker_key| {
|
||||
let key = marker_key.strip_prefix(SNAPSHOT_BROKERED_UNSET_ENV_PREFIX)?;
|
||||
is_valid_shell_variable_name(key).then_some((key, marker_key))
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
unset_markers.sort_unstable();
|
||||
let unset_exports = unset_markers
|
||||
.into_iter()
|
||||
.map(|(key, marker_key)| {
|
||||
let restore = format!(
|
||||
"if [ -z \"${{{marker_key}+x}}\" ]; then exit 1; fi\nif [ -n \"${{{key}+x}}\" ] && [ -n \"${{{key}}}\" ]; then unset {key} || exit 1; fi\nif [ -n \"${{{key}+x}}\" ] && [ -n \"${{{key}}}\" ]; then exit 1; fi"
|
||||
);
|
||||
if remove_copies {
|
||||
format!("{restore}\nunset {marker_key} || exit 1")
|
||||
} else {
|
||||
restore
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let exports = [value_exports, unset_exports]
|
||||
.into_iter()
|
||||
.filter(|block| !block.is_empty())
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
if exports.is_empty() {
|
||||
return exports;
|
||||
}
|
||||
format!(
|
||||
"case $- in\n *x*) __CODEX_SNAPSHOT_BROKER_XTRACE=1; set +x ;;\n *) __CODEX_SNAPSHOT_BROKER_XTRACE= ;;\nesac\n{exports}\nif [ -n \"$__CODEX_SNAPSHOT_BROKER_XTRACE\" ]; then\n unset __CODEX_SNAPSHOT_BROKER_XTRACE\n set -x\nelse\n unset __CODEX_SNAPSHOT_BROKER_XTRACE\nfi"
|
||||
)
|
||||
}
|
||||
|
||||
fn is_valid_shell_variable_name(name: &str) -> bool {
|
||||
let mut chars = name.chars();
|
||||
let Some(first) = chars.next() else {
|
||||
return false;
|
||||
};
|
||||
if !(first == '_' || first.is_ascii_alphabetic()) {
|
||||
return false;
|
||||
}
|
||||
chars.all(|c| c == '_' || c.is_ascii_alphanumeric())
|
||||
}
|
||||
Reference in New Issue
Block a user