Files
codex/codex-rs/shell-command/src/shell_snapshot_render.rs
T
viyatb-oai 1c5ca2a979 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
2026-10-15 22:25:58 +00:00

120 lines
3.6 KiB
Rust

//! Serialize prepared snapshot values and apply export-name policy without executing shell code.
//! Native declarations stay unchanged unless the credential stage supplies a replacement.
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.
pub fn render_state(&self) -> String {
format!("{}{}", self.state, self.aliases)
}
/// Persist only exports admitted by the policy. Explicit overrides are supplied
/// by the command environment; never retain their original captured values.
pub fn render_script(&self, policy: &ShellEnvironmentPolicy) -> String {
let allowed = create_env_from_vars(
self.exports
.iter()
.map(|export| (export.key.to_string(), String::new())),
policy,
/*thread_id*/ None,
);
let mut script = self.render_state();
script.push_str("# exports (native declarations)\n");
for export in &self.exports {
if allowed.contains_key(export.key) && !policy.r#set.contains_key(export.key) {
script.push_str(&export.source);
}
}
script
}
}
#[derive(Default)]
pub(super) struct Value {
pub parts: Vec<ValuePart>,
}
pub(super) enum ValuePart {
Literal(String),
Credential { key: String },
}
pub(super) enum Export<'a> {
Captured(&'a str),
Assignment {
declaration: Cow<'a, str>,
value: Value,
},
Array {
prefix: &'a str,
elements: Vec<Value>,
suffix: &'a str,
},
ArrayBinding {
key: &'a str,
declaration: &'a str,
suffix: &'a str,
},
}
impl Value {
fn render(&self) -> Option<String> {
let mut output = String::new();
for part in &self.parts {
match part {
ValuePart::Literal(value) => {
output.push_str(shlex::try_quote(value).ok()?.as_ref())
}
ValuePart::Credential { key } => {
output.push_str(&format!("\"${{{key}-}}\""));
}
}
}
if output.is_empty() {
output.push_str("''");
}
Some(output)
}
}
pub(super) fn render(state: &str, aliases: &str, exports: &[Export<'_>]) -> Option<String> {
let mut output = format!("{state}{aliases}");
if !exports.is_empty() {
output.push_str("# exports (native declarations)\n");
}
for export in exports {
match export {
Export::Captured(source) => output.push_str(source),
Export::Assignment { declaration, value } => {
output.push_str(&format!("{declaration}={}\n", value.render()?));
}
Export::Array {
prefix,
elements,
suffix,
} => {
let elements = elements
.iter()
.map(Value::render)
.collect::<Option<Vec<_>>>()?;
output.push_str(&format!("{prefix}({}){suffix}", elements.join(" ")));
}
Export::ArrayBinding {
key,
declaration,
suffix,
} => {
output.push_str(&format!(
"if [ \"${{{key}+x}}\" = x ]; then\n{declaration}{}\nfi\n",
suffix.trim_end()
));
}
}
}
Some(output)
}