Files
codex/codex-rs/exec/src/cli.rs
T
Eric Traut 11ca3f7531 Require explicit project trust in non-interactive runs (#60041)
## What changed

Keep untrusted project configuration and MCP servers disabled in exec runs, even with `--sandbox workspace-write`, and warn when project configuration is disabled.

Add `--trust` to `codex exec` and `codex review` to save project trust for the current and future chats, including MCP commands outside the sandbox. The grant persists with `--ephemeral`, refuses explicitly untrusted projects, and cannot be combined with `--ignore-user-config`.

Pass the working directory as a configuration default so embedded server startup does not implicitly grant trust. Preserve the selected `--oss` provider across configuration reloads.

## Testing

Extend CLI parsing and MCP integration coverage for explicit trust, persistence across ephemeral runs, disabled project configuration with write access, and rejection of conflicting or explicitly untrusted settings.

GitOrigin-RevId: b57f4f8829f98d77c5487fa8405c7f29b5d70d0c
2026-11-07 02:39:03 +00:00

348 lines
11 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use clap::Args;
use clap::FromArgMatches;
use clap::Parser;
use clap::ValueEnum;
use codex_protocol::protocol::ThreadSource;
use codex_utils_cli::CliConfigOverrides;
use codex_utils_cli::SharedCliOptions;
use std::path::PathBuf;
#[derive(Parser, Debug)]
#[command(
version,
override_usage = "codex exec [OPTIONS] [PROMPT]\n codex exec [OPTIONS] <COMMAND> [ARGS]"
)]
pub struct Cli {
/// Action to perform. If omitted, runs a new non-interactive session.
#[command(subcommand)]
pub command: Option<Command>,
/// Error out when config.toml contains fields that are not recognized by this version of Codex.
#[arg(long = "strict-config", global = true, default_value_t = false)]
pub strict_config: bool,
#[clap(flatten)]
pub shared: ExecSharedCliOptions,
/// Source classification for newly created or forked threads.
#[arg(long = "thread-source", value_name = "SOURCE", global = true)]
pub thread_source: Option<ThreadSource>,
/// Request an experimental Cyber access program for this turn (OpenAI provider only).
/// Omit to use server defaults. Not supported with review; fork requires a prompt.
#[arg(long, value_enum, value_name = "PROGRAM", global = true)]
pub cyber_access_program: Option<CyberAccessProgramCliArg>,
/// Allow running Codex outside a Git repository.
#[arg(long = "skip-git-repo-check", global = true, default_value_t = false)]
pub skip_git_repo_check: bool,
/// Save trust for this project and future chats, including MCP commands outside the sandbox.
/// Persists even with --ephemeral. Does not override an explicitly untrusted project.
#[arg(long, global = true)]
pub trust: bool,
/// Run without persisting session files to disk.
#[arg(long = "ephemeral", global = true, default_value_t = false)]
pub ephemeral: bool,
/// Do not load `$CODEX_HOME/config.toml`; auth still uses `CODEX_HOME`.
#[arg(long = "ignore-user-config", global = true, default_value_t = false)]
pub ignore_user_config: bool,
/// Do not load user or project execpolicy `.rules` files.
#[arg(long = "ignore-rules", global = true, default_value_t = false)]
pub ignore_rules: bool,
/// Path to a JSON Schema file describing the model's final response shape.
#[arg(long = "output-schema", value_name = "FILE", global = true)]
pub output_schema: Option<PathBuf>,
#[clap(skip)]
pub config_overrides: CliConfigOverrides,
/// Specifies color settings for use in the output.
#[arg(long = "color", value_enum, default_value_t = Color::Auto)]
pub color: Color,
/// Print events to stdout as JSONL.
#[arg(
long = "json",
alias = "experimental-json",
default_value_t = false,
global = true
)]
pub json: bool,
/// Specifies file where the last message from the agent should be written.
#[arg(
long = "output-last-message",
short = 'o',
value_name = "FILE",
global = true
)]
pub last_message_file: Option<PathBuf>,
/// Initial instructions for the agent. If not provided as an argument (or
/// if `-` is used), instructions are read from stdin. If stdin is piped and
/// a prompt is also provided, stdin is appended as a `<stdin>` block.
#[arg(value_name = "PROMPT", value_hint = clap::ValueHint::Other)]
pub prompt: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)]
#[value(rename_all = "snake_case")]
pub enum CyberAccessProgramCliArg {
Standard,
DaybreakBlue,
DaybreakRed,
}
impl From<CyberAccessProgramCliArg> for codex_app_server_protocol::CyberAccessProgram {
fn from(value: CyberAccessProgramCliArg) -> Self {
match value {
CyberAccessProgramCliArg::Standard => Self::Standard,
CyberAccessProgramCliArg::DaybreakBlue => Self::DaybreakBlue,
CyberAccessProgramCliArg::DaybreakRed => Self::DaybreakRed,
}
}
}
impl std::ops::Deref for Cli {
type Target = SharedCliOptions;
fn deref(&self) -> &Self::Target {
&self.shared.0
}
}
impl std::ops::DerefMut for Cli {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.shared.0
}
}
#[derive(Debug, Default)]
pub struct ExecSharedCliOptions(SharedCliOptions);
impl ExecSharedCliOptions {
pub fn into_inner(self) -> SharedCliOptions {
self.0
}
}
impl std::ops::Deref for ExecSharedCliOptions {
type Target = SharedCliOptions;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl std::ops::DerefMut for ExecSharedCliOptions {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Args for ExecSharedCliOptions {
fn augment_args(cmd: clap::Command) -> clap::Command {
mark_exec_global_args(SharedCliOptions::augment_args(cmd))
}
fn augment_args_for_update(cmd: clap::Command) -> clap::Command {
mark_exec_global_args(SharedCliOptions::augment_args_for_update(cmd))
}
}
impl FromArgMatches for ExecSharedCliOptions {
fn from_arg_matches(matches: &clap::ArgMatches) -> Result<Self, clap::Error> {
SharedCliOptions::from_arg_matches(matches).map(Self)
}
fn update_from_arg_matches(&mut self, matches: &clap::ArgMatches) -> Result<(), clap::Error> {
self.0.update_from_arg_matches(matches)
}
}
fn mark_exec_global_args(cmd: clap::Command) -> clap::Command {
cmd.mut_arg("model", |arg| arg.global(true))
.mut_arg("dangerously_bypass_approvals_and_sandbox", |arg| {
arg.global(true)
})
.mut_arg("bypass_hook_trust", |arg| arg.global(true))
.mut_arg("worktree", |arg| arg.global(true))
}
#[derive(Debug, clap::Subcommand)]
pub enum Command {
/// Resume a previous session by id or pick the most recent with --last.
Resume(ResumeArgs),
/// Fork a previous session by id into a new session.
Fork(ForkArgs),
/// Run a code review against the current repository.
Review(ReviewArgs),
}
#[derive(Args, Debug)]
pub struct ForkArgs {
/// Conversation/session id (UUID) or thread name to fork.
#[arg(value_name = "SESSION_ID")]
pub session_id: String,
/// Optional image(s) to attach to the prompt sent after forking.
#[arg(
long = "image",
short = 'i',
value_name = "FILE",
value_delimiter = ',',
num_args = 1
)]
pub images: Vec<PathBuf>,
/// Optional prompt to send after forking. If `-` is used, read from stdin.
#[arg(value_name = "PROMPT", value_hint = clap::ValueHint::Other)]
pub prompt: Option<String>,
}
#[derive(Args, Debug)]
struct ResumeArgsRaw {
// Note: This is the direct clap shape. We reinterpret the positional when --last is set
// so "codex resume --last <prompt>" treats the positional as a prompt, not a session id.
/// Conversation/session id (UUID) or thread name. UUIDs take precedence if it parses.
/// If omitted, use --last to pick the most recent recorded session.
#[arg(value_name = "SESSION_ID")]
session_id: Option<String>,
/// Resume the most recent recorded session (newest) without specifying an id.
#[arg(long = "last", default_value_t = false)]
last: bool,
/// Show all sessions (disables cwd filtering).
#[arg(long = "all", default_value_t = false)]
all: bool,
/// Optional image(s) to attach to the prompt sent after resuming.
#[arg(
long = "image",
short = 'i',
value_name = "FILE",
value_delimiter = ',',
num_args = 1
)]
images: Vec<PathBuf>,
/// Prompt to send after resuming the session. If `-` is used, read from stdin.
#[arg(value_name = "PROMPT", value_hint = clap::ValueHint::Other)]
prompt: Option<String>,
}
#[derive(Debug)]
pub struct ResumeArgs {
/// Conversation/session id (UUID) or thread name. UUIDs take precedence if it parses.
/// If omitted, use --last to pick the most recent recorded session.
pub session_id: Option<String>,
/// Resume the most recent recorded session (newest) without specifying an id.
pub last: bool,
/// Show all sessions (disables cwd filtering).
pub all: bool,
/// Optional image(s) to attach to the prompt sent after resuming.
pub images: Vec<PathBuf>,
/// Prompt to send after resuming the session. If `-` is used, read from stdin.
pub prompt: Option<String>,
}
impl From<ResumeArgsRaw> for ResumeArgs {
fn from(raw: ResumeArgsRaw) -> Self {
// When --last is used without an explicit prompt, treat the positional as the prompt
// (clap can’t express this conditional positional meaning cleanly).
let (session_id, prompt) = if raw.last && raw.prompt.is_none() {
(None, raw.session_id)
} else {
(raw.session_id, raw.prompt)
};
Self {
session_id,
last: raw.last,
all: raw.all,
images: raw.images,
prompt,
}
}
}
impl Args for ResumeArgs {
fn augment_args(cmd: clap::Command) -> clap::Command {
ResumeArgsRaw::augment_args(cmd)
}
fn augment_args_for_update(cmd: clap::Command) -> clap::Command {
ResumeArgsRaw::augment_args_for_update(cmd)
}
}
impl FromArgMatches for ResumeArgs {
fn from_arg_matches(matches: &clap::ArgMatches) -> Result<Self, clap::Error> {
ResumeArgsRaw::from_arg_matches(matches).map(Self::from)
}
fn update_from_arg_matches(&mut self, matches: &clap::ArgMatches) -> Result<(), clap::Error> {
*self = ResumeArgsRaw::from_arg_matches(matches).map(Self::from)?;
Ok(())
}
}
#[derive(Args, Debug)]
pub struct ReviewArgs {
/// Review staged, unstaged, and untracked changes.
#[arg(
long = "uncommitted",
default_value_t = false,
conflicts_with_all = ["base", "commit", "prompt"]
)]
pub uncommitted: bool,
/// Review changes against the given base branch.
#[arg(
long = "base",
value_name = "BRANCH",
conflicts_with_all = ["uncommitted", "commit", "prompt"]
)]
pub base: Option<String>,
/// Review the changes introduced by a commit.
#[arg(
long = "commit",
value_name = "SHA",
conflicts_with_all = ["uncommitted", "base", "prompt"]
)]
pub commit: Option<String>,
/// Optional commit title to display in the review summary.
#[arg(long = "title", value_name = "TITLE", requires = "commit")]
pub commit_title: Option<String>,
/// Custom review instructions. If `-` is used, read from stdin.
#[arg(value_name = "PROMPT", value_hint = clap::ValueHint::Other)]
pub prompt: Option<String>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, ValueEnum)]
#[value(rename_all = "kebab-case")]
pub enum Color {
Always,
Never,
#[default]
Auto,
}
#[cfg(test)]
#[path = "cli_tests.rs"]
mod tests;