## Summary - extend rollout format to store all session data in JSON - add resume/write helpers for rollouts - track session state after each conversation - support `LoadSession` op to resume a previous rollout - allow starting Codex with an existing session via `experimental_resume` config variable We need a way later for exploring the available sessions in a user friendly way. ## Testing - `cargo test --no-run` *(fails: `cargo: command not found`)* ------ https://chatgpt.com/codex/tasks/task_i_68792a29dd5c832190bf6930d3466fba This video is outdated. you should use `-c experimental_resume:<full path>` instead of `--resume <full path>` https://github.com/user-attachments/assets/7a9975c7-aa04-4f4e-899a-9e87defd947a
175 lines
6.0 KiB
Rust
175 lines
6.0 KiB
Rust
//! 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 JSON. If it fails to
|
|
/// parse as JSON, 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 JSON. 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((key.to_string(), 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(())
|
|
}
|
|
}
|
|
|
|
/// 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(all(test, feature = "cli"))]
|
|
#[allow(clippy::expect_used, clippy::unwrap_used)]
|
|
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 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 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));
|
|
}
|
|
}
|