mirror of
https://github.com/openai/codex.git
synced 2026-10-21 16:38:24 +00:00
## Why `codex exec` and the TypeScript SDK need a way to request a Cyber access program for an individual turn. Resuming with an API key can also leave compaction using a saved program that the current credentials cannot access. ## What changed - Add `--cyber-access-program` with `standard`, `daybreak_blue`, and `daybreak_red`, and expose the equivalent `cyberAccessProgram` turn option and `CyberAccessProgram` type in the TypeScript SDK. - Apply explicit selections to new, resumed, and prompted fork turns. Override automatic Daybreak selection for that turn without changing the saved choice. Reject the flag for review and forks without a prompt. - Filter Cyber programs by the built-in OpenAI provider when starting turns and restoring previous-turn settings for compaction. - Allow previous-model compaction fallback with API-key authentication, comparing both model and program so a program change can trigger a retry with the current pair even when the model is unchanged. ## Testing Add CLI help, argument parsing, exec, and SDK coverage for per-turn selection and unsupported commands. Extend core tests for API-key compaction retries, denied explicit selections, custom-provider filtering, remote compaction, and WebSocket response reuse. GitOrigin-RevId: 586f25b621924f3bc509c9a39b39fe2020ff9cd2
343 lines
10 KiB
Rust
343 lines
10 KiB
Rust
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,
|
||
|
||
/// 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;
|