mirror of
https://github.com/openai/codex.git
synced 2026-04-24 06:35:50 +00:00
changes
This commit is contained in:
@@ -2,6 +2,7 @@ use std::collections::VecDeque;
|
||||
use std::ffi::OsString;
|
||||
use std::fs;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io;
|
||||
use std::io::BufRead;
|
||||
use std::io::BufReader;
|
||||
use std::io::Write;
|
||||
@@ -32,6 +33,7 @@ use codex_app_server_protocol::CommandExecutionRequestApprovalParams;
|
||||
use codex_app_server_protocol::CommandExecutionRequestApprovalResponse;
|
||||
use codex_app_server_protocol::CommandExecutionStatus;
|
||||
use codex_app_server_protocol::DynamicToolSpec;
|
||||
use codex_app_server_protocol::ExecPolicyAmendment;
|
||||
use codex_app_server_protocol::FileChangeApprovalDecision;
|
||||
use codex_app_server_protocol::FileChangeRequestApprovalParams;
|
||||
use codex_app_server_protocol::FileChangeRequestApprovalResponse;
|
||||
@@ -146,10 +148,28 @@ struct Cli {
|
||||
#[arg(long, value_name = "json-or-@file", global = true)]
|
||||
dynamic_tools: Option<String>,
|
||||
|
||||
/// Attach a skill input item to V2 turn/start requests.
|
||||
///
|
||||
/// Must be paired with --skill-path.
|
||||
#[arg(long, value_name = "skill-name", global = true)]
|
||||
skill_name: Option<String>,
|
||||
|
||||
/// Path to the SKILL.md file for --skill-name.
|
||||
///
|
||||
/// Must be paired with --skill-name.
|
||||
#[arg(long, value_name = "path-to-skill-md", global = true)]
|
||||
skill_path: Option<PathBuf>,
|
||||
|
||||
#[command(subcommand)]
|
||||
command: CliCommand,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct SkillSelection {
|
||||
name: String,
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum CliCommand {
|
||||
/// Start `codex app-server` on a websocket endpoint in the background.
|
||||
@@ -247,19 +267,24 @@ pub fn run() -> Result<()> {
|
||||
url,
|
||||
config_overrides,
|
||||
dynamic_tools,
|
||||
skill_name,
|
||||
skill_path,
|
||||
command,
|
||||
} = Cli::parse();
|
||||
|
||||
let dynamic_tools = parse_dynamic_tools_arg(&dynamic_tools)?;
|
||||
let skill_selection = parse_skill_selection(skill_name, skill_path)?;
|
||||
|
||||
match command {
|
||||
CliCommand::Serve { listen, kill } => {
|
||||
ensure_dynamic_tools_unused(&dynamic_tools, "serve")?;
|
||||
ensure_skill_unused(&skill_selection, "serve")?;
|
||||
let codex_bin = codex_bin.unwrap_or_else(|| PathBuf::from("codex"));
|
||||
serve(&codex_bin, &config_overrides, &listen, kill)
|
||||
}
|
||||
CliCommand::SendMessage { user_message } => {
|
||||
ensure_dynamic_tools_unused(&dynamic_tools, "send-message")?;
|
||||
ensure_skill_unused(&skill_selection, "send-message")?;
|
||||
let endpoint = resolve_endpoint(codex_bin, url)?;
|
||||
send_message(&endpoint, &config_overrides, user_message)
|
||||
}
|
||||
@@ -274,6 +299,7 @@ pub fn run() -> Result<()> {
|
||||
user_message,
|
||||
experimental_api,
|
||||
&dynamic_tools,
|
||||
skill_selection.as_ref(),
|
||||
)
|
||||
}
|
||||
CliCommand::ResumeMessageV2 {
|
||||
@@ -287,24 +313,43 @@ pub fn run() -> Result<()> {
|
||||
thread_id,
|
||||
user_message,
|
||||
&dynamic_tools,
|
||||
skill_selection.as_ref(),
|
||||
)
|
||||
}
|
||||
CliCommand::ThreadResume { thread_id } => {
|
||||
ensure_dynamic_tools_unused(&dynamic_tools, "thread-resume")?;
|
||||
ensure_skill_unused(&skill_selection, "thread-resume")?;
|
||||
let endpoint = resolve_endpoint(codex_bin, url)?;
|
||||
thread_resume_follow(&endpoint, &config_overrides, thread_id)
|
||||
}
|
||||
CliCommand::TriggerCmdApproval { user_message } => {
|
||||
let endpoint = resolve_endpoint(codex_bin, url)?;
|
||||
trigger_cmd_approval(&endpoint, &config_overrides, user_message, &dynamic_tools)
|
||||
trigger_cmd_approval(
|
||||
&endpoint,
|
||||
&config_overrides,
|
||||
user_message,
|
||||
&dynamic_tools,
|
||||
skill_selection.as_ref(),
|
||||
)
|
||||
}
|
||||
CliCommand::TriggerPatchApproval { user_message } => {
|
||||
let endpoint = resolve_endpoint(codex_bin, url)?;
|
||||
trigger_patch_approval(&endpoint, &config_overrides, user_message, &dynamic_tools)
|
||||
trigger_patch_approval(
|
||||
&endpoint,
|
||||
&config_overrides,
|
||||
user_message,
|
||||
&dynamic_tools,
|
||||
skill_selection.as_ref(),
|
||||
)
|
||||
}
|
||||
CliCommand::NoTriggerCmdApproval => {
|
||||
let endpoint = resolve_endpoint(codex_bin, url)?;
|
||||
no_trigger_cmd_approval(&endpoint, &config_overrides, &dynamic_tools)
|
||||
no_trigger_cmd_approval(
|
||||
&endpoint,
|
||||
&config_overrides,
|
||||
&dynamic_tools,
|
||||
skill_selection.as_ref(),
|
||||
)
|
||||
}
|
||||
CliCommand::SendFollowUpV2 {
|
||||
first_message,
|
||||
@@ -317,6 +362,7 @@ pub fn run() -> Result<()> {
|
||||
first_message,
|
||||
follow_up_message,
|
||||
&dynamic_tools,
|
||||
skill_selection.as_ref(),
|
||||
)
|
||||
}
|
||||
CliCommand::TriggerZshForkMultiCmdApproval {
|
||||
@@ -336,21 +382,25 @@ pub fn run() -> Result<()> {
|
||||
}
|
||||
CliCommand::TestLogin => {
|
||||
ensure_dynamic_tools_unused(&dynamic_tools, "test-login")?;
|
||||
ensure_skill_unused(&skill_selection, "test-login")?;
|
||||
let endpoint = resolve_endpoint(codex_bin, url)?;
|
||||
test_login(&endpoint, &config_overrides)
|
||||
}
|
||||
CliCommand::GetAccountRateLimits => {
|
||||
ensure_dynamic_tools_unused(&dynamic_tools, "get-account-rate-limits")?;
|
||||
ensure_skill_unused(&skill_selection, "get-account-rate-limits")?;
|
||||
let endpoint = resolve_endpoint(codex_bin, url)?;
|
||||
get_account_rate_limits(&endpoint, &config_overrides)
|
||||
}
|
||||
CliCommand::ModelList => {
|
||||
ensure_dynamic_tools_unused(&dynamic_tools, "model-list")?;
|
||||
ensure_skill_unused(&skill_selection, "model-list")?;
|
||||
let endpoint = resolve_endpoint(codex_bin, url)?;
|
||||
model_list(&endpoint, &config_overrides)
|
||||
}
|
||||
CliCommand::ThreadList { limit } => {
|
||||
ensure_dynamic_tools_unused(&dynamic_tools, "thread-list")?;
|
||||
ensure_skill_unused(&skill_selection, "thread-list")?;
|
||||
let endpoint = resolve_endpoint(codex_bin, url)?;
|
||||
thread_list(&endpoint, &config_overrides, limit)
|
||||
}
|
||||
@@ -526,6 +576,7 @@ pub fn send_message_v2(
|
||||
user_message,
|
||||
true,
|
||||
dynamic_tools,
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -535,6 +586,7 @@ fn send_message_v2_endpoint(
|
||||
user_message: String,
|
||||
experimental_api: bool,
|
||||
dynamic_tools: &Option<Vec<DynamicToolSpec>>,
|
||||
skill_selection: Option<&SkillSelection>,
|
||||
) -> Result<()> {
|
||||
if dynamic_tools.is_some() && !experimental_api {
|
||||
bail!("--dynamic-tools requires --experimental-api for send-message-v2");
|
||||
@@ -548,6 +600,7 @@ fn send_message_v2_endpoint(
|
||||
None,
|
||||
None,
|
||||
dynamic_tools,
|
||||
skill_selection,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -652,6 +705,7 @@ fn resume_message_v2(
|
||||
thread_id: String,
|
||||
user_message: String,
|
||||
dynamic_tools: &Option<Vec<DynamicToolSpec>>,
|
||||
skill_selection: Option<&SkillSelection>,
|
||||
) -> Result<()> {
|
||||
ensure_dynamic_tools_unused(dynamic_tools, "resume-message-v2")?;
|
||||
|
||||
@@ -668,10 +722,7 @@ fn resume_message_v2(
|
||||
|
||||
let turn_response = client.turn_start(TurnStartParams {
|
||||
thread_id: resume_response.thread.id.clone(),
|
||||
input: vec![V2UserInput::Text {
|
||||
text: user_message,
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
input: build_v2_input(user_message, skill_selection),
|
||||
..Default::default()
|
||||
})?;
|
||||
println!("< turn/start response: {turn_response:?}");
|
||||
@@ -706,6 +757,7 @@ fn trigger_cmd_approval(
|
||||
config_overrides: &[String],
|
||||
user_message: Option<String>,
|
||||
dynamic_tools: &Option<Vec<DynamicToolSpec>>,
|
||||
skill_selection: Option<&SkillSelection>,
|
||||
) -> Result<()> {
|
||||
let default_prompt =
|
||||
"Run `touch /tmp/should-trigger-approval` so I can confirm the file exists.";
|
||||
@@ -720,6 +772,7 @@ fn trigger_cmd_approval(
|
||||
access: ReadOnlyAccess::FullAccess,
|
||||
}),
|
||||
dynamic_tools,
|
||||
skill_selection,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -728,6 +781,7 @@ fn trigger_patch_approval(
|
||||
config_overrides: &[String],
|
||||
user_message: Option<String>,
|
||||
dynamic_tools: &Option<Vec<DynamicToolSpec>>,
|
||||
skill_selection: Option<&SkillSelection>,
|
||||
) -> Result<()> {
|
||||
let default_prompt =
|
||||
"Create a file named APPROVAL_DEMO.txt containing a short hello message using apply_patch.";
|
||||
@@ -742,6 +796,7 @@ fn trigger_patch_approval(
|
||||
access: ReadOnlyAccess::FullAccess,
|
||||
}),
|
||||
dynamic_tools,
|
||||
skill_selection,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -749,6 +804,7 @@ fn no_trigger_cmd_approval(
|
||||
endpoint: &Endpoint,
|
||||
config_overrides: &[String],
|
||||
dynamic_tools: &Option<Vec<DynamicToolSpec>>,
|
||||
skill_selection: Option<&SkillSelection>,
|
||||
) -> Result<()> {
|
||||
let prompt = "Run `touch should_not_trigger_approval.txt`";
|
||||
send_message_v2_with_policies(
|
||||
@@ -759,6 +815,7 @@ fn no_trigger_cmd_approval(
|
||||
None,
|
||||
None,
|
||||
dynamic_tools,
|
||||
skill_selection,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -770,6 +827,7 @@ fn send_message_v2_with_policies(
|
||||
approval_policy: Option<AskForApproval>,
|
||||
sandbox_policy: Option<SandboxPolicy>,
|
||||
dynamic_tools: &Option<Vec<DynamicToolSpec>>,
|
||||
skill_selection: Option<&SkillSelection>,
|
||||
) -> Result<()> {
|
||||
let mut client = CodexClient::connect(endpoint, config_overrides)?;
|
||||
|
||||
@@ -783,11 +841,7 @@ fn send_message_v2_with_policies(
|
||||
println!("< thread/start response: {thread_response:?}");
|
||||
let mut turn_params = TurnStartParams {
|
||||
thread_id: thread_response.thread.id.clone(),
|
||||
input: vec![V2UserInput::Text {
|
||||
text: user_message,
|
||||
// Test client sends plain text without UI element ranges.
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
input: build_v2_input(user_message, skill_selection),
|
||||
..Default::default()
|
||||
};
|
||||
turn_params.approval_policy = approval_policy;
|
||||
@@ -807,6 +861,7 @@ fn send_follow_up_v2(
|
||||
first_message: String,
|
||||
follow_up_message: String,
|
||||
dynamic_tools: &Option<Vec<DynamicToolSpec>>,
|
||||
skill_selection: Option<&SkillSelection>,
|
||||
) -> Result<()> {
|
||||
let mut client = CodexClient::connect(endpoint, config_overrides)?;
|
||||
|
||||
@@ -821,11 +876,7 @@ fn send_follow_up_v2(
|
||||
|
||||
let first_turn_params = TurnStartParams {
|
||||
thread_id: thread_response.thread.id.clone(),
|
||||
input: vec![V2UserInput::Text {
|
||||
text: first_message,
|
||||
// Test client sends plain text without UI element ranges.
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
input: build_v2_input(first_message, skill_selection),
|
||||
..Default::default()
|
||||
};
|
||||
let first_turn_response = client.turn_start(first_turn_params)?;
|
||||
@@ -834,11 +885,7 @@ fn send_follow_up_v2(
|
||||
|
||||
let follow_up_params = TurnStartParams {
|
||||
thread_id: thread_response.thread.id.clone(),
|
||||
input: vec![V2UserInput::Text {
|
||||
text: follow_up_message,
|
||||
// Test client sends plain text without UI element ranges.
|
||||
text_elements: Vec::new(),
|
||||
}],
|
||||
input: build_v2_input(follow_up_message, skill_selection),
|
||||
..Default::default()
|
||||
};
|
||||
let follow_up_response = client.turn_start(follow_up_params)?;
|
||||
@@ -934,6 +981,51 @@ fn ensure_dynamic_tools_unused(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_skill_unused(skill_selection: &Option<SkillSelection>, command: &str) -> Result<()> {
|
||||
if skill_selection.is_some() {
|
||||
bail!(
|
||||
"skill input is only supported for v2 turn/start commands; remove --skill-name/--skill-path for {command}"
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn parse_skill_selection(
|
||||
skill_name: Option<String>,
|
||||
skill_path: Option<PathBuf>,
|
||||
) -> Result<Option<SkillSelection>> {
|
||||
match (skill_name, skill_path) {
|
||||
(None, None) => Ok(None),
|
||||
(Some(name), Some(path)) => {
|
||||
let path = fs::canonicalize(&path)
|
||||
.with_context(|| format!("canonicalize --skill-path {}", path.display()))?;
|
||||
Ok(Some(SkillSelection { name, path }))
|
||||
}
|
||||
(Some(_), None) => bail!("--skill-name requires --skill-path"),
|
||||
(None, Some(_)) => bail!("--skill-path requires --skill-name"),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_v2_input(
|
||||
user_message: String,
|
||||
skill_selection: Option<&SkillSelection>,
|
||||
) -> Vec<V2UserInput> {
|
||||
let mut input = vec![V2UserInput::Text {
|
||||
text: user_message,
|
||||
// Test client sends plain text without UI element ranges.
|
||||
text_elements: Vec::new(),
|
||||
}];
|
||||
|
||||
if let Some(skill_selection) = skill_selection {
|
||||
input.push(V2UserInput::Skill {
|
||||
name: skill_selection.name.clone(),
|
||||
path: skill_selection.path.clone(),
|
||||
});
|
||||
}
|
||||
|
||||
input
|
||||
}
|
||||
|
||||
fn parse_dynamic_tools_arg(dynamic_tools: &Option<String>) -> Result<Option<Vec<DynamicToolSpec>>> {
|
||||
let Some(raw_arg) = dynamic_tools.as_deref() else {
|
||||
return Ok(None);
|
||||
@@ -980,6 +1072,7 @@ struct CodexClient {
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum CommandApprovalBehavior {
|
||||
Prompt,
|
||||
AlwaysAccept,
|
||||
AbortOn(usize),
|
||||
}
|
||||
@@ -1030,7 +1123,7 @@ impl CodexClient {
|
||||
stdout: BufReader::new(stdout),
|
||||
},
|
||||
pending_notifications: VecDeque::new(),
|
||||
command_approval_behavior: CommandApprovalBehavior::AlwaysAccept,
|
||||
command_approval_behavior: CommandApprovalBehavior::Prompt,
|
||||
command_approval_count: 0,
|
||||
command_approval_item_ids: Vec::new(),
|
||||
command_execution_statuses: Vec::new(),
|
||||
@@ -1051,7 +1144,7 @@ impl CodexClient {
|
||||
socket: Box::new(socket),
|
||||
},
|
||||
pending_notifications: VecDeque::new(),
|
||||
command_approval_behavior: CommandApprovalBehavior::AlwaysAccept,
|
||||
command_approval_behavior: CommandApprovalBehavior::Prompt,
|
||||
command_approval_count: 0,
|
||||
command_approval_item_ids: Vec::new(),
|
||||
command_execution_statuses: Vec::new(),
|
||||
@@ -1571,19 +1664,20 @@ impl CodexClient {
|
||||
}
|
||||
|
||||
let decision = match self.command_approval_behavior {
|
||||
CommandApprovalBehavior::Prompt => {
|
||||
self.command_approval_decision(proposed_execpolicy_amendment)?
|
||||
}
|
||||
CommandApprovalBehavior::AlwaysAccept => CommandExecutionApprovalDecision::Accept,
|
||||
CommandApprovalBehavior::AbortOn(index) if self.command_approval_count == index => {
|
||||
CommandExecutionApprovalDecision::Cancel
|
||||
}
|
||||
CommandApprovalBehavior::AbortOn(_) => CommandExecutionApprovalDecision::Accept,
|
||||
};
|
||||
let response = CommandExecutionRequestApprovalResponse {
|
||||
decision: decision.clone(),
|
||||
};
|
||||
let response = CommandExecutionRequestApprovalResponse { decision };
|
||||
self.send_server_request_response(request_id, &response)?;
|
||||
println!(
|
||||
"< commandExecution decision for approval #{} on item {item_id}: {:?}",
|
||||
self.command_approval_count, decision
|
||||
self.command_approval_count, response.decision
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
@@ -1627,14 +1721,39 @@ impl CodexClient {
|
||||
println!("< grant root: {}", grant_root.display());
|
||||
}
|
||||
|
||||
let response = FileChangeRequestApprovalResponse {
|
||||
decision: FileChangeApprovalDecision::Accept,
|
||||
};
|
||||
let decision = self.file_change_approval_decision()?;
|
||||
let response = FileChangeRequestApprovalResponse { decision };
|
||||
self.send_server_request_response(request_id, &response)?;
|
||||
println!("< approved fileChange request for item {item_id}");
|
||||
println!(
|
||||
"< responded to fileChange request for item {item_id}: {:?}",
|
||||
response.decision
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn command_approval_decision(
|
||||
&self,
|
||||
proposed_execpolicy_amendment: Option<ExecPolicyAmendment>,
|
||||
) -> Result<CommandExecutionApprovalDecision> {
|
||||
if let Some(execpolicy_amendment) = proposed_execpolicy_amendment {
|
||||
return prompt_for_command_approval_with_amendment(execpolicy_amendment);
|
||||
}
|
||||
|
||||
if prompt_for_yes_no("Approve command execution request? [y/n] ")? {
|
||||
Ok(CommandExecutionApprovalDecision::Accept)
|
||||
} else {
|
||||
Ok(CommandExecutionApprovalDecision::Decline)
|
||||
}
|
||||
}
|
||||
|
||||
fn file_change_approval_decision(&self) -> Result<FileChangeApprovalDecision> {
|
||||
if prompt_for_yes_no("Approve file-change request? [y/n] ")? {
|
||||
Ok(FileChangeApprovalDecision::Accept)
|
||||
} else {
|
||||
Ok(FileChangeApprovalDecision::Decline)
|
||||
}
|
||||
}
|
||||
|
||||
fn send_server_request_response<T>(&mut self, request_id: RequestId, response: &T) -> Result<()>
|
||||
where
|
||||
T: Serialize,
|
||||
@@ -1709,6 +1828,59 @@ fn print_multiline_with_prefix(prefix: &str, payload: &str) {
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_for_yes_no(prompt: &str) -> Result<bool> {
|
||||
loop {
|
||||
print!("{prompt}");
|
||||
io::stdout()
|
||||
.flush()
|
||||
.context("failed to flush approval prompt")?;
|
||||
|
||||
let mut line = String::new();
|
||||
io::stdin()
|
||||
.read_line(&mut line)
|
||||
.context("failed to read approval input")?;
|
||||
let input = line.trim().to_ascii_lowercase();
|
||||
if matches!(input.as_str(), "y" | "yes") {
|
||||
return Ok(true);
|
||||
}
|
||||
if matches!(input.as_str(), "n" | "no") {
|
||||
return Ok(false);
|
||||
}
|
||||
println!("please answer y or n");
|
||||
}
|
||||
}
|
||||
|
||||
fn prompt_for_command_approval_with_amendment(
|
||||
execpolicy_amendment: ExecPolicyAmendment,
|
||||
) -> Result<CommandExecutionApprovalDecision> {
|
||||
loop {
|
||||
print!("Approve command execution request? [y/n/a] (a=always allow) ");
|
||||
io::stdout()
|
||||
.flush()
|
||||
.context("failed to flush approval prompt")?;
|
||||
|
||||
let mut line = String::new();
|
||||
io::stdin()
|
||||
.read_line(&mut line)
|
||||
.context("failed to read approval input")?;
|
||||
let input = line.trim().to_ascii_lowercase();
|
||||
if matches!(input.as_str(), "y" | "yes") {
|
||||
return Ok(CommandExecutionApprovalDecision::Accept);
|
||||
}
|
||||
if matches!(input.as_str(), "n" | "no") {
|
||||
return Ok(CommandExecutionApprovalDecision::Decline);
|
||||
}
|
||||
if matches!(input.as_str(), "a" | "always" | "always allow") {
|
||||
return Ok(
|
||||
CommandExecutionApprovalDecision::AcceptWithExecpolicyAmendment {
|
||||
execpolicy_amendment: execpolicy_amendment.clone(),
|
||||
},
|
||||
);
|
||||
}
|
||||
println!("please answer y, n, or a");
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for CodexClient {
|
||||
fn drop(&mut self) {
|
||||
let ClientTransport::Stdio { child, stdin, .. } = &mut self.transport else {
|
||||
|
||||
Reference in New Issue
Block a user