mirror of
https://github.com/openai/codex.git
synced 2026-11-05 20:10:33 +00:00
Preserve complete shell snapshot exports through filtering and replay (#43907)
## Why Line-based export parsing can truncate multiline values or mistake their contents for declarations. Snapshot replay also needs to restore Bash options before parsing functions that use extended glob syntax. ## What changed - Capture shell state, aliases, and complete export records with NUL-delimited boundaries, separating capture, credential preparation, and rendering. - Apply credential and environment policy to whole exports, preserving multiline values and making credential aliases follow allowed overrides. - Restore Bash `shopt` options before functions, including for Bash running as `sh`. - Support non-evaluating POSIX `ENV` path expansion and preserve native environment metadata and unset exports. - Move core and exec-server snapshot consumers to the shared capture parser. Allow capture overhead while enforcing the exec-server's 512 KiB state-and-environment limit before filtering. ## Testing Add regression coverage for complete capture records, multiline export filtering and replay, credential alias overrides, `ENV` expansion, and `PATH` export state. Extend exec-server tests to cover Bash-backed `sh`, `extglob`, `nocasematch`, `set -u`, and large environments. GitOrigin-RevId: 7589ab4d137529a395dcee10b5162718de24a621
This commit is contained in:
@@ -17,7 +17,10 @@ use anyhow::bail;
|
||||
use codex_exec_server::Environment;
|
||||
use codex_otel::SessionTelemetry;
|
||||
use codex_protocol::ThreadId;
|
||||
use codex_shell_command::shell_snapshot::snapshot_script;
|
||||
use codex_shell_command::shell_snapshot::CapturedSnapshot;
|
||||
use codex_shell_command::shell_snapshot::SnapshotCaptureOptions;
|
||||
use codex_shell_command::shell_snapshot::SnapshotStartup;
|
||||
use codex_shell_command::shell_snapshot::snapshot_capture_script;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_path_uri::PathUri;
|
||||
use tokio::fs;
|
||||
@@ -209,8 +212,7 @@ async fn write_shell_snapshot(
|
||||
let shell =
|
||||
get_shell(shell_type).with_context(|| format!("No available shell for {shell_type:?}"))?;
|
||||
|
||||
let raw_snapshot = capture_snapshot(&shell, cwd).await?;
|
||||
let snapshot = strip_snapshot_preamble(&raw_snapshot)?;
|
||||
let snapshot = capture_snapshot(&shell, cwd).await?;
|
||||
|
||||
if let Some(parent) = output_path.parent() {
|
||||
let parent_display = parent.display();
|
||||
@@ -229,18 +231,19 @@ async fn write_shell_snapshot(
|
||||
|
||||
async fn capture_snapshot(shell: &Shell, cwd: &AbsolutePathBuf) -> Result<String> {
|
||||
let shell_type = shell.shell_type;
|
||||
let script = snapshot_script(shell_type)
|
||||
.ok_or_else(|| anyhow!("Shell snapshotting is not yet supported for {shell_type:?}"))?;
|
||||
run_shell_script(shell, &script, cwd).await
|
||||
}
|
||||
|
||||
fn strip_snapshot_preamble(snapshot: &str) -> Result<String> {
|
||||
let marker = "# Snapshot file";
|
||||
let Some(start) = snapshot.find(marker) else {
|
||||
bail!("Snapshot output missing marker {marker}");
|
||||
};
|
||||
|
||||
Ok(snapshot[start..].to_string())
|
||||
let script = snapshot_capture_script(
|
||||
shell_type,
|
||||
SnapshotCaptureOptions {
|
||||
startup: SnapshotStartup::Interactive,
|
||||
declarations: true,
|
||||
environment: false,
|
||||
},
|
||||
)
|
||||
.ok_or_else(|| anyhow!("Shell snapshotting is not yet supported for {shell_type:?}"))?;
|
||||
let captured = run_shell_script(shell, &script, cwd).await?;
|
||||
CapturedSnapshot::parse(shell_type, captured.as_bytes())
|
||||
.map(|snapshot| snapshot.render_script())
|
||||
.ok_or_else(|| anyhow!("Invalid shell snapshot capture"))
|
||||
}
|
||||
|
||||
async fn validate_snapshot(
|
||||
|
||||
@@ -86,19 +86,6 @@ async fn get_snapshot(shell_type: ShellType) -> Result<String> {
|
||||
Ok(content)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strip_snapshot_preamble_removes_leading_output() {
|
||||
let snapshot = "noise\n# Snapshot file\nexport PATH=/bin\n";
|
||||
let cleaned = strip_snapshot_preamble(snapshot).expect("snapshot marker exists");
|
||||
assert_eq!(cleaned, "# Snapshot file\nexport PATH=/bin\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn strip_snapshot_preamble_requires_marker() {
|
||||
let result = strip_snapshot_preamble("missing header");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn snapshot_file_name_parser_supports_legacy_and_suffixed_names() {
|
||||
let session_id = "019cf82b-6a62-7700-bbbd-46909794ef89";
|
||||
@@ -217,7 +204,15 @@ async fn snapshot_shell_does_not_inherit_stdin() -> Result<()> {
|
||||
let home_display = home.display();
|
||||
let script = format!(
|
||||
"HOME=\"{home_display}\"; export HOME; {}",
|
||||
snapshot_script(ShellType::Bash).expect("bash supports snapshots")
|
||||
snapshot_capture_script(
|
||||
ShellType::Bash,
|
||||
SnapshotCaptureOptions {
|
||||
startup: SnapshotStartup::Interactive,
|
||||
declarations: true,
|
||||
environment: false,
|
||||
}
|
||||
)
|
||||
.expect("bash supports snapshots")
|
||||
);
|
||||
let output = run_script_with_timeout(
|
||||
&shell,
|
||||
|
||||
Reference in New Issue
Block a user