mirror of
https://github.com/openai/codex.git
synced 2026-04-18 13:31:43 +03:00
Compare commits
8 Commits
tokencount
...
user-frien
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
19606f0b36 | ||
|
|
62bd0e3d9d | ||
|
|
a9c68ea270 | ||
|
|
ac58749bd3 | ||
|
|
79cbd2ab1b | ||
|
|
f08b08680f | ||
|
|
55d876404b | ||
|
|
efd82025a5 |
22
.github/workflows/rust-ci.yml
vendored
22
.github/workflows/rust-ci.yml
vendored
@@ -63,6 +63,24 @@ jobs:
|
||||
- name: cargo fmt
|
||||
run: cargo fmt -- --config imports_granularity=Item --check
|
||||
|
||||
cargo_shear:
|
||||
name: cargo shear
|
||||
runs-on: ubuntu-24.04
|
||||
needs: changed
|
||||
if: ${{ needs.changed.outputs.codex == 'true' || needs.changed.outputs.workflows == 'true' || github.event_name == 'push' }}
|
||||
defaults:
|
||||
run:
|
||||
working-directory: codex-rs
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
- uses: dtolnay/rust-toolchain@1.89
|
||||
- uses: taiki-e/install-action@v2
|
||||
with:
|
||||
tool: cargo-shear
|
||||
version: 1.5.1
|
||||
- name: cargo shear
|
||||
run: cargo shear
|
||||
|
||||
# --- CI to validate on different os/targets --------------------------------
|
||||
lint_build_test:
|
||||
name: ${{ matrix.runner }} - ${{ matrix.target }}${{ matrix.profile == 'release' && ' (release)' || '' }}
|
||||
@@ -182,7 +200,7 @@ jobs:
|
||||
# --- Gatherer job that you mark as the ONLY required status -----------------
|
||||
results:
|
||||
name: CI results (required)
|
||||
needs: [changed, general, lint_build_test]
|
||||
needs: [changed, general, cargo_shear, lint_build_test]
|
||||
if: always()
|
||||
runs-on: ubuntu-24.04
|
||||
steps:
|
||||
@@ -190,6 +208,7 @@ jobs:
|
||||
shell: bash
|
||||
run: |
|
||||
echo "general: ${{ needs.general.result }}"
|
||||
echo "shear : ${{ needs.cargo_shear.result }}"
|
||||
echo "matrix : ${{ needs.lint_build_test.result }}"
|
||||
|
||||
# If nothing relevant changed (PR touching only root README, etc.),
|
||||
@@ -201,4 +220,5 @@ jobs:
|
||||
|
||||
# Otherwise require the jobs to have succeeded
|
||||
[[ '${{ needs.general.result }}' == 'success' ]] || { echo 'general failed'; exit 1; }
|
||||
[[ '${{ needs.cargo_shear.result }}' == 'success' ]] || { echo 'cargo_shear failed'; exit 1; }
|
||||
[[ '${{ needs.lint_build_test.result }}' == 'success' ]] || { echo 'matrix failed'; exit 1; }
|
||||
|
||||
622
codex-rs/Cargo.lock
generated
622
codex-rs/Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -12,7 +12,6 @@ clap = { version = "4", features = ["derive"] }
|
||||
codex-common = { path = "../common", features = ["cli"] }
|
||||
codex-core = { path = "../core" }
|
||||
codex-protocol = { path = "../protocol" }
|
||||
reqwest = { version = "0.12", features = ["json", "stream"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
@@ -26,14 +26,12 @@ eventsource-stream = "0.2.3"
|
||||
futures = "0.3"
|
||||
libc = "0.2.175"
|
||||
mcp-types = { path = "../mcp-types" }
|
||||
mime_guess = "2.0"
|
||||
os_info = "3.12.0"
|
||||
portable-pty = "0.9.0"
|
||||
rand = "0.9"
|
||||
regex-lite = "0.1.7"
|
||||
reqwest = { version = "0.12", features = ["json", "stream"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_bytes = "0.11"
|
||||
serde_json = "1"
|
||||
sha1 = "0.10.6"
|
||||
shlex = "1.3.0"
|
||||
@@ -56,7 +54,6 @@ tracing = { version = "0.1.41", features = ["log"] }
|
||||
tree-sitter = "0.25.9"
|
||||
tree-sitter-bash = "0.25.0"
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
whoami = "1.6.1"
|
||||
wildmatch = "2.4.0"
|
||||
|
||||
|
||||
@@ -85,3 +82,6 @@ tempfile = "3"
|
||||
tokio-test = "0.4"
|
||||
walkdir = "2.5.0"
|
||||
wiremock = "0.6"
|
||||
|
||||
[package.metadata.cargo-shear]
|
||||
ignored = ["openssl-sys"]
|
||||
|
||||
@@ -320,6 +320,9 @@ impl ModelClient {
|
||||
if status == StatusCode::INTERNAL_SERVER_ERROR {
|
||||
return Err(CodexErr::InternalServerError);
|
||||
}
|
||||
if status == StatusCode::UNAUTHORIZED {
|
||||
return Err(CodexErr::UnauthorizedError);
|
||||
}
|
||||
|
||||
return Err(CodexErr::RetryLimit(status));
|
||||
}
|
||||
|
||||
@@ -405,7 +405,7 @@ impl Session {
|
||||
let rollout_fut = RolloutRecorder::new(&config, rollout_params);
|
||||
|
||||
let mcp_fut = McpConnectionManager::new(config.mcp_servers.clone());
|
||||
let default_shell_fut = shell::default_user_shell();
|
||||
let default_shell_fut = shell::default_user_shell(conversation_id.0, &config.codex_home);
|
||||
let history_meta_fut = crate::message_history::history_metadata(&config);
|
||||
|
||||
// Join all independent futures.
|
||||
@@ -477,6 +477,7 @@ impl Session {
|
||||
shell_environment_policy: config.shell_environment_policy.clone(),
|
||||
cwd,
|
||||
};
|
||||
|
||||
let sess = Arc::new(Session {
|
||||
conversation_id,
|
||||
tx_event: tx_event.clone(),
|
||||
@@ -1638,7 +1639,14 @@ async fn run_turn(
|
||||
Ok(output) => return Ok(output),
|
||||
Err(CodexErr::Interrupted) => return Err(CodexErr::Interrupted),
|
||||
Err(CodexErr::EnvVar(var)) => return Err(CodexErr::EnvVar(var)),
|
||||
Err(e @ (CodexErr::UsageLimitReached(_) | CodexErr::UsageNotIncluded)) => {
|
||||
Err(
|
||||
e @ (CodexErr::UsageLimitReached(_)
|
||||
| CodexErr::UsageNotIncluded
|
||||
| CodexErr::UnexpectedStatus(_, _)
|
||||
| CodexErr::RetryLimit(_)
|
||||
| CodexErr::UnauthorizedError
|
||||
| CodexErr::InternalServerError),
|
||||
) => {
|
||||
return Err(e);
|
||||
}
|
||||
Err(e) => {
|
||||
@@ -2323,13 +2331,25 @@ pub struct ExecInvokeArgs<'a> {
|
||||
pub stdout_stream: Option<StdoutStream>,
|
||||
}
|
||||
|
||||
fn should_translate_shell_command(
|
||||
shell: &crate::shell::Shell,
|
||||
shell_policy: &ShellEnvironmentPolicy,
|
||||
) -> bool {
|
||||
matches!(shell, crate::shell::Shell::PowerShell(_))
|
||||
|| shell_policy.use_profile
|
||||
|| matches!(
|
||||
shell,
|
||||
crate::shell::Shell::Posix(shell) if shell.shell_snapshot.is_some()
|
||||
)
|
||||
}
|
||||
|
||||
fn maybe_translate_shell_command(
|
||||
params: ExecParams,
|
||||
sess: &Session,
|
||||
turn_context: &TurnContext,
|
||||
) -> ExecParams {
|
||||
let should_translate = matches!(sess.user_shell, crate::shell::Shell::PowerShell(_))
|
||||
|| turn_context.shell_environment_policy.use_profile;
|
||||
let should_translate =
|
||||
should_translate_shell_command(&sess.user_shell, &turn_context.shell_environment_policy);
|
||||
|
||||
if should_translate
|
||||
&& let Some(command) = sess
|
||||
@@ -2946,10 +2966,15 @@ fn convert_call_tool_result_to_function_call_output_payload(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::config_types::ShellEnvironmentPolicyInherit;
|
||||
use mcp_types::ContentBlock;
|
||||
use mcp_types::TextContent;
|
||||
use pretty_assertions::assert_eq;
|
||||
use serde_json::json;
|
||||
use shell::ShellSnapshot;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration as StdDuration;
|
||||
|
||||
fn text_block(s: &str) -> ContentBlock {
|
||||
@@ -2960,6 +2985,48 @@ mod tests {
|
||||
})
|
||||
}
|
||||
|
||||
fn shell_policy_with_profile(use_profile: bool) -> ShellEnvironmentPolicy {
|
||||
ShellEnvironmentPolicy {
|
||||
inherit: ShellEnvironmentPolicyInherit::All,
|
||||
ignore_default_excludes: false,
|
||||
exclude: Vec::new(),
|
||||
r#set: HashMap::new(),
|
||||
include_only: Vec::new(),
|
||||
use_profile,
|
||||
}
|
||||
}
|
||||
|
||||
fn zsh_shell(shell_snapshot: Option<Arc<ShellSnapshot>>) -> shell::Shell {
|
||||
shell::Shell::Posix(shell::PosixShell {
|
||||
shell_path: "/bin/zsh".to_string(),
|
||||
rc_path: "/Users/example/.zshrc".to_string(),
|
||||
shell_snapshot,
|
||||
})
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn translates_commands_when_shell_policy_requests_profile() {
|
||||
let policy = shell_policy_with_profile(true);
|
||||
let shell = zsh_shell(None);
|
||||
assert!(should_translate_shell_command(&shell, &policy));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn translates_commands_for_zsh_with_snapshot() {
|
||||
let policy = shell_policy_with_profile(false);
|
||||
let shell = zsh_shell(Some(Arc::new(ShellSnapshot::new(PathBuf::from(
|
||||
"/tmp/snapshot",
|
||||
)))));
|
||||
assert!(should_translate_shell_command(&shell, &policy));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bypasses_translation_for_zsh_without_snapshot_or_profile() {
|
||||
let policy = shell_policy_with_profile(false);
|
||||
let shell = zsh_shell(None);
|
||||
assert!(!should_translate_shell_command(&shell, &policy));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn prefers_structured_content_when_present() {
|
||||
let ctr = CallToolResult {
|
||||
|
||||
@@ -83,6 +83,9 @@ pub enum CodexErr {
|
||||
#[error("We're currently experiencing high demand, which may cause temporary errors.")]
|
||||
InternalServerError,
|
||||
|
||||
#[error("The API key is invalid or has expired. Please check your API key and try again.")]
|
||||
UnauthorizedError,
|
||||
|
||||
/// Retry limit exceeded.
|
||||
#[error("exceeded retry limit, last status: {0}")]
|
||||
RetryLimit(StatusCode),
|
||||
|
||||
@@ -69,3 +69,8 @@
|
||||
; Added on top of Chrome profile
|
||||
; Needed for python multiprocessing on MacOS for the SemLock
|
||||
(allow ipc-posix-sem)
|
||||
|
||||
; needed to look up user info, see https://crbug.com/792228
|
||||
(allow mach-lookup
|
||||
(global-name "com.apple.system.opendirectoryd.libinfo")
|
||||
)
|
||||
|
||||
@@ -1,18 +1,36 @@
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use shlex;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tracing::trace;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
pub struct ZshShell {
|
||||
shell_path: String,
|
||||
zshrc_path: String,
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
/// This structure cannot derive Clone or this will break the Drop implementation.
|
||||
pub struct ShellSnapshot {
|
||||
pub(crate) path: PathBuf,
|
||||
}
|
||||
|
||||
impl ShellSnapshot {
|
||||
pub fn new(path: PathBuf) -> Self {
|
||||
Self { path }
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for ShellSnapshot {
|
||||
fn drop(&mut self) {
|
||||
delete_shell_snapshot(&self.path);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
pub struct BashShell {
|
||||
shell_path: String,
|
||||
bashrc_path: String,
|
||||
pub struct PosixShell {
|
||||
pub(crate) shell_path: String,
|
||||
pub(crate) rc_path: String,
|
||||
#[serde(skip_serializing, skip_deserializing)]
|
||||
pub(crate) shell_snapshot: Option<Arc<ShellSnapshot>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
@@ -23,8 +41,7 @@ pub struct PowerShellConfig {
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)]
|
||||
pub enum Shell {
|
||||
Zsh(ZshShell),
|
||||
Bash(BashShell),
|
||||
Posix(PosixShell),
|
||||
PowerShell(PowerShellConfig),
|
||||
Unknown,
|
||||
}
|
||||
@@ -32,11 +49,27 @@ pub enum Shell {
|
||||
impl Shell {
|
||||
pub fn format_default_shell_invocation(&self, command: Vec<String>) -> Option<Vec<String>> {
|
||||
match self {
|
||||
Shell::Zsh(zsh) => {
|
||||
format_shell_invocation_with_rc(&command, &zsh.shell_path, &zsh.zshrc_path)
|
||||
}
|
||||
Shell::Bash(bash) => {
|
||||
format_shell_invocation_with_rc(&command, &bash.shell_path, &bash.bashrc_path)
|
||||
Shell::Posix(shell) => {
|
||||
let joined = strip_bash_lc(&command)
|
||||
.or_else(|| shlex::try_join(command.iter().map(|s| s.as_str())).ok())?;
|
||||
|
||||
let mut source_path = Path::new(&shell.rc_path);
|
||||
|
||||
let session_cmd = if let Some(shell_snapshot) = &shell.shell_snapshot
|
||||
&& shell_snapshot.path.exists()
|
||||
{
|
||||
source_path = shell_snapshot.path.as_path();
|
||||
"-c".to_string()
|
||||
} else {
|
||||
"-lc".to_string()
|
||||
};
|
||||
|
||||
let source_path_str = source_path.to_string_lossy().to_string();
|
||||
let quoted_source_path = shlex::try_quote(&source_path_str).ok()?;
|
||||
let rc_command =
|
||||
format!("[ -f {quoted_source_path} ] && . {quoted_source_path}; ({joined})");
|
||||
|
||||
Some(vec![shell.shell_path.clone(), session_cmd, rc_command])
|
||||
}
|
||||
Shell::PowerShell(ps) => {
|
||||
// If model generated a bash command, prefer a detected bash fallback
|
||||
@@ -89,33 +122,20 @@ impl Shell {
|
||||
|
||||
pub fn name(&self) -> Option<String> {
|
||||
match self {
|
||||
Shell::Zsh(zsh) => std::path::Path::new(&zsh.shell_path)
|
||||
.file_name()
|
||||
.map(|s| s.to_string_lossy().to_string()),
|
||||
Shell::Bash(bash) => std::path::Path::new(&bash.shell_path)
|
||||
Shell::Posix(shell) => Path::new(&shell.shell_path)
|
||||
.file_name()
|
||||
.map(|s| s.to_string_lossy().to_string()),
|
||||
Shell::PowerShell(ps) => Some(ps.exe.clone()),
|
||||
Shell::Unknown => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn format_shell_invocation_with_rc(
|
||||
command: &Vec<String>,
|
||||
shell_path: &str,
|
||||
rc_path: &str,
|
||||
) -> Option<Vec<String>> {
|
||||
let joined = strip_bash_lc(command)
|
||||
.or_else(|| shlex::try_join(command.iter().map(|s| s.as_str())).ok())?;
|
||||
|
||||
let rc_command = if std::path::Path::new(rc_path).exists() {
|
||||
format!("source {rc_path} && ({joined})")
|
||||
} else {
|
||||
joined
|
||||
};
|
||||
|
||||
Some(vec![shell_path.to_string(), "-lc".to_string(), rc_command])
|
||||
pub fn get_snapshot(&self) -> Option<Arc<ShellSnapshot>> {
|
||||
match self {
|
||||
Shell::Posix(shell) => shell.shell_snapshot.clone(),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn strip_bash_lc(command: &Vec<String>) -> Option<String> {
|
||||
@@ -132,7 +152,7 @@ fn strip_bash_lc(command: &Vec<String>) -> Option<String> {
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn detect_default_user_shell() -> Shell {
|
||||
async fn detect_default_user_shell(session_id: Uuid, codex_home: &Path) -> Shell {
|
||||
use libc::getpwuid;
|
||||
use libc::getuid;
|
||||
use std::ffi::CStr;
|
||||
@@ -147,31 +167,45 @@ fn detect_default_user_shell() -> Shell {
|
||||
.into_owned();
|
||||
let home_path = CStr::from_ptr((*pw).pw_dir).to_string_lossy().into_owned();
|
||||
|
||||
if shell_path.ends_with("/zsh") {
|
||||
return Shell::Zsh(ZshShell {
|
||||
shell_path,
|
||||
zshrc_path: format!("{home_path}/.zshrc"),
|
||||
});
|
||||
}
|
||||
let rc_path = if shell_path.ends_with("/zsh") {
|
||||
format!("{home_path}/.zshrc")
|
||||
} else if shell_path.ends_with("/bash") {
|
||||
format!("{home_path}/.bashrc")
|
||||
} else {
|
||||
return Shell::Unknown;
|
||||
};
|
||||
|
||||
if shell_path.ends_with("/bash") {
|
||||
return Shell::Bash(BashShell {
|
||||
shell_path,
|
||||
bashrc_path: format!("{home_path}/.bashrc"),
|
||||
});
|
||||
let snapshot_path = snapshots::ensure_posix_snapshot(
|
||||
&shell_path,
|
||||
&rc_path,
|
||||
Path::new(&home_path),
|
||||
codex_home,
|
||||
session_id,
|
||||
)
|
||||
.await;
|
||||
if snapshot_path.is_none() {
|
||||
trace!("failed to prepare posix snapshot; using live profile");
|
||||
}
|
||||
let shell_snapshot =
|
||||
snapshot_path.map(|snapshot| Arc::new(ShellSnapshot::new(snapshot)));
|
||||
|
||||
return Shell::Posix(PosixShell {
|
||||
shell_path,
|
||||
rc_path,
|
||||
shell_snapshot,
|
||||
});
|
||||
}
|
||||
}
|
||||
Shell::Unknown
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
pub async fn default_user_shell() -> Shell {
|
||||
detect_default_user_shell()
|
||||
pub async fn default_user_shell(session_id: Uuid, codex_home: &Path) -> Shell {
|
||||
detect_default_user_shell(session_id, codex_home).await
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
pub async fn default_user_shell() -> Shell {
|
||||
pub async fn default_user_shell(_session_id: Uuid, _codex_home: &Path) -> Shell {
|
||||
use tokio::process::Command;
|
||||
|
||||
// Prefer PowerShell 7+ (`pwsh`) if available, otherwise fall back to Windows PowerShell.
|
||||
@@ -211,42 +245,158 @@ pub async fn default_user_shell() -> Shell {
|
||||
}
|
||||
|
||||
#[cfg(all(not(target_os = "windows"), not(unix)))]
|
||||
pub async fn default_user_shell() -> Shell {
|
||||
pub async fn default_user_shell(_session_id: Uuid, _codex_home: &Path) -> Shell {
|
||||
Shell::Unknown
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
mod snapshots {
|
||||
use super::*;
|
||||
|
||||
fn zsh_profile_paths(home: &Path) -> Vec<PathBuf> {
|
||||
[".zshenv", ".zprofile", ".zshrc", ".zlogin"]
|
||||
.into_iter()
|
||||
.map(|name| home.join(name))
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn posix_profile_source_script(home: &Path) -> String {
|
||||
zsh_profile_paths(home)
|
||||
.into_iter()
|
||||
.map(|profile| {
|
||||
let profile_string = profile.to_string_lossy().into_owned();
|
||||
let quoted = shlex::try_quote(&profile_string)
|
||||
.map(|cow| cow.into_owned())
|
||||
.unwrap_or(profile_string.clone());
|
||||
|
||||
format!("[ -f {quoted} ] && . {quoted}")
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("; ")
|
||||
}
|
||||
|
||||
pub(crate) async fn ensure_posix_snapshot(
|
||||
shell_path: &str,
|
||||
rc_path: &str,
|
||||
home: &Path,
|
||||
codex_home: &Path,
|
||||
session_id: Uuid,
|
||||
) -> Option<PathBuf> {
|
||||
let snapshot_path = codex_home.join(format!("shell_snapshots/snapshot_{session_id}.zsh"));
|
||||
|
||||
// Check if an update in the profile requires to re-generate the snapshot.
|
||||
let snapshot_is_stale = async {
|
||||
let snapshot_metadata = tokio::fs::metadata(&snapshot_path).await.ok()?;
|
||||
let snapshot_modified = snapshot_metadata.modified().ok()?;
|
||||
|
||||
for profile in zsh_profile_paths(home) {
|
||||
let Ok(profile_metadata) = tokio::fs::metadata(&profile).await else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let Ok(profile_modified) = profile_metadata.modified() else {
|
||||
return Some(true);
|
||||
};
|
||||
|
||||
if profile_modified > snapshot_modified {
|
||||
return Some(true);
|
||||
}
|
||||
}
|
||||
|
||||
Some(false)
|
||||
}
|
||||
.await
|
||||
.unwrap_or(true);
|
||||
|
||||
if !snapshot_is_stale {
|
||||
return Some(snapshot_path);
|
||||
}
|
||||
|
||||
match regenerate_posix_snapshot(shell_path, rc_path, home, &snapshot_path).await {
|
||||
Ok(()) => Some(snapshot_path),
|
||||
Err(err) => {
|
||||
tracing::warn!("failed to generate posix snapshot: {err}");
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn regenerate_posix_snapshot(
|
||||
shell_path: &str,
|
||||
rc_path: &str,
|
||||
home: &Path,
|
||||
snapshot_path: &Path,
|
||||
) -> std::io::Result<()> {
|
||||
// Use `emulate -L sh` instead of `set -o posix` so we work on zsh builds
|
||||
// that disable that option. Guard `alias -p` with `|| true` so the script
|
||||
// keeps a zero exit status even if aliases are disabled.
|
||||
let mut capture_script = String::new();
|
||||
let profile_sources = posix_profile_source_script(home);
|
||||
if !profile_sources.is_empty() {
|
||||
capture_script.push_str(&format!("{profile_sources}; "));
|
||||
}
|
||||
|
||||
let zshrc = home.join(rc_path);
|
||||
|
||||
capture_script.push_str(
|
||||
&format!(". {}; setopt posixbuiltins; export -p; {{ alias | sed 's/^/alias /'; }} 2>/dev/null || true", zshrc.display()),
|
||||
);
|
||||
let output = tokio::process::Command::new(shell_path)
|
||||
.arg("-lc")
|
||||
.arg(capture_script)
|
||||
.env("HOME", home)
|
||||
.output()
|
||||
.await?;
|
||||
|
||||
if !output.status.success() {
|
||||
return Err(std::io::Error::other(format!(
|
||||
"snapshot capture exited with status {}",
|
||||
output.status
|
||||
)));
|
||||
}
|
||||
|
||||
let mut contents = String::from("# Generated by Codex. Do not edit.\n");
|
||||
|
||||
contents.push_str(&String::from_utf8_lossy(&output.stdout));
|
||||
contents.push('\n');
|
||||
|
||||
if let Some(parent) = snapshot_path.parent() {
|
||||
tokio::fs::create_dir_all(parent).await?;
|
||||
}
|
||||
|
||||
let tmp_path = snapshot_path.with_extension("tmp");
|
||||
tokio::fs::write(&tmp_path, contents).await?;
|
||||
|
||||
// Restrict the snapshot to user read/write so that environment variables or aliases
|
||||
// that may contain secrets are not exposed to other users on the system.
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
let permissions = std::fs::Permissions::from_mode(0o600);
|
||||
tokio::fs::set_permissions(&tmp_path, permissions).await?;
|
||||
|
||||
tokio::fs::rename(&tmp_path, snapshot_path).await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn delete_shell_snapshot(path: &Path) {
|
||||
if let Err(err) = std::fs::remove_file(path) {
|
||||
trace!("failed to delete shell snapshot {path:?}: {err}");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[cfg(unix)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::process::Command;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_current_shell_detects_zsh() {
|
||||
let shell = Command::new("sh")
|
||||
.arg("-c")
|
||||
.arg("echo $SHELL")
|
||||
.output()
|
||||
.unwrap();
|
||||
|
||||
let home = std::env::var("HOME").unwrap();
|
||||
let shell_path = String::from_utf8_lossy(&shell.stdout).trim().to_string();
|
||||
if shell_path.ends_with("/zsh") {
|
||||
assert_eq!(
|
||||
default_user_shell().await,
|
||||
Shell::Zsh(ZshShell {
|
||||
shell_path: shell_path.to_string(),
|
||||
zshrc_path: format!("{home}/.zshrc",),
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_run_with_profile_zshrc_not_exists() {
|
||||
let shell = Shell::Zsh(ZshShell {
|
||||
let shell = Shell::Posix(PosixShell {
|
||||
shell_path: "/bin/zsh".to_string(),
|
||||
zshrc_path: "/does/not/exist/.zshrc".to_string(),
|
||||
rc_path: "/does/not/exist/.zshrc".to_string(),
|
||||
shell_snapshot: None,
|
||||
});
|
||||
let actual_cmd = shell.format_default_shell_invocation(vec!["myecho".to_string()]);
|
||||
assert_eq!(
|
||||
@@ -254,24 +404,7 @@ mod tests {
|
||||
Some(vec![
|
||||
"/bin/zsh".to_string(),
|
||||
"-lc".to_string(),
|
||||
"myecho".to_string()
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_run_with_profile_bashrc_not_exists() {
|
||||
let shell = Shell::Bash(BashShell {
|
||||
shell_path: "/bin/bash".to_string(),
|
||||
bashrc_path: "/does/not/exist/.bashrc".to_string(),
|
||||
});
|
||||
let actual_cmd = shell.format_default_shell_invocation(vec!["myecho".to_string()]);
|
||||
assert_eq!(
|
||||
actual_cmd,
|
||||
Some(vec![
|
||||
"/bin/bash".to_string(),
|
||||
"-lc".to_string(),
|
||||
"myecho".to_string()
|
||||
"[ -f /does/not/exist/.zshrc ] && . /does/not/exist/.zshrc; (myecho)".to_string(),
|
||||
])
|
||||
);
|
||||
}
|
||||
@@ -283,7 +416,11 @@ mod tests {
|
||||
let cases = vec![
|
||||
(
|
||||
vec!["myecho"],
|
||||
vec![shell_path, "-lc", "source BASHRC_PATH && (myecho)"],
|
||||
vec![
|
||||
shell_path,
|
||||
"-lc",
|
||||
"[ -f BASHRC_PATH ] && . BASHRC_PATH; (myecho)",
|
||||
],
|
||||
Some("It works!\n"),
|
||||
),
|
||||
(
|
||||
@@ -291,7 +428,7 @@ mod tests {
|
||||
vec![
|
||||
shell_path,
|
||||
"-lc",
|
||||
"source BASHRC_PATH && (echo 'single' \"double\")",
|
||||
"[ -f BASHRC_PATH ] && . BASHRC_PATH; (echo 'single' \"double\")",
|
||||
],
|
||||
Some("single double\n"),
|
||||
),
|
||||
@@ -317,9 +454,10 @@ mod tests {
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let shell = Shell::Bash(BashShell {
|
||||
let shell = Shell::Posix(PosixShell {
|
||||
shell_path: shell_path.to_string(),
|
||||
bashrc_path: bashrc_path.to_str().unwrap().to_string(),
|
||||
rc_path: bashrc_path.to_str().unwrap().to_string(),
|
||||
shell_snapshot: None,
|
||||
});
|
||||
|
||||
let actual_cmd = shell
|
||||
@@ -369,6 +507,82 @@ mod tests {
|
||||
#[cfg(target_os = "macos")]
|
||||
mod macos_tests {
|
||||
use super::*;
|
||||
use crate::shell::snapshots::ensure_posix_snapshot;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_snapshot_generation_uses_session_id_and_cleanup() {
|
||||
let shell_path = "/bin/zsh";
|
||||
|
||||
let temp_home = tempfile::tempdir().unwrap();
|
||||
let codex_home = tempfile::tempdir().unwrap();
|
||||
std::fs::write(
|
||||
temp_home.path().join(".zshrc"),
|
||||
"export SNAPSHOT_TEST_VAR=1\nalias snapshot_test_alias='echo hi'\n",
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let session_id = Uuid::new_v4();
|
||||
let snapshot_path = ensure_posix_snapshot(
|
||||
shell_path,
|
||||
".zshrc",
|
||||
temp_home.path(),
|
||||
codex_home.path(),
|
||||
session_id,
|
||||
)
|
||||
.await
|
||||
.expect("snapshot path");
|
||||
|
||||
let filename = snapshot_path
|
||||
.file_name()
|
||||
.unwrap()
|
||||
.to_string_lossy()
|
||||
.to_string();
|
||||
assert!(filename.contains(&session_id.to_string()));
|
||||
assert!(snapshot_path.exists());
|
||||
|
||||
let snapshot_path_second = ensure_posix_snapshot(
|
||||
shell_path,
|
||||
".zshrc",
|
||||
temp_home.path(),
|
||||
codex_home.path(),
|
||||
session_id,
|
||||
)
|
||||
.await
|
||||
.expect("snapshot path");
|
||||
assert_eq!(snapshot_path, snapshot_path_second);
|
||||
|
||||
let contents = std::fs::read_to_string(&snapshot_path).unwrap();
|
||||
assert!(contents.contains("alias snapshot_test_alias='echo hi'"));
|
||||
assert!(contents.contains("SNAPSHOT_TEST_VAR=1"));
|
||||
|
||||
delete_shell_snapshot(&snapshot_path);
|
||||
assert!(!snapshot_path.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn format_default_shell_invocation_prefers_snapshot_when_available() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let snapshot_path = temp_dir.path().join("snapshot.zsh");
|
||||
std::fs::write(&snapshot_path, "export SNAPSHOT_READY=1").unwrap();
|
||||
|
||||
let shell = Shell::Posix(PosixShell {
|
||||
shell_path: "/bin/zsh".to_string(),
|
||||
rc_path: {
|
||||
let path = temp_dir.path().join(".zshrc");
|
||||
std::fs::write(&path, "# test zshrc").unwrap();
|
||||
path.to_string_lossy().to_string()
|
||||
},
|
||||
shell_snapshot: Some(Arc::new(ShellSnapshot::new(snapshot_path.clone()))),
|
||||
});
|
||||
|
||||
let invocation = shell.format_default_shell_invocation(vec!["echo".to_string()]);
|
||||
let expected_command = vec!["/bin/zsh".to_string(), "-c".to_string(), {
|
||||
let snapshot_path = snapshot_path.to_string_lossy();
|
||||
format!("[ -f {snapshot_path} ] && . {snapshot_path}; (echo)")
|
||||
}];
|
||||
|
||||
assert_eq!(invocation, Some(expected_command));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_run_with_profile_escaping_and_execution() {
|
||||
@@ -377,12 +591,20 @@ mod macos_tests {
|
||||
let cases = vec![
|
||||
(
|
||||
vec!["myecho"],
|
||||
vec![shell_path, "-lc", "source ZSHRC_PATH && (myecho)"],
|
||||
vec![
|
||||
shell_path,
|
||||
"-lc",
|
||||
"[ -f ZSHRC_PATH ] && . ZSHRC_PATH; (myecho)",
|
||||
],
|
||||
Some("It works!\n"),
|
||||
),
|
||||
(
|
||||
vec!["myecho"],
|
||||
vec![shell_path, "-lc", "source ZSHRC_PATH && (myecho)"],
|
||||
vec![
|
||||
shell_path,
|
||||
"-lc",
|
||||
"[ -f ZSHRC_PATH ] && . ZSHRC_PATH; (myecho)",
|
||||
],
|
||||
Some("It works!\n"),
|
||||
),
|
||||
(
|
||||
@@ -390,7 +612,7 @@ mod macos_tests {
|
||||
vec![
|
||||
shell_path,
|
||||
"-lc",
|
||||
"source ZSHRC_PATH && (bash -c \"echo 'single' \\\"double\\\"\")",
|
||||
"[ -f ZSHRC_PATH ] && . ZSHRC_PATH; (bash -c \"echo 'single' \\\"double\\\"\")",
|
||||
],
|
||||
Some("single double\n"),
|
||||
),
|
||||
@@ -399,7 +621,7 @@ mod macos_tests {
|
||||
vec![
|
||||
shell_path,
|
||||
"-lc",
|
||||
"source ZSHRC_PATH && (echo 'single' \"double\")",
|
||||
"[ -f ZSHRC_PATH ] && . ZSHRC_PATH; (echo 'single' \"double\")",
|
||||
],
|
||||
Some("single double\n"),
|
||||
),
|
||||
@@ -426,9 +648,10 @@ mod macos_tests {
|
||||
"#,
|
||||
)
|
||||
.unwrap();
|
||||
let shell = Shell::Zsh(ZshShell {
|
||||
let shell = Shell::Posix(PosixShell {
|
||||
shell_path: shell_path.to_string(),
|
||||
zshrc_path: zshrc_path.to_str().unwrap().to_string(),
|
||||
rc_path: zshrc_path.to_str().unwrap().to_string(),
|
||||
shell_snapshot: None,
|
||||
});
|
||||
|
||||
let actual_cmd = shell
|
||||
|
||||
@@ -17,6 +17,7 @@ use core_test_support::load_default_config_for_test;
|
||||
use core_test_support::load_sse_fixture_with_id;
|
||||
use core_test_support::wait_for_event;
|
||||
use tempfile::TempDir;
|
||||
use uuid::Uuid;
|
||||
use wiremock::Mock;
|
||||
use wiremock::MockServer;
|
||||
use wiremock::ResponseTemplate;
|
||||
@@ -269,7 +270,7 @@ async fn prefixes_context_and_instructions_once_and_consistently_across_requests
|
||||
let requests = server.received_requests().await.unwrap();
|
||||
assert_eq!(requests.len(), 2, "expected two POST requests");
|
||||
|
||||
let shell = default_user_shell().await;
|
||||
let shell = default_user_shell(Uuid::new_v4(), codex_home.path()).await;
|
||||
|
||||
let expected_env_text = format!(
|
||||
r#"<environment_context>
|
||||
|
||||
@@ -159,6 +159,41 @@ async fn read_only_forbids_all_writes() {
|
||||
.await;
|
||||
}
|
||||
|
||||
/// Verify that user lookups via `pwd.getpwuid(os.getuid())` work under the
|
||||
/// seatbelt sandbox. Prior to allowing the necessary mach‑lookup for
|
||||
/// OpenDirectory libinfo, this would fail with `KeyError: getpwuid(): uid not found`.
|
||||
#[tokio::test]
|
||||
async fn python_getpwuid_works_under_seatbelt() {
|
||||
if std::env::var(CODEX_SANDBOX_ENV_VAR) == Ok("seatbelt".to_string()) {
|
||||
eprintln!("{CODEX_SANDBOX_ENV_VAR} is set to 'seatbelt', skipping test.");
|
||||
return;
|
||||
}
|
||||
|
||||
// ReadOnly is sufficient here since we are only exercising user lookup.
|
||||
let policy = SandboxPolicy::ReadOnly;
|
||||
|
||||
let mut child = spawn_command_under_seatbelt(
|
||||
vec![
|
||||
"python3".to_string(),
|
||||
"-c".to_string(),
|
||||
// Print the passwd struct; success implies lookup worked.
|
||||
"import pwd, os; print(pwd.getpwuid(os.getuid()))".to_string(),
|
||||
],
|
||||
&policy,
|
||||
std::env::current_dir().expect("should be able to get current dir"),
|
||||
StdioPolicy::RedirectForShellTool,
|
||||
HashMap::new(),
|
||||
)
|
||||
.await
|
||||
.expect("should be able to spawn python under seatbelt");
|
||||
|
||||
let status = child
|
||||
.wait()
|
||||
.await
|
||||
.expect("should be able to wait for child process");
|
||||
assert!(status.success(), "python exited with {status:?}");
|
||||
}
|
||||
|
||||
#[expect(clippy::expect_used)]
|
||||
fn create_test_scenario(tmp: &TempDir) -> TestScenario {
|
||||
let repo_parent = tmp.path().to_path_buf();
|
||||
|
||||
88
codex-rs/core/tests/unexpected_status_no_retry.rs
Normal file
88
codex-rs/core/tests/unexpected_status_no_retry.rs
Normal file
@@ -0,0 +1,88 @@
|
||||
use std::time::Duration;
|
||||
|
||||
use codex_core::ConversationManager;
|
||||
use codex_core::ModelProviderInfo;
|
||||
use codex_core::WireApi;
|
||||
use codex_core::protocol::EventMsg;
|
||||
use codex_core::protocol::InputItem;
|
||||
use codex_core::protocol::Op;
|
||||
use codex_core::spawn::CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR;
|
||||
use codex_login::CodexAuth;
|
||||
use core_test_support::load_default_config_for_test;
|
||||
use core_test_support::wait_for_event_with_timeout;
|
||||
use serde_json::json;
|
||||
use tempfile::TempDir;
|
||||
use wiremock::Mock;
|
||||
use wiremock::MockServer;
|
||||
use wiremock::ResponseTemplate;
|
||||
use wiremock::matchers::method;
|
||||
use wiremock::matchers::path;
|
||||
|
||||
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
|
||||
async fn fails_fast_on_unexpected_status() {
|
||||
if std::env::var(CODEX_SANDBOX_NETWORK_DISABLED_ENV_VAR).is_ok() {
|
||||
println!(
|
||||
"Skipping test because it cannot execute when network is disabled in a Codex sandbox."
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let server = MockServer::start().await;
|
||||
|
||||
let err_body = json!({
|
||||
"error": {"message": "bad request"}
|
||||
});
|
||||
let tmpl = ResponseTemplate::new(400)
|
||||
.insert_header("content-type", "application/json")
|
||||
.set_body_string(err_body.to_string());
|
||||
|
||||
Mock::given(method("POST"))
|
||||
.and(path("/v1/responses"))
|
||||
.respond_with(tmpl)
|
||||
.expect(1)
|
||||
.mount(&server)
|
||||
.await;
|
||||
|
||||
let provider = ModelProviderInfo {
|
||||
name: "openai".into(),
|
||||
base_url: Some(format!("{}/v1", server.uri())),
|
||||
env_key: Some("PATH".into()),
|
||||
env_key_instructions: None,
|
||||
wire_api: WireApi::Responses,
|
||||
query_params: None,
|
||||
http_headers: None,
|
||||
env_http_headers: None,
|
||||
request_max_retries: Some(0),
|
||||
stream_max_retries: Some(3),
|
||||
stream_idle_timeout_ms: Some(2000),
|
||||
requires_openai_auth: false,
|
||||
};
|
||||
|
||||
let home = TempDir::new().unwrap();
|
||||
let mut config = load_default_config_for_test(&home);
|
||||
config.model_provider = provider;
|
||||
|
||||
let conversation_manager =
|
||||
ConversationManager::with_auth(CodexAuth::from_api_key("Test API Key"));
|
||||
let codex = conversation_manager
|
||||
.new_conversation(config)
|
||||
.await
|
||||
.unwrap()
|
||||
.conversation;
|
||||
|
||||
codex
|
||||
.submit(Op::UserInput {
|
||||
items: vec![InputItem::Text {
|
||||
text: "hello".into(),
|
||||
}],
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
wait_for_event_with_timeout(
|
||||
&codex,
|
||||
|ev| matches!(ev, EventMsg::Error(_)),
|
||||
Duration::from_secs(5),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
@@ -25,7 +25,6 @@ codex-common = { path = "../common", features = [
|
||||
"sandbox_summary",
|
||||
] }
|
||||
codex-core = { path = "../core" }
|
||||
codex-login = { path = "../login" }
|
||||
codex-ollama = { path = "../ollama" }
|
||||
codex-protocol = { path = "../protocol" }
|
||||
owo-colors = "4.2.0"
|
||||
|
||||
@@ -15,9 +15,7 @@ path = "src/lib.rs"
|
||||
workspace = true
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
anyhow = "1"
|
||||
clap = { version = "4", features = ["derive"] }
|
||||
codex-common = { path = "../common", features = ["cli"] }
|
||||
codex-core = { path = "../core" }
|
||||
landlock = "0.4.1"
|
||||
libc = "0.2.175"
|
||||
|
||||
@@ -17,7 +17,6 @@ serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
sha2 = "0.10"
|
||||
tempfile = "3"
|
||||
thiserror = "2.0.16"
|
||||
tiny_http = "0.12"
|
||||
tokio = { version = "1", features = [
|
||||
"io-std",
|
||||
@@ -31,5 +30,4 @@ urlencoding = "2.1"
|
||||
webbrowser = "1.0"
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1.4.1"
|
||||
tempfile = "3"
|
||||
|
||||
@@ -26,7 +26,6 @@ schemars = "0.8.22"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
shlex = "1.3.0"
|
||||
strum_macros = "0.27.2"
|
||||
tokio = { version = "1", features = [
|
||||
"io-std",
|
||||
"macros",
|
||||
@@ -44,5 +43,4 @@ assert_cmd = "2"
|
||||
mcp_test_support = { path = "tests/common" }
|
||||
pretty_assertions = "1.4.1"
|
||||
tempfile = "3"
|
||||
tokio-test = "0.4"
|
||||
wiremock = "0.6"
|
||||
|
||||
@@ -9,20 +9,16 @@ path = "lib.rs"
|
||||
[dependencies]
|
||||
anyhow = "1"
|
||||
assert_cmd = "2"
|
||||
codex-core = { path = "../../../core" }
|
||||
codex-mcp-server = { path = "../.." }
|
||||
codex-protocol = { path = "../../../protocol" }
|
||||
mcp-types = { path = "../../../mcp-types" }
|
||||
pretty_assertions = "1.4.1"
|
||||
serde = { version = "1" }
|
||||
serde_json = "1"
|
||||
shlex = "1.3.0"
|
||||
tempfile = "3"
|
||||
tokio = { version = "1", features = [
|
||||
"io-std",
|
||||
"macros",
|
||||
"process",
|
||||
"rt-multi-thread",
|
||||
] }
|
||||
uuid = { version = "1", features = ["serde", "v4"] }
|
||||
wiremock = "0.6"
|
||||
|
||||
@@ -24,9 +24,7 @@ tokio = { version = "1", features = [
|
||||
"rt-multi-thread",
|
||||
"signal",
|
||||
] }
|
||||
toml = "0.9.5"
|
||||
tracing = { version = "0.1.41", features = ["log"] }
|
||||
wiremock = "0.6"
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
|
||||
@@ -17,7 +17,6 @@ icu_locale_core = "2.0.0"
|
||||
mcp-types = { path = "../mcp-types" }
|
||||
mime_guess = "2.0.5"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_bytes = "0.11"
|
||||
serde_json = "1"
|
||||
serde_with = { version = "3.14.0", features = ["macros", "base64"] }
|
||||
strum = "0.27.2"
|
||||
@@ -29,3 +28,7 @@ uuid = { version = "1", features = ["serde", "v4"] }
|
||||
|
||||
[dev-dependencies]
|
||||
pretty_assertions = "1.4.1"
|
||||
|
||||
[package.metadata.cargo-shear]
|
||||
# Required because the not imported as strum_macros in non-nightly builds.
|
||||
ignored = ["strum"]
|
||||
|
||||
@@ -59,9 +59,7 @@ ratatui = { version = "0.29.0", features = [
|
||||
"unstable-rendered-line-info",
|
||||
"unstable-widget-ref",
|
||||
] }
|
||||
ratatui-image = "8.0.0"
|
||||
regex-lite = "0.1"
|
||||
reqwest = { version = "0.12", features = ["json"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = { version = "1", features = ["preserve_order"] }
|
||||
shlex = "1.3.0"
|
||||
@@ -81,12 +79,10 @@ tokio-stream = "0.1.17"
|
||||
tracing = { version = "0.1.41", features = ["log"] }
|
||||
tracing-appender = "0.2.3"
|
||||
tracing-subscriber = { version = "0.3.19", features = ["env-filter"] }
|
||||
tui-input = "0.14.0"
|
||||
tui-markdown = "0.3.3"
|
||||
unicode-segmentation = "1.12.0"
|
||||
unicode-width = "0.1"
|
||||
url = "2"
|
||||
uuid = "1"
|
||||
pathdiff = "0.2"
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
|
||||
@@ -6,9 +6,12 @@ Codex supports several mechanisms for setting config values:
|
||||
- Config-specific command-line flags, such as `--model o3` (highest precedence).
|
||||
- A generic `-c`/`--config` flag that takes a `key=value` pair, such as `--config model="o3"`.
|
||||
- The key can contain dots to set a value deeper than the root, e.g. `--config model_providers.openai.wire_api="chat"`.
|
||||
- Values can contain objects, such as `--config shell_environment_policy.include_only=["PATH", "HOME", "USER"]`.
|
||||
- For consistency with `config.toml`, values are in TOML format rather than JSON format, so use `{a = 1, b = 2}` rather than `{"a": 1, "b": 2}`.
|
||||
- If `value` cannot be parsed as a valid TOML value, it is treated as a string value. This means that both `-c model="o3"` and `-c model=o3` are equivalent.
|
||||
- For consistency with `config.toml`, values are a string in TOML format rather than JSON format, so use `key='{a = 1, b = 2}'` rather than `key='{"a": 1, "b": 2}'`.
|
||||
- The quotes around the value are necessary, as without them your shell would split the config argument on spaces, resulting in `codex` receiving `-c key={a` with (invalid) additional arguments `=`, `1,`, `b`, `=`, `2}`.
|
||||
- Values can contain any TOML object, such as `--config shell_environment_policy.include_only='["PATH", "HOME", "USER"]'`.
|
||||
- If `value` cannot be parsed as a valid TOML value, it is treated as a string value. This means that `-c model='"o3"'` and `-c model=o3` are equivalent.
|
||||
- In the first case, the value is the TOML string `"o3"`, while in the second the value is `o3`, which is not valid TOML and therefore treated as the TOML string `"o3"`.
|
||||
- Because quotes are interpreted by one's shell, `-c key="true"` will be correctly interpreted in TOML as `key = true` (a boolean) and not `key = "true"` (a string). If for some reason you needed the string `"true"`, you would need to use `-c key='"true"'` (note the two sets of quotes).
|
||||
- The `$CODEX_HOME/config.toml` configuration file where the `CODEX_HOME` environment value defaults to `~/.codex`. (Note `CODEX_HOME` will also be where logs and other Codex-related information are stored.)
|
||||
|
||||
Both the `--config` flag and the `config.toml` file support the following options:
|
||||
|
||||
Reference in New Issue
Block a user