mirror of
https://github.com/openai/codex.git
synced 2026-04-29 00:55:38 +00:00
feat: split codex-common into smaller utils crates (#11422)
We are removing feature-gated shared crates from the `codex-rs` workspace. `codex-common` grouped several unrelated utilities behind `[features]`, which made dependency boundaries harder to reason about and worked against the ongoing effort to eliminate feature flags from workspace crates. Splitting these utilities into dedicated crates under `utils/` aligns this area with existing workspace structure and keeps each dependency explicit at the crate boundary. ## What changed - Removed `codex-rs/common` (`codex-common`) from workspace members and workspace dependencies. - Added six new utility crates under `codex-rs/utils/`: - `codex-utils-cli` - `codex-utils-elapsed` - `codex-utils-sandbox-summary` - `codex-utils-approval-presets` - `codex-utils-oss` - `codex-utils-fuzzy-match` - Migrated the corresponding modules out of `codex-common` into these crates (with tests), and added matching `BUILD.bazel` targets. - Updated direct consumers to use the new crates instead of `codex-common`: - `codex-rs/cli` - `codex-rs/tui` - `codex-rs/exec` - `codex-rs/app-server` - `codex-rs/mcp-server` - `codex-rs/chatgpt` - `codex-rs/cloud-tasks` - Updated workspace lockfile entries to reflect the new dependency graph and removal of `codex-common`.
This commit is contained in:
37
codex-rs/utils/cli/src/approval_mode_cli_arg.rs
Normal file
37
codex-rs/utils/cli/src/approval_mode_cli_arg.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
//! Standard type to use with the `--approval-mode` CLI option.
|
||||
|
||||
use clap::ValueEnum;
|
||||
|
||||
use codex_core::protocol::AskForApproval;
|
||||
|
||||
#[derive(Clone, Copy, Debug, ValueEnum)]
|
||||
#[value(rename_all = "kebab-case")]
|
||||
pub enum ApprovalModeCliArg {
|
||||
/// Only run "trusted" commands (e.g. ls, cat, sed) without asking for user
|
||||
/// approval. Will escalate to the user if the model proposes a command that
|
||||
/// is not in the "trusted" set.
|
||||
Untrusted,
|
||||
|
||||
/// Run all commands without asking for user approval.
|
||||
/// Only asks for approval if a command fails to execute, in which case it
|
||||
/// will escalate to the user to ask for un-sandboxed execution.
|
||||
OnFailure,
|
||||
|
||||
/// The model decides when to ask the user for approval.
|
||||
OnRequest,
|
||||
|
||||
/// Never ask for user approval
|
||||
/// Execution failures are immediately returned to the model.
|
||||
Never,
|
||||
}
|
||||
|
||||
impl From<ApprovalModeCliArg> for AskForApproval {
|
||||
fn from(value: ApprovalModeCliArg) -> Self {
|
||||
match value {
|
||||
ApprovalModeCliArg::Untrusted => AskForApproval::UnlessTrusted,
|
||||
ApprovalModeCliArg::OnFailure => AskForApproval::OnFailure,
|
||||
ApprovalModeCliArg::OnRequest => AskForApproval::OnRequest,
|
||||
ApprovalModeCliArg::Never => AskForApproval::Never,
|
||||
}
|
||||
}
|
||||
}
|
||||
200
codex-rs/utils/cli/src/config_override.rs
Normal file
200
codex-rs/utils/cli/src/config_override.rs
Normal file
@@ -0,0 +1,200 @@
|
||||
//! Support for `-c key=value` overrides shared across Codex CLI tools.
|
||||
//!
|
||||
//! This module provides a [`CliConfigOverrides`] struct that can be embedded
|
||||
//! into a `clap`-derived CLI struct using `#[clap(flatten)]`. Each occurrence
|
||||
//! of `-c key=value` (or `--config key=value`) will be collected as a raw
|
||||
//! string. Helper methods are provided to convert the raw strings into
|
||||
//! key/value pairs as well as to apply them onto a mutable
|
||||
//! `serde_json::Value` representing the configuration tree.
|
||||
|
||||
use clap::ArgAction;
|
||||
use clap::Parser;
|
||||
use serde::de::Error as SerdeError;
|
||||
use toml::Value;
|
||||
|
||||
/// CLI option that captures arbitrary configuration overrides specified as
|
||||
/// `-c key=value`. It intentionally keeps both halves **unparsed** so that the
|
||||
/// calling code can decide how to interpret the right-hand side.
|
||||
#[derive(Parser, Debug, Default, Clone)]
|
||||
pub struct CliConfigOverrides {
|
||||
/// Override a configuration value that would otherwise be loaded from
|
||||
/// `~/.codex/config.toml`. Use a dotted path (`foo.bar.baz`) to override
|
||||
/// nested values. The `value` portion is parsed as TOML. If it fails to
|
||||
/// parse as TOML, the raw string is used as a literal.
|
||||
///
|
||||
/// Examples:
|
||||
/// - `-c model="o3"`
|
||||
/// - `-c 'sandbox_permissions=["disk-full-read-access"]'`
|
||||
/// - `-c shell_environment_policy.inherit=all`
|
||||
#[arg(
|
||||
short = 'c',
|
||||
long = "config",
|
||||
value_name = "key=value",
|
||||
action = ArgAction::Append,
|
||||
global = true,
|
||||
)]
|
||||
pub raw_overrides: Vec<String>,
|
||||
}
|
||||
|
||||
impl CliConfigOverrides {
|
||||
/// Parse the raw strings captured from the CLI into a list of `(path,
|
||||
/// value)` tuples where `value` is a `serde_json::Value`.
|
||||
pub fn parse_overrides(&self) -> Result<Vec<(String, Value)>, String> {
|
||||
self.raw_overrides
|
||||
.iter()
|
||||
.map(|s| {
|
||||
// Only split on the *first* '=' so values are free to contain
|
||||
// the character.
|
||||
let mut parts = s.splitn(2, '=');
|
||||
let key = match parts.next() {
|
||||
Some(k) => k.trim(),
|
||||
None => return Err("Override missing key".to_string()),
|
||||
};
|
||||
let value_str = parts
|
||||
.next()
|
||||
.ok_or_else(|| format!("Invalid override (missing '='): {s}"))?
|
||||
.trim();
|
||||
|
||||
if key.is_empty() {
|
||||
return Err(format!("Empty key in override: {s}"));
|
||||
}
|
||||
|
||||
// Attempt to parse as TOML. If that fails, treat it as a raw
|
||||
// string. This allows convenient usage such as
|
||||
// `-c model=o3` without the quotes.
|
||||
let value: Value = match parse_toml_value(value_str) {
|
||||
Ok(v) => v,
|
||||
Err(_) => {
|
||||
// Strip leading/trailing quotes if present
|
||||
let trimmed = value_str.trim().trim_matches(|c| c == '"' || c == '\'');
|
||||
Value::String(trimmed.to_string())
|
||||
}
|
||||
};
|
||||
|
||||
Ok((canonicalize_override_key(key), value))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Apply all parsed overrides onto `target`. Intermediate objects will be
|
||||
/// created as necessary. Values located at the destination path will be
|
||||
/// replaced.
|
||||
pub fn apply_on_value(&self, target: &mut Value) -> Result<(), String> {
|
||||
let overrides = self.parse_overrides()?;
|
||||
for (path, value) in overrides {
|
||||
apply_single_override(target, &path, value);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn canonicalize_override_key(key: &str) -> String {
|
||||
if key == "use_linux_sandbox_bwrap" {
|
||||
"features.use_linux_sandbox_bwrap".to_string()
|
||||
} else {
|
||||
key.to_string()
|
||||
}
|
||||
}
|
||||
|
||||
/// Apply a single override onto `root`, creating intermediate objects as
|
||||
/// necessary.
|
||||
fn apply_single_override(root: &mut Value, path: &str, value: Value) {
|
||||
use toml::value::Table;
|
||||
|
||||
let parts: Vec<&str> = path.split('.').collect();
|
||||
let mut current = root;
|
||||
|
||||
for (i, part) in parts.iter().enumerate() {
|
||||
let is_last = i == parts.len() - 1;
|
||||
|
||||
if is_last {
|
||||
match current {
|
||||
Value::Table(tbl) => {
|
||||
tbl.insert((*part).to_string(), value);
|
||||
}
|
||||
_ => {
|
||||
let mut tbl = Table::new();
|
||||
tbl.insert((*part).to_string(), value);
|
||||
*current = Value::Table(tbl);
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Traverse or create intermediate table.
|
||||
match current {
|
||||
Value::Table(tbl) => {
|
||||
current = tbl
|
||||
.entry((*part).to_string())
|
||||
.or_insert_with(|| Value::Table(Table::new()));
|
||||
}
|
||||
_ => {
|
||||
*current = Value::Table(Table::new());
|
||||
if let Value::Table(tbl) = current {
|
||||
current = tbl
|
||||
.entry((*part).to_string())
|
||||
.or_insert_with(|| Value::Table(Table::new()));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_toml_value(raw: &str) -> Result<Value, toml::de::Error> {
|
||||
let wrapped = format!("_x_ = {raw}");
|
||||
let table: toml::Table = toml::from_str(&wrapped)?;
|
||||
table
|
||||
.get("_x_")
|
||||
.cloned()
|
||||
.ok_or_else(|| SerdeError::custom("missing sentinel key"))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn parses_basic_scalar() {
|
||||
let v = parse_toml_value("42").expect("parse");
|
||||
assert_eq!(v.as_integer(), Some(42));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_bool() {
|
||||
let true_literal = parse_toml_value("true").expect("parse");
|
||||
assert_eq!(true_literal.as_bool(), Some(true));
|
||||
|
||||
let false_literal = parse_toml_value("false").expect("parse");
|
||||
assert_eq!(false_literal.as_bool(), Some(false));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn fails_on_unquoted_string() {
|
||||
assert!(parse_toml_value("hello").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_array() {
|
||||
let v = parse_toml_value("[1, 2, 3]").expect("parse");
|
||||
let arr = v.as_array().expect("array");
|
||||
assert_eq!(arr.len(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canonicalizes_use_linux_sandbox_bwrap_alias() {
|
||||
let overrides = CliConfigOverrides {
|
||||
raw_overrides: vec!["use_linux_sandbox_bwrap=true".to_string()],
|
||||
};
|
||||
let parsed = overrides.parse_overrides().expect("parse_overrides");
|
||||
assert_eq!(parsed[0].0.as_str(), "features.use_linux_sandbox_bwrap");
|
||||
assert_eq!(parsed[0].1.as_bool(), Some(true));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parses_inline_table() {
|
||||
let v = parse_toml_value("{a = 1, b = 2}").expect("parse");
|
||||
let tbl = v.as_table().expect("table");
|
||||
assert_eq!(tbl.get("a").unwrap().as_integer(), Some(1));
|
||||
assert_eq!(tbl.get("b").unwrap().as_integer(), Some(2));
|
||||
}
|
||||
}
|
||||
62
codex-rs/utils/cli/src/format_env_display.rs
Normal file
62
codex-rs/utils/cli/src/format_env_display.rs
Normal file
@@ -0,0 +1,62 @@
|
||||
use std::collections::HashMap;
|
||||
|
||||
pub fn format_env_display(env: Option<&HashMap<String, String>>, env_vars: &[String]) -> String {
|
||||
let mut parts: Vec<String> = Vec::new();
|
||||
|
||||
if let Some(map) = env {
|
||||
let mut pairs: Vec<_> = map.iter().collect();
|
||||
pairs.sort_by(|(a, _), (b, _)| a.cmp(b));
|
||||
parts.extend(pairs.into_iter().map(|(key, _)| format!("{key}=*****")));
|
||||
}
|
||||
|
||||
if !env_vars.is_empty() {
|
||||
parts.extend(env_vars.iter().map(|var| format!("{var}=*****")));
|
||||
}
|
||||
|
||||
if parts.is_empty() {
|
||||
"-".to_string()
|
||||
} else {
|
||||
parts.join(", ")
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn returns_dash_when_empty() {
|
||||
assert_eq!(format_env_display(None, &[]), "-");
|
||||
|
||||
let empty_map = HashMap::new();
|
||||
assert_eq!(format_env_display(Some(&empty_map), &[]), "-");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formats_sorted_env_pairs() {
|
||||
let mut env = HashMap::new();
|
||||
env.insert("B".to_string(), "two".to_string());
|
||||
env.insert("A".to_string(), "one".to_string());
|
||||
|
||||
assert_eq!(format_env_display(Some(&env), &[]), "A=*****, B=*****");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn formats_env_vars_with_dollar_prefix() {
|
||||
let vars = vec!["TOKEN".to_string(), "PATH".to_string()];
|
||||
|
||||
assert_eq!(format_env_display(None, &vars), "TOKEN=*****, PATH=*****");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn combines_env_pairs_and_vars() {
|
||||
let mut env = HashMap::new();
|
||||
env.insert("HOME".to_string(), "/tmp".to_string());
|
||||
let vars = vec!["TOKEN".to_string()];
|
||||
|
||||
assert_eq!(
|
||||
format_env_display(Some(&env), &vars),
|
||||
"HOME=*****, TOKEN=*****"
|
||||
);
|
||||
}
|
||||
}
|
||||
8
codex-rs/utils/cli/src/lib.rs
Normal file
8
codex-rs/utils/cli/src/lib.rs
Normal file
@@ -0,0 +1,8 @@
|
||||
mod approval_mode_cli_arg;
|
||||
mod config_override;
|
||||
pub mod format_env_display;
|
||||
mod sandbox_mode_cli_arg;
|
||||
|
||||
pub use approval_mode_cli_arg::ApprovalModeCliArg;
|
||||
pub use config_override::CliConfigOverrides;
|
||||
pub use sandbox_mode_cli_arg::SandboxModeCliArg;
|
||||
47
codex-rs/utils/cli/src/sandbox_mode_cli_arg.rs
Normal file
47
codex-rs/utils/cli/src/sandbox_mode_cli_arg.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
//! Standard type to use with the `--sandbox` (`-s`) CLI option.
|
||||
//!
|
||||
//! This mirrors the variants of [`codex_core::protocol::SandboxPolicy`], but
|
||||
//! without any of the associated data so it can be expressed as a simple flag
|
||||
//! on the command-line. Users that need to tweak the advanced options for
|
||||
//! `workspace-write` can continue to do so via `-c` overrides or their
|
||||
//! `config.toml`.
|
||||
|
||||
use clap::ValueEnum;
|
||||
use codex_protocol::config_types::SandboxMode;
|
||||
|
||||
#[derive(Clone, Copy, Debug, ValueEnum)]
|
||||
#[value(rename_all = "kebab-case")]
|
||||
pub enum SandboxModeCliArg {
|
||||
ReadOnly,
|
||||
WorkspaceWrite,
|
||||
DangerFullAccess,
|
||||
}
|
||||
|
||||
impl From<SandboxModeCliArg> for SandboxMode {
|
||||
fn from(value: SandboxModeCliArg) -> Self {
|
||||
match value {
|
||||
SandboxModeCliArg::ReadOnly => SandboxMode::ReadOnly,
|
||||
SandboxModeCliArg::WorkspaceWrite => SandboxMode::WorkspaceWrite,
|
||||
SandboxModeCliArg::DangerFullAccess => SandboxMode::DangerFullAccess,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
#[test]
|
||||
fn maps_cli_args_to_protocol_modes() {
|
||||
assert_eq!(SandboxMode::ReadOnly, SandboxModeCliArg::ReadOnly.into());
|
||||
assert_eq!(
|
||||
SandboxMode::WorkspaceWrite,
|
||||
SandboxModeCliArg::WorkspaceWrite.into()
|
||||
);
|
||||
assert_eq!(
|
||||
SandboxMode::DangerFullAccess,
|
||||
SandboxModeCliArg::DangerFullAccess.into()
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user