use std::collections::BTreeMap;
use std::ffi::{OsStr, OsString};
use std::path::Path;
use crate::config::ReasoningEffort;
use thiserror::Error;
#[derive(Clone, Debug, Default)]
pub(crate) struct ChildEnvironment {
values: BTreeMap<OsString, OsString>,
}
impl ChildEnvironment {
pub(crate) fn from_iter(values: impl IntoIterator<Item = (OsString, OsString)>) -> Self {
let values = values
.into_iter()
.filter(|(name, _)| allowed_name(name))
.collect();
Self { values }
}
pub(crate) fn current() -> Self {
Self::from_iter(std::env::vars_os())
}
pub(crate) fn apply_to(&self, command: &mut tokio::process::Command) {
self.apply_to_std(command.as_std_mut());
}
pub(crate) fn apply_to_std(&self, command: &mut std::process::Command) {
command.env_clear().envs(self.values.iter());
}
#[cfg(test)]
pub(crate) fn get(&self, name: &str) -> Option<&str> {
self.values
.get(OsStr::new(name))
.and_then(|value| value.to_str())
}
}
pub(crate) fn toml_override(key: &str, value: &str) -> String {
format!("{key}={}", toml::Value::String(value.to_owned()))
}
#[derive(Debug, Error)]
pub(crate) enum CommandError {
#[error("Codex instructions path is not valid UTF-8")]
NonUtf8InstructionsPath,
}
pub(crate) fn invocation_args(
model: &str,
effort: Option<&ReasoningEffort>,
instructions: &Path,
schema: &Path,
cwd: &Path,
) -> Result<Vec<OsString>, CommandError> {
let instructions = instructions
.to_str()
.ok_or(CommandError::NonUtf8InstructionsPath)?;
let schema = schema.as_os_str();
let cwd = cwd.as_os_str();
let mut args = vec![
OsString::from("-c"),
OsString::from(toml_override("forced_login_method", "chatgpt")),
OsString::from("-c"),
OsString::from(toml_override("model_instructions_file", instructions)),
];
if let Some(effort) = effort {
args.push(OsString::from("-c"));
args.push(OsString::from(toml_override(
"model_reasoning_effort",
effort.as_str(),
)));
}
args.extend([
OsString::from("-c"),
OsString::from("project_doc_max_bytes=0"),
OsString::from("-c"),
OsString::from(toml_override("web_search", "disabled")),
OsString::from("--disable"),
OsString::from("shell_tool"),
OsString::from("--disable"),
OsString::from("unified_exec"),
OsString::from("--disable"),
OsString::from("apps"),
OsString::from("--disable"),
OsString::from("multi_agent"),
OsString::from("--disable"),
OsString::from("hooks"),
OsString::from("--disable"),
OsString::from("memories"),
OsString::from("-a"),
OsString::from("never"),
OsString::from("exec"),
OsString::from("--ephemeral"),
OsString::from("--ignore-user-config"),
OsString::from("--ignore-rules"),
OsString::from("--sandbox"),
OsString::from("read-only"),
OsString::from("--skip-git-repo-check"),
OsString::from("-C"),
cwd.to_owned(),
OsString::from("--model"),
OsString::from(model),
OsString::from("--output-schema"),
schema.to_owned(),
OsString::from("--json"),
OsString::from("-"),
]);
Ok(args)
}
pub(crate) fn instructions_text(system_prompt: &str) -> String {
format!(
"{system_prompt}\n\nIsolation contract:\n\
- Use only the code supplied on stdin.\n\
- Do not use tools, commands, files, network access, apps, MCP, or subagents.\n\
- Return only the JSON object required by the response schema.\n"
)
}
fn allowed_name(name: &OsStr) -> bool {
let Some(name) = name.to_str() else {
return false;
};
if !allowed_nonsensitive_name(name, cfg!(windows)) {
return false;
}
!sensitive_name(name)
}
pub(crate) fn allowed_nonsensitive_name(name: &str, windows: bool) -> bool {
const ALLOWED: &[&str] = &[
"PATH",
"HOME",
"CODEX_HOME",
"TMPDIR",
"TMP",
"TEMP",
"SSL_CERT_FILE",
"SSL_CERT_DIR",
"REQUESTS_CA_BUNDLE",
"HTTP_PROXY",
"HTTPS_PROXY",
"ALL_PROXY",
"NO_PROXY",
"http_proxy",
"https_proxy",
"all_proxy",
"no_proxy",
"LANG",
"LC_ALL",
"TZ",
"SYSTEMROOT",
"WINDIR",
"USERPROFILE",
"APPDATA",
"LOCALAPPDATA",
"ComSpec",
"PATHEXT",
];
if windows {
ALLOWED
.iter()
.any(|allowed| name.eq_ignore_ascii_case(allowed))
|| name
.get(..3)
.is_some_and(|prefix| prefix.eq_ignore_ascii_case("LC_"))
} else {
ALLOWED.contains(&name) || name.starts_with("LC_")
}
}
pub(crate) fn sensitive_name(name: &str) -> bool {
let name = name.to_ascii_uppercase();
name.starts_with("DREP_")
|| [
"_API_KEY",
"TOKEN",
"SECRET",
"PASSWORD",
"CREDENTIAL",
"AUTH",
]
.iter()
.any(|pattern| name.contains(pattern))
}