#![forbid(unsafe_code)]
use harness_codex::{AuthCommand, ConfigPreview, UserConfigTarget, restore_from_receipt};
use serde::Serialize;
use std::env;
use std::path::{Component, Path, PathBuf};
use std::process::ExitCode;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Action {
Preview,
Apply,
Restore,
}
struct Arguments {
action: Action,
config: PathBuf,
port: Option<u16>,
auth_command: Option<PathBuf>,
auth_args: Vec<String>,
confirmation: Option<String>,
preview_digest: Option<String>,
}
#[derive(Serialize)]
struct PreviewOutput<'a> {
status: &'static str,
human_diff: &'a str,
machine_plan: &'a harness_codex::MachinePlan,
apply_required: bool,
auth_command: &'static str,
}
#[derive(Serialize)]
struct StateOutput<'a> {
status: &'static str,
target: &'a Path,
}
fn main() -> ExitCode {
match run() {
Ok(()) => ExitCode::SUCCESS,
Err(message) => {
eprintln!("codex-config-plan: {message}");
ExitCode::from(2)
}
}
}
fn run() -> Result<(), &'static str> {
let arguments = parse_arguments()?;
let target = validate_target(&arguments.config)?;
let target_path = target.path().to_path_buf();
match arguments.action {
Action::Restore => {
if arguments.confirmation.as_deref() != Some("RESTORE_USER_CONFIG") {
return Err("restore requires --confirm RESTORE_USER_CONFIG");
}
restore_from_receipt(target)
.map_err(|_| "restore refused; inspect recovery artifacts")?;
print_json(&StateOutput {
status: "restored",
target: &target_path,
})
}
Action::Preview | Action::Apply => {
let port = arguments.port.ok_or("preview/apply requires --port")?;
let command = arguments
.auth_command
.ok_or("preview/apply requires --auth-command")?;
let auth_command = AuthCommand::new(command, arguments.auth_args)
.map_err(|_| "authentication command was refused")?;
let preview = ConfigPreview::build(target, port, &auth_command)
.map_err(|_| "configuration plan was refused")?;
print_json(&PreviewOutput {
status: "preview",
human_diff: &preview.human_diff,
machine_plan: &preview.machine_plan,
apply_required: true,
auth_command: "[REDACTED]",
})?;
if arguments.action == Action::Preview {
return Ok(());
}
if arguments.confirmation.as_deref() != Some("APPLY_USER_CONFIG") {
return Err("apply requires --confirm APPLY_USER_CONFIG");
}
let preview_digest = arguments
.preview_digest
.as_deref()
.ok_or("apply requires --preview-digest")?;
preview
.confirm_digest(preview_digest)
.map_err(|_| "configuration preview digest was refused")?
.apply()
.map_err(|_| "configuration apply was refused")?;
print_json(&StateOutput {
status: "applied",
target: &target_path,
})
}
}
}
fn print_json(value: &impl Serialize) -> Result<(), &'static str> {
let encoded = serde_json::to_string(value).map_err(|_| "output encoding failed")?;
println!("{encoded}");
Ok(())
}
fn parse_arguments() -> Result<Arguments, &'static str> {
let mut action = Action::Preview;
let mut config = None;
let mut port = None;
let mut auth_command = None;
let mut auth_args = Vec::new();
let mut confirmation = None;
let mut preview_digest = None;
let mut arguments = env::args().skip(1);
while let Some(argument) = arguments.next() {
match argument.as_str() {
"--config" => config = Some(PathBuf::from(next_value(&mut arguments)?)),
"--port" => {
port = Some(
next_value(&mut arguments)?
.parse()
.map_err(|_| "--port must be a valid u16")?,
);
}
"--auth-command" => auth_command = Some(PathBuf::from(next_value(&mut arguments)?)),
"--auth-arg" => auth_args.push(next_value(&mut arguments)?),
"--confirm" => confirmation = Some(next_value(&mut arguments)?),
"--preview-digest" => preview_digest = Some(next_value(&mut arguments)?),
"--apply" if action == Action::Preview => action = Action::Apply,
"--restore" if action == Action::Preview => action = Action::Restore,
"--apply" | "--restore" => return Err("choose only one of --apply or --restore"),
"--help" | "-h" => {
println!(
"codex-config-plan --config ABSOLUTE_CONFIG_TOML [--port PORT --auth-command ABSOLUTE_EXECUTABLE [--auth-arg ARG]...] [--apply --preview-digest SHA256 --confirm APPLY_USER_CONFIG | --restore --confirm RESTORE_USER_CONFIG]"
);
std::process::exit(0);
}
_ => return Err("unknown argument; use --help"),
}
}
Ok(Arguments {
action,
config: config.ok_or("--config is required")?,
port,
auth_command,
auth_args,
confirmation,
preview_digest,
})
}
fn next_value(arguments: &mut impl Iterator<Item = String>) -> Result<String, &'static str> {
arguments.next().ok_or("argument value is missing")
}
fn validate_target(path: &Path) -> Result<UserConfigTarget, &'static str> {
if !path.is_absolute() || path.components().any(|part| part == Component::ParentDir) {
return Err("--config must be an absolute normalized path");
}
if path.file_name().and_then(|name| name.to_str()) != Some("config.toml") {
return Err("configuration file must be named config.toml");
}
let current = env::current_dir().map_err(|_| "current directory is unavailable")?;
let lexical_project_target = current.join(".codex/config.toml");
if path == lexical_project_target
|| (path.starts_with(¤t) && has_codex_config_suffix(path))
{
return Err("project .codex/config.toml targets are prohibited");
}
let config_root = path.parent().ok_or("configuration path has no parent")?;
let target = UserConfigTarget::under_config_root(config_root)
.map_err(|_| "configuration root must be an existing non-symlink directory")?;
let approved_root = env::var_os("CODEX_HOME")
.map(PathBuf::from)
.or_else(|| env::var_os("HOME").map(|home| PathBuf::from(home).join(".codex")))
.ok_or("CODEX_HOME or HOME is required to establish the user config root")?;
let approved_target = UserConfigTarget::under_config_root(&approved_root)
.map_err(|_| "approved user configuration root must exist and must not be a symlink")?;
if target.path() != approved_target.path() {
return Err("configuration target must be contained by the approved user config root");
}
if fs_target_is_symlink(target.path()) {
return Err("configuration file must not be a symbolic link");
}
let canonical_current =
std::fs::canonicalize(¤t).map_err(|_| "current directory is unavailable")?;
let project_target = canonical_current.join(".codex/config.toml");
if target.path() == project_target
|| (target.path().starts_with(&canonical_current) && has_codex_config_suffix(target.path()))
{
return Err("project .codex/config.toml targets are prohibited");
}
let mut ancestor = target.path().parent().and_then(Path::parent);
while let Some(directory) = ancestor {
if target.path() == directory.join(".codex/config.toml")
&& (directory.join(".git").exists()
|| directory.join("Cargo.toml").exists()
|| directory.join("package.json").exists())
{
return Err("project .codex/config.toml targets are prohibited");
}
ancestor = directory.parent();
}
Ok(target)
}
fn fs_target_is_symlink(path: &Path) -> bool {
std::fs::symlink_metadata(path).is_ok_and(|metadata| metadata.file_type().is_symlink())
}
fn has_codex_config_suffix(path: &Path) -> bool {
path.parent()
.and_then(Path::file_name)
.and_then(|name| name.to_str())
== Some(".codex")
}