Compare commits

...

8 Commits

Author SHA1 Message Date
jif-oai
1b2e60476c one more try 2026-01-21 14:17:09 +00:00
jif-oai
438995c4b5 make sh more complex 2026-01-21 14:04:02 +00:00
jif-oai
a3315dcba0 make it even stronger 2026-01-21 13:10:46 +00:00
jif-oai
4fe0475330 make shell snapshot more resilient 2026-01-21 12:40:40 +00:00
jif-oai
9e6232a6ab Merge branch 'main' into jif/shell-snapshot-stable 2026-01-21 12:21:24 +00:00
jif-oai
faa8ba433f Merge branch 'main' into jif/shell-snapshot-stable 2026-01-20 23:15:50 +00:00
jif-oai
518b87b60f fix test 2026-01-20 15:53:35 +00:00
jif-oai
debdde16d2 chore: shell snapshot as stable 2026-01-20 11:41:44 +00:00
3 changed files with 162 additions and 24 deletions

View File

@@ -341,10 +341,10 @@ pub const FEATURES: &[FeatureSpec] = &[
default_enabled: false,
},
FeatureSpec {
id: Feature::WebSearchCached,
key: "web_search_cached",
stage: Stage::Beta,
default_enabled: false,
id: Feature::ShellSnapshot,
key: "shell_snapshot",
stage: Stage::Stable,
default_enabled: true,
},
// Beta program. Rendered in the `/experimental` menu for users.
FeatureSpec {
@@ -358,13 +358,9 @@ pub const FEATURES: &[FeatureSpec] = &[
default_enabled: false,
},
FeatureSpec {
id: Feature::ShellSnapshot,
key: "shell_snapshot",
stage: Stage::Experimental {
name: "Shell snapshot",
menu_description: "Snapshot your shell environment to avoid re-running login scripts for every command.",
announcement: "NEW! Try shell snapshotting to make your Codex faster. Enable in /experimental!",
},
id: Feature::WebSearchCached,
key: "web_search_cached",
stage: Stage::Beta,
default_enabled: false,
},
FeatureSpec {

View File

@@ -205,9 +205,17 @@ alias_count=$(alias -L | wc -l | tr -d ' ')
print "# aliases $alias_count"
alias -L
print ''
export_count=$(export -p | wc -l | tr -d ' ')
export_count=$(env | awk -F= '$1 ~ /^[A-Za-z_][A-Za-z0-9_]*$/ {count++} END{print count}')
print "# exports $export_count"
export -p
env | sort | while IFS='=' read -r key value; do
case "$key" in
""|[0-9]*|*[^A-Za-z0-9_]*)
continue
;;
esac
escaped=${value//\'/\'\"\'\"\'}
print -r -- "export $key='$escaped'"
done
"##
}
@@ -232,9 +240,17 @@ alias_count=$(alias -p | wc -l | tr -d ' ')
echo "# aliases $alias_count"
alias -p
echo ''
export_count=$(export -p | wc -l | tr -d ' ')
export_count=$(env | awk -F= '$1 ~ /^[A-Za-z_][A-Za-z0-9_]*$/ {count++} END{print count}')
echo "# exports $export_count"
export -p
env | sort | while IFS='=' read -r key value; do
case "$key" in
""|[0-9]*|*[^A-Za-z0-9_]*)
continue
;;
esac
escaped=${value//\'/\'\"\'\"\'}
printf "export %s='%s'\n" "$key" "$escaped"
done
"##
}
@@ -271,18 +287,93 @@ if alias >/dev/null 2>&1; then
else
echo '# aliases 0'
fi
if export -p >/dev/null 2>&1; then
export_count=$(export -p | wc -l | tr -d ' ')
echo "# exports $export_count"
export -p
tmp="${TMPDIR:-/tmp}/codex-env-$$"
env_cmd=""
if [ -x /usr/bin/env ]; then
env_cmd="/usr/bin/env"
elif [ -x /bin/env ]; then
env_cmd="/bin/env"
elif command -v env >/dev/null 2>&1; then
env_cmd="env"
fi
if [ -n "$env_cmd" ]; then
"$env_cmd" > "$tmp"
else
export_count=$(env | wc -l | tr -d ' ')
echo "# exports $export_count"
env | sort | while IFS='=' read -r key value; do
export -p > "$tmp"
fi
if command -v sort >/dev/null 2>&1; then
if sort "$tmp" > "${tmp}.sorted"; then
mv "${tmp}.sorted" "$tmp"
else
rm -f "${tmp}.sorted"
fi
fi
count=0
if [ -n "$env_cmd" ]; then
while IFS='=' read -r key value; do
case "$key" in
""|[0-9]*|*[^A-Za-z0-9_]*)
continue
;;
esac
count=$((count+1))
done < "$tmp"
else
while IFS= read -r line; do
case "$line" in
export\ *)
rest=${line#export }
;;
declare\ -x\ *)
rest=${line#declare -x }
;;
*)
continue
;;
esac
key=${rest%%=*}
case "$key" in
""|[0-9]*|*[^A-Za-z0-9_]*)
continue
;;
esac
count=$((count+1))
done < "$tmp"
fi
echo "# exports $count"
if [ -n "$env_cmd" ]; then
while IFS='=' read -r key value; do
case "$key" in
""|[0-9]*|*[^A-Za-z0-9_]*)
continue
;;
esac
escaped=$(printf "%s" "$value" | sed "s/'/'\"'\"'/g")
printf "export %s='%s'\n" "$key" "$escaped"
done
done < "$tmp"
else
while IFS= read -r line; do
case "$line" in
export\ *)
rest=${line#export }
;;
declare\ -x\ *)
rest=${line#declare -x }
;;
*)
continue
;;
esac
key=${rest%%=*}
case "$key" in
""|[0-9]*|*[^A-Za-z0-9_]*)
continue
;;
esac
printf "export %s\n" "$rest"
done < "$tmp"
fi
rm -f "$tmp"
"##
}
@@ -537,6 +628,53 @@ mod tests {
Ok(())
}
#[cfg_attr(target_os = "windows", ignore)]
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn posix_snapshot_filters_invalid_env_keys() -> Result<()> {
static INVALID_KEY: &str = "NEXTEST_BIN_EXE_codex-write-config-schema";
static VALID_KEY: &str = "NEXTEST_BIN_EXE_codex_write_config_schema";
async fn run_snapshot_script_with_env(
shell_type: ShellType,
script: &str,
) -> Result<String> {
let shell = get_shell(shell_type.clone(), None)
.ok_or_else(|| anyhow!("No available shell for {shell_type:?}"))?;
let output = Command::new(shell.shell_path)
.arg("-c")
.arg(script)
.env(INVALID_KEY, "invalid-value")
.env(VALID_KEY, "valid-value")
.output()
.await?;
if !output.status.success() {
let status = output.status;
let stderr = String::from_utf8_lossy(&output.stderr);
bail!("Snapshot command exited with status {status}: {stderr}");
}
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
}
let bash_snapshot =
run_snapshot_script_with_env(ShellType::Bash, bash_snapshot_script()).await?;
assert!(!bash_snapshot.contains(INVALID_KEY));
assert!(bash_snapshot.contains(VALID_KEY));
let sh_snapshot = run_snapshot_script_with_env(ShellType::Sh, sh_snapshot_script()).await?;
assert!(!sh_snapshot.contains(INVALID_KEY));
assert!(sh_snapshot.contains(VALID_KEY));
if get_shell(ShellType::Zsh, None).is_some() {
let zsh_snapshot =
run_snapshot_script_with_env(ShellType::Zsh, zsh_snapshot_script()).await?;
assert!(!zsh_snapshot.contains(INVALID_KEY));
assert!(zsh_snapshot.contains(VALID_KEY));
}
Ok(())
}
#[cfg(target_os = "windows")]
#[ignore]
#[tokio::test]

View File

@@ -1,6 +1,7 @@
use anyhow::Context;
use codex_core::NewThread;
use codex_core::ThreadManager;
use codex_core::features::Feature;
use codex_core::protocol::EventMsg;
use codex_core::protocol::ExecCommandEndEvent;
use codex_core::protocol::ExecCommandSource;
@@ -129,7 +130,10 @@ async fn user_shell_cmd_can_be_interrupted() {
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn user_shell_command_history_is_persisted_and_shared_with_model() -> anyhow::Result<()> {
let server = responses::start_mock_server().await;
let mut builder = core_test_support::test_codex::test_codex();
// Disable it to ease command matching.
let mut builder = core_test_support::test_codex::test_codex().with_config(move |config| {
config.features.disable(Feature::ShellSnapshot);
});
let test = builder.build(&server).await?;
#[cfg(windows)]