mirror of
https://github.com/openai/codex.git
synced 2026-02-25 10:13:49 +00:00
Compare commits
5 Commits
jif/record
...
dev/cc/new
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
09d312bce1 | ||
|
|
9e3392f51b | ||
|
|
bb76c41e7f | ||
|
|
cd3edc28d8 | ||
|
|
9814ea1488 |
@@ -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 {
|
||||
|
||||
@@ -135,6 +135,8 @@ pub enum Feature {
|
||||
SkillEnvVarDependencyPrompt,
|
||||
/// Emit skill approval test prompts/events.
|
||||
SkillApproval,
|
||||
/// Run matching shell_command skill executables under the skill sandbox.
|
||||
SkillShellCommandSandbox,
|
||||
/// Steer feature flag - when enabled, Enter submits immediately instead of queuing.
|
||||
Steer,
|
||||
/// Enable collaboration modes (Plan, Default).
|
||||
@@ -629,6 +631,12 @@ pub const FEATURES: &[FeatureSpec] = &[
|
||||
stage: Stage::UnderDevelopment,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::SkillShellCommandSandbox,
|
||||
key: "skill_shell_command_sandbox",
|
||||
stage: Stage::UnderDevelopment,
|
||||
default_enabled: false,
|
||||
},
|
||||
FeatureSpec {
|
||||
id: Feature::Steer,
|
||||
key: "steer",
|
||||
|
||||
@@ -433,6 +433,7 @@ mod tests {
|
||||
interface: None,
|
||||
dependencies: Some(SkillDependencies { tools }),
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: PathBuf::from("skill"),
|
||||
scope: SkillScope::User,
|
||||
|
||||
@@ -482,6 +482,7 @@ mod tests {
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: PathBuf::from(path),
|
||||
scope: codex_protocol::protocol::SkillScope::User,
|
||||
|
||||
@@ -87,6 +87,21 @@ pub(crate) fn detect_implicit_skill_script_invocation_for_tokens(
|
||||
detect_skill_script_run(outcome, command, workdir)
|
||||
}
|
||||
|
||||
pub(crate) fn detect_implicit_skill_executable_invocation_for_tokens(
|
||||
outcome: &SkillLoadOutcome,
|
||||
command: &[String],
|
||||
) -> Option<(SkillMetadata, PathBuf)> {
|
||||
let executable = Path::new(command.first()?);
|
||||
if !executable.is_absolute() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let executable = normalize_path(executable);
|
||||
let parent = executable.parent()?;
|
||||
let skill = detect_skill_for_path_under_scripts(outcome, parent)?;
|
||||
Some((skill, executable))
|
||||
}
|
||||
|
||||
fn tokenize_command(command: &str) -> Vec<String> {
|
||||
shlex::split(command).unwrap_or_else(|| {
|
||||
command
|
||||
@@ -255,8 +270,14 @@ fn detect_skill_script_run(
|
||||
workdir.join(script_path)
|
||||
};
|
||||
let script_path = normalize_path(script_path.as_path());
|
||||
detect_skill_for_path_under_scripts(outcome, script_path.as_path())
|
||||
}
|
||||
|
||||
for ancestor in script_path.ancestors() {
|
||||
fn detect_skill_for_path_under_scripts(
|
||||
outcome: &SkillLoadOutcome,
|
||||
path: &Path,
|
||||
) -> Option<SkillMetadata> {
|
||||
for ancestor in path.ancestors() {
|
||||
if let Some(candidate) = outcome.implicit_skills_by_scripts_dir.get(ancestor) {
|
||||
return Some(candidate.clone());
|
||||
}
|
||||
@@ -317,6 +338,7 @@ fn normalize_path(path: &Path) -> PathBuf {
|
||||
mod tests {
|
||||
use super::SkillLoadOutcome;
|
||||
use super::SkillMetadata;
|
||||
use super::detect_implicit_skill_executable_invocation_for_tokens;
|
||||
use super::detect_implicit_skill_script_invocation_for_command;
|
||||
use super::detect_skill_doc_read;
|
||||
use super::detect_skill_script_run;
|
||||
@@ -336,6 +358,7 @@ mod tests {
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: skill_doc_path,
|
||||
scope: codex_protocol::protocol::SkillScope::User,
|
||||
@@ -483,4 +506,44 @@ mod tests {
|
||||
|
||||
assert_eq!(found, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_skill_executable_detection_matches_absolute_path() {
|
||||
let skill_doc_path = PathBuf::from("/tmp/skill-test/SKILL.md");
|
||||
let scripts_dir = normalize_path(Path::new("/tmp/skill-test/scripts"));
|
||||
let skill = test_skill_metadata(skill_doc_path);
|
||||
let outcome = SkillLoadOutcome {
|
||||
implicit_skills_by_scripts_dir: Arc::new(HashMap::from([(scripts_dir, skill)])),
|
||||
implicit_skills_by_doc_path: Arc::new(HashMap::new()),
|
||||
..Default::default()
|
||||
};
|
||||
let command = vec!["/tmp/skill-test/scripts/run".to_string()];
|
||||
|
||||
let found = detect_implicit_skill_executable_invocation_for_tokens(&outcome, &command);
|
||||
|
||||
assert_eq!(
|
||||
found.map(|(skill, path)| (skill.name, path)),
|
||||
Some((
|
||||
"test-skill".to_string(),
|
||||
PathBuf::from("/tmp/skill-test/scripts/run")
|
||||
))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn direct_skill_executable_detection_ignores_relative_path() {
|
||||
let skill_doc_path = PathBuf::from("/tmp/skill-test/SKILL.md");
|
||||
let scripts_dir = normalize_path(Path::new("/tmp/skill-test/scripts"));
|
||||
let skill = test_skill_metadata(skill_doc_path);
|
||||
let outcome = SkillLoadOutcome {
|
||||
implicit_skills_by_scripts_dir: Arc::new(HashMap::from([(scripts_dir, skill)])),
|
||||
implicit_skills_by_doc_path: Arc::new(HashMap::new()),
|
||||
..Default::default()
|
||||
};
|
||||
let command = vec!["scripts/run".to_string()];
|
||||
|
||||
let found = detect_implicit_skill_executable_invocation_for_tokens(&outcome, &command);
|
||||
|
||||
assert_eq!(found, None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,6 +13,7 @@ use crate::skills::model::SkillMetadata;
|
||||
use crate::skills::model::SkillPolicy;
|
||||
use crate::skills::model::SkillToolDependency;
|
||||
use crate::skills::permissions::compile_permission_profile;
|
||||
use crate::skills::permissions::normalize_permission_profile;
|
||||
use crate::skills::system::system_cache_root_dir;
|
||||
use codex_app_server_protocol::ConfigLayerSource;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
@@ -506,7 +507,7 @@ fn parse_skill_file(path: &Path, scope: SkillScope) -> Result<SkillMetadata, Ski
|
||||
.as_deref()
|
||||
.map(sanitize_single_line)
|
||||
.filter(|value| !value.is_empty());
|
||||
let (interface, dependencies, policy, permissions) = load_skill_metadata(path);
|
||||
let (interface, dependencies, policy, permission_profile, permissions) = load_skill_metadata(path);
|
||||
|
||||
validate_len(&name, MAX_NAME_LEN, "name")?;
|
||||
validate_len(&description, MAX_DESCRIPTION_LEN, "description")?;
|
||||
@@ -527,6 +528,7 @@ fn parse_skill_file(path: &Path, scope: SkillScope) -> Result<SkillMetadata, Ski
|
||||
interface,
|
||||
dependencies,
|
||||
policy,
|
||||
permission_profile,
|
||||
permissions,
|
||||
path_to_skills_md: resolved_path,
|
||||
scope,
|
||||
@@ -539,17 +541,18 @@ fn load_skill_metadata(
|
||||
Option<SkillInterface>,
|
||||
Option<SkillDependencies>,
|
||||
Option<SkillPolicy>,
|
||||
Option<PermissionProfile>,
|
||||
Option<Permissions>,
|
||||
) {
|
||||
// Fail open: optional metadata should not block loading SKILL.md.
|
||||
let Some(skill_dir) = skill_path.parent() else {
|
||||
return (None, None, None, None);
|
||||
return (None, None, None, None, None);
|
||||
};
|
||||
let metadata_path = skill_dir
|
||||
.join(SKILLS_METADATA_DIR)
|
||||
.join(SKILLS_METADATA_FILENAME);
|
||||
if !metadata_path.exists() {
|
||||
return (None, None, None, None);
|
||||
return (None, None, None, None, None);
|
||||
}
|
||||
|
||||
let contents = match fs::read_to_string(&metadata_path) {
|
||||
@@ -560,7 +563,7 @@ fn load_skill_metadata(
|
||||
path = metadata_path.display(),
|
||||
label = SKILLS_METADATA_FILENAME
|
||||
);
|
||||
return (None, None, None, None);
|
||||
return (None, None, None, None, None);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -572,7 +575,7 @@ fn load_skill_metadata(
|
||||
path = metadata_path.display(),
|
||||
label = SKILLS_METADATA_FILENAME
|
||||
);
|
||||
return (None, None, None, None);
|
||||
return (None, None, None, None, None);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -583,11 +586,14 @@ fn load_skill_metadata(
|
||||
permissions,
|
||||
} = parsed;
|
||||
|
||||
let permission_profile = normalize_permission_profile(skill_dir, permissions);
|
||||
|
||||
(
|
||||
resolve_interface(interface, skill_dir),
|
||||
resolve_dependencies(dependencies),
|
||||
resolve_policy(policy),
|
||||
compile_permission_profile(skill_dir, permissions),
|
||||
permission_profile.clone(),
|
||||
compile_permission_profile(skill_dir, permission_profile),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1048,6 +1054,7 @@ mod tests {
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::User,
|
||||
@@ -1196,6 +1203,7 @@ mod tests {
|
||||
],
|
||||
}),
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::User,
|
||||
@@ -1252,6 +1260,7 @@ interface:
|
||||
}),
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(skill_path.as_path()),
|
||||
scope: SkillScope::User,
|
||||
@@ -1356,6 +1365,17 @@ permissions:
|
||||
outcome.errors
|
||||
);
|
||||
assert_eq!(outcome.skills.len(), 1);
|
||||
assert_eq!(
|
||||
outcome.skills[0].permission_profile,
|
||||
Some(PermissionProfile {
|
||||
network: Some(true),
|
||||
file_system: Some(codex_protocol::models::FileSystemPermissions {
|
||||
read: Some(vec![normalized(skill_dir.join("data").as_path())]),
|
||||
write: Some(vec![normalized(skill_dir.join("output").as_path())]),
|
||||
}),
|
||||
macos: None,
|
||||
})
|
||||
);
|
||||
#[cfg(target_os = "macos")]
|
||||
let macos_seatbelt_profile_extensions =
|
||||
Some(crate::seatbelt_permissions::MacOsSeatbeltProfileExtensions::default());
|
||||
@@ -1586,6 +1606,7 @@ permissions:
|
||||
}),
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::User,
|
||||
@@ -1627,6 +1648,7 @@ permissions:
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::User,
|
||||
@@ -1681,6 +1703,7 @@ permissions:
|
||||
}),
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::User,
|
||||
@@ -1723,6 +1746,7 @@ permissions:
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::User,
|
||||
@@ -1768,6 +1792,7 @@ permissions:
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&shared_skill_path),
|
||||
scope: SkillScope::User,
|
||||
@@ -1829,6 +1854,7 @@ permissions:
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::User,
|
||||
@@ -1866,6 +1892,7 @@ permissions:
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&shared_skill_path),
|
||||
scope: SkillScope::Admin,
|
||||
@@ -1907,6 +1934,7 @@ permissions:
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&linked_skill_path),
|
||||
scope: SkillScope::Repo,
|
||||
@@ -1975,6 +2003,7 @@ permissions:
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&within_depth_path),
|
||||
scope: SkillScope::User,
|
||||
@@ -2003,6 +2032,7 @@ permissions:
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::User,
|
||||
@@ -2035,6 +2065,7 @@ permissions:
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::User,
|
||||
@@ -2148,6 +2179,7 @@ permissions:
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::Repo,
|
||||
@@ -2184,6 +2216,7 @@ permissions:
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::Repo,
|
||||
@@ -2238,6 +2271,7 @@ permissions:
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&nested_skill_path),
|
||||
scope: SkillScope::Repo,
|
||||
@@ -2249,6 +2283,7 @@ permissions:
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&root_skill_path),
|
||||
scope: SkillScope::Repo,
|
||||
@@ -2289,6 +2324,7 @@ permissions:
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::Repo,
|
||||
@@ -2327,6 +2363,7 @@ permissions:
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::Repo,
|
||||
@@ -2369,6 +2406,7 @@ permissions:
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&repo_skill_path),
|
||||
scope: SkillScope::Repo,
|
||||
@@ -2380,6 +2418,7 @@ permissions:
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&user_skill_path),
|
||||
scope: SkillScope::User,
|
||||
@@ -2445,6 +2484,7 @@ permissions:
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: first_path,
|
||||
scope: SkillScope::Repo,
|
||||
@@ -2456,6 +2496,7 @@ permissions:
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: second_path,
|
||||
scope: SkillScope::Repo,
|
||||
@@ -2528,6 +2569,7 @@ permissions:
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::Repo,
|
||||
@@ -2587,6 +2629,7 @@ permissions:
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: normalized(&skill_path),
|
||||
scope: SkillScope::System,
|
||||
|
||||
@@ -16,6 +16,7 @@ pub(crate) use injection::build_skill_injections;
|
||||
pub(crate) use injection::collect_explicit_skill_mentions;
|
||||
pub(crate) use invocation_utils::SKILL_APPROVAL_DECLINED_MESSAGE;
|
||||
pub(crate) use invocation_utils::build_implicit_skill_path_indexes;
|
||||
pub(crate) use invocation_utils::detect_implicit_skill_executable_invocation_for_tokens;
|
||||
pub(crate) use invocation_utils::ensure_skill_approval_for_command;
|
||||
pub(crate) use invocation_utils::maybe_emit_implicit_skill_invocation;
|
||||
pub use loader::load_skills;
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::config::Permissions;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
use codex_protocol::protocol::SkillScope;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
@@ -14,6 +15,7 @@ pub struct SkillMetadata {
|
||||
pub interface: Option<SkillInterface>,
|
||||
pub dependencies: Option<SkillDependencies>,
|
||||
pub policy: Option<SkillPolicy>,
|
||||
pub permission_profile: Option<PermissionProfile>,
|
||||
// This is an experimental field.
|
||||
pub permissions: Option<Permissions>,
|
||||
/// Path to the SKILLS.md file that declares this skill.
|
||||
|
||||
@@ -5,6 +5,7 @@ use std::path::PathBuf;
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
use codex_protocol::models::MacOsAutomationValue;
|
||||
use codex_protocol::models::FileSystemPermissions;
|
||||
use codex_protocol::models::MacOsPermissions;
|
||||
#[cfg(target_os = "macos")]
|
||||
use codex_protocol::models::MacOsPreferencesValue;
|
||||
@@ -25,10 +26,53 @@ use crate::seatbelt_permissions::MacOsSeatbeltProfileExtensions;
|
||||
#[cfg(not(target_os = "macos"))]
|
||||
type MacOsSeatbeltProfileExtensions = ();
|
||||
|
||||
pub(crate) fn normalize_permission_profile(
|
||||
skill_dir: &Path,
|
||||
permissions: Option<PermissionProfile>,
|
||||
) -> Option<PermissionProfile> {
|
||||
let PermissionProfile {
|
||||
network,
|
||||
file_system,
|
||||
macos,
|
||||
} = permissions?;
|
||||
let file_system = file_system.unwrap_or_default();
|
||||
let read = normalize_permission_paths(
|
||||
skill_dir,
|
||||
file_system.read.as_deref().unwrap_or_default(),
|
||||
"permissions.file_system.read",
|
||||
)
|
||||
.into_iter()
|
||||
.map(|path| path.as_path().to_path_buf())
|
||||
.collect::<Vec<PathBuf>>();
|
||||
let write = normalize_permission_paths(
|
||||
skill_dir,
|
||||
file_system.write.as_deref().unwrap_or_default(),
|
||||
"permissions.file_system.write",
|
||||
)
|
||||
.into_iter()
|
||||
.map(|path| path.as_path().to_path_buf())
|
||||
.collect::<Vec<PathBuf>>();
|
||||
let file_system = if read.is_empty() && write.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(FileSystemPermissions {
|
||||
read: (!read.is_empty()).then_some(read),
|
||||
write: (!write.is_empty()).then_some(write),
|
||||
})
|
||||
};
|
||||
|
||||
Some(PermissionProfile {
|
||||
network,
|
||||
file_system,
|
||||
macos,
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn compile_permission_profile(
|
||||
skill_dir: &Path,
|
||||
permissions: Option<PermissionProfile>,
|
||||
) -> Option<Permissions> {
|
||||
let permissions = normalize_permission_profile(skill_dir, permissions);
|
||||
let PermissionProfile {
|
||||
network,
|
||||
file_system,
|
||||
|
||||
@@ -14,7 +14,11 @@ use crate::function_tool::FunctionCallError;
|
||||
use crate::is_safe_command::is_known_safe_command;
|
||||
use crate::protocol::ExecCommandSource;
|
||||
use crate::shell::Shell;
|
||||
use crate::shell::ShellType;
|
||||
use crate::skills::SKILL_APPROVAL_DECLINED_MESSAGE;
|
||||
use crate::skills::SkillLoadOutcome;
|
||||
use crate::skills::SkillMetadata;
|
||||
use crate::skills::detect_implicit_skill_executable_invocation_for_tokens;
|
||||
use crate::skills::ensure_skill_approval_for_command;
|
||||
use crate::skills::maybe_emit_implicit_skill_invocation;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
@@ -31,6 +35,7 @@ use crate::tools::registry::ToolKind;
|
||||
use crate::tools::runtimes::shell::ShellRequest;
|
||||
use crate::tools::runtimes::shell::ShellRuntime;
|
||||
use crate::tools::runtimes::shell::ShellRuntimeBackend;
|
||||
use crate::tools::sandboxing::ExecApprovalRequirement;
|
||||
use crate::tools::sandboxing::ToolCtx;
|
||||
use crate::tools::spec::ShellCommandBackendConfig;
|
||||
use codex_protocol::models::PermissionProfile;
|
||||
@@ -47,6 +52,9 @@ pub struct ShellCommandHandler {
|
||||
backend: ShellCommandBackend,
|
||||
}
|
||||
|
||||
const SKILL_SHELL_COMMAND_APPROVAL_REASON: &str =
|
||||
"This command runs a skill script and requires approval.";
|
||||
|
||||
struct RunExecLikeArgs {
|
||||
tool_name: String,
|
||||
exec_params: ExecParams,
|
||||
@@ -297,6 +305,27 @@ impl ToolHandler for ShellCommandHandler {
|
||||
}
|
||||
|
||||
impl ShellHandler {
|
||||
fn detect_skill_shell_command(
|
||||
outcome: &SkillLoadOutcome,
|
||||
shell: &Shell,
|
||||
command: &[String],
|
||||
shell_runtime_backend: ShellRuntimeBackend,
|
||||
) -> Option<SkillMetadata> {
|
||||
if shell_runtime_backend != ShellRuntimeBackend::ShellCommandClassic
|
||||
|| shell.shell_type != ShellType::Zsh
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let commands = crate::bash::parse_shell_lc_plain_commands(command)?;
|
||||
let [inner_command] = commands.as_slice() else {
|
||||
return None;
|
||||
};
|
||||
let (skill, _path) =
|
||||
detect_implicit_skill_executable_invocation_for_tokens(outcome, inner_command)?;
|
||||
Some(skill)
|
||||
}
|
||||
|
||||
async fn run_exec_like(args: RunExecLikeArgs) -> Result<ToolOutput, FunctionCallError> {
|
||||
let RunExecLikeArgs {
|
||||
tool_name,
|
||||
@@ -397,17 +426,65 @@ impl ShellHandler {
|
||||
let event_ctx = ToolEventCtx::new(session.as_ref(), turn.as_ref(), &call_id, None);
|
||||
emitter.begin(event_ctx).await;
|
||||
|
||||
let exec_approval_requirement = session
|
||||
.services
|
||||
.exec_policy
|
||||
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
|
||||
command: &exec_params.command,
|
||||
approval_policy: turn.approval_policy.value(),
|
||||
sandbox_policy: turn.sandbox_policy.get(),
|
||||
sandbox_permissions: exec_params.sandbox_permissions,
|
||||
prefix_rule,
|
||||
})
|
||||
.await;
|
||||
let (effective_sandbox_policy, exec_approval_requirement, additional_permissions) =
|
||||
if turn.features.enabled(Feature::SkillShellCommandSandbox) {
|
||||
let user_shell = session.user_shell();
|
||||
let matched_skill = Self::detect_skill_shell_command(
|
||||
turn.turn_skills.outcome.as_ref(),
|
||||
user_shell.as_ref(),
|
||||
&exec_params.command,
|
||||
shell_runtime_backend,
|
||||
);
|
||||
let additional_permissions = matched_skill
|
||||
.as_ref()
|
||||
.and_then(|skill| skill.permission_profile.clone());
|
||||
let effective_sandbox_policy = matched_skill
|
||||
.as_ref()
|
||||
.and_then(|skill| skill.permissions.as_ref())
|
||||
.map(|permissions| permissions.sandbox_policy.get().clone())
|
||||
.unwrap_or_else(|| turn.sandbox_policy.get().clone());
|
||||
let exec_approval_requirement = if matched_skill.is_some() {
|
||||
ExecApprovalRequirement::NeedsApproval {
|
||||
reason: Some(SKILL_SHELL_COMMAND_APPROVAL_REASON.to_string()),
|
||||
proposed_execpolicy_amendment: None,
|
||||
}
|
||||
} else {
|
||||
session
|
||||
.services
|
||||
.exec_policy
|
||||
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
|
||||
command: &exec_params.command,
|
||||
approval_policy: turn.approval_policy.value(),
|
||||
sandbox_policy: &effective_sandbox_policy,
|
||||
sandbox_permissions: exec_params.sandbox_permissions,
|
||||
prefix_rule,
|
||||
})
|
||||
.await
|
||||
};
|
||||
(
|
||||
effective_sandbox_policy,
|
||||
exec_approval_requirement,
|
||||
additional_permissions,
|
||||
)
|
||||
} else {
|
||||
let effective_sandbox_policy = turn.sandbox_policy.get().clone();
|
||||
let exec_approval_requirement = session
|
||||
.services
|
||||
.exec_policy
|
||||
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
|
||||
command: &exec_params.command,
|
||||
approval_policy: turn.approval_policy.value(),
|
||||
sandbox_policy: &effective_sandbox_policy,
|
||||
sandbox_permissions: exec_params.sandbox_permissions,
|
||||
prefix_rule,
|
||||
})
|
||||
.await;
|
||||
(
|
||||
effective_sandbox_policy,
|
||||
exec_approval_requirement,
|
||||
normalized_additional_permissions,
|
||||
)
|
||||
};
|
||||
|
||||
let req = ShellRequest {
|
||||
command: exec_params.command.clone(),
|
||||
@@ -417,9 +494,10 @@ impl ShellHandler {
|
||||
explicit_env_overrides,
|
||||
network: exec_params.network.clone(),
|
||||
sandbox_permissions: exec_params.sandbox_permissions,
|
||||
additional_permissions: normalized_additional_permissions,
|
||||
additional_permissions,
|
||||
justification: exec_params.justification.clone(),
|
||||
exec_approval_requirement,
|
||||
effective_sandbox_policy,
|
||||
};
|
||||
let mut orchestrator = ToolOrchestrator::new();
|
||||
let mut runtime = {
|
||||
@@ -458,10 +536,12 @@ impl ShellHandler {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use codex_protocol::models::ShellCommandToolCallParams;
|
||||
use codex_protocol::protocol::SkillScope;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use crate::codex::make_session_and_context;
|
||||
@@ -473,9 +553,28 @@ mod tests {
|
||||
use crate::shell::Shell;
|
||||
use crate::shell::ShellType;
|
||||
use crate::shell_snapshot::ShellSnapshot;
|
||||
use crate::skills::SkillLoadOutcome;
|
||||
use crate::skills::SkillMetadata;
|
||||
use crate::tools::handlers::ShellCommandHandler;
|
||||
use crate::tools::handlers::ShellHandler;
|
||||
use crate::tools::runtimes::shell::ShellRuntimeBackend;
|
||||
use tokio::sync::watch;
|
||||
|
||||
fn test_skill_metadata(path_to_skills_md: PathBuf) -> SkillMetadata {
|
||||
SkillMetadata {
|
||||
name: "test-skill".to_string(),
|
||||
description: "test".to_string(),
|
||||
short_description: None,
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md,
|
||||
scope: SkillScope::User,
|
||||
}
|
||||
}
|
||||
|
||||
/// The logic for is_known_safe_command() has heuristics for known shells,
|
||||
/// so we must ensure the commands generated by [ShellCommandHandler] can be
|
||||
/// recognized as safe if the `command` is safe.
|
||||
@@ -638,4 +737,88 @@ mod tests {
|
||||
"unexpected error: {err}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_skill_shell_command_matches_direct_absolute_executable() {
|
||||
let shell = Shell {
|
||||
shell_type: ShellType::Zsh,
|
||||
shell_path: PathBuf::from("/bin/zsh"),
|
||||
shell_snapshot: crate::shell::empty_shell_snapshot_receiver(),
|
||||
};
|
||||
let skill = test_skill_metadata(PathBuf::from("/tmp/skill-test/SKILL.md"));
|
||||
let outcome = SkillLoadOutcome {
|
||||
implicit_skills_by_scripts_dir: Arc::new(HashMap::from([(
|
||||
PathBuf::from("/tmp/skill-test/scripts"),
|
||||
skill,
|
||||
)])),
|
||||
..Default::default()
|
||||
};
|
||||
let command = shell.derive_exec_args("/tmp/skill-test/scripts/run-tool", true);
|
||||
|
||||
let found = ShellHandler::detect_skill_shell_command(
|
||||
&outcome,
|
||||
&shell,
|
||||
&command,
|
||||
ShellRuntimeBackend::ShellCommandClassic,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
found.map(|skill| skill.name),
|
||||
Some("test-skill".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_skill_shell_command_ignores_multi_command_shell_script() {
|
||||
let shell = Shell {
|
||||
shell_type: ShellType::Zsh,
|
||||
shell_path: PathBuf::from("/bin/zsh"),
|
||||
shell_snapshot: crate::shell::empty_shell_snapshot_receiver(),
|
||||
};
|
||||
let skill = test_skill_metadata(PathBuf::from("/tmp/skill-test/SKILL.md"));
|
||||
let outcome = SkillLoadOutcome {
|
||||
implicit_skills_by_scripts_dir: Arc::new(HashMap::from([(
|
||||
PathBuf::from("/tmp/skill-test/scripts"),
|
||||
skill,
|
||||
)])),
|
||||
..Default::default()
|
||||
};
|
||||
let command = shell.derive_exec_args("/tmp/skill-test/scripts/run-tool && echo done", true);
|
||||
|
||||
let found = ShellHandler::detect_skill_shell_command(
|
||||
&outcome,
|
||||
&shell,
|
||||
&command,
|
||||
ShellRuntimeBackend::ShellCommandClassic,
|
||||
);
|
||||
|
||||
assert_eq!(found, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_skill_shell_command_requires_classic_backend() {
|
||||
let shell = Shell {
|
||||
shell_type: ShellType::Zsh,
|
||||
shell_path: PathBuf::from("/bin/zsh"),
|
||||
shell_snapshot: crate::shell::empty_shell_snapshot_receiver(),
|
||||
};
|
||||
let skill = test_skill_metadata(PathBuf::from("/tmp/skill-test/SKILL.md"));
|
||||
let outcome = SkillLoadOutcome {
|
||||
implicit_skills_by_scripts_dir: Arc::new(HashMap::from([(
|
||||
PathBuf::from("/tmp/skill-test/scripts"),
|
||||
skill,
|
||||
)])),
|
||||
..Default::default()
|
||||
};
|
||||
let command = shell.derive_exec_args("/tmp/skill-test/scripts/run-tool", true);
|
||||
|
||||
let found = ShellHandler::detect_skill_shell_command(
|
||||
&outcome,
|
||||
&shell,
|
||||
&command,
|
||||
ShellRuntimeBackend::ShellCommandZshFork,
|
||||
);
|
||||
|
||||
assert_eq!(found, None);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use crate::exec_policy::ExecApprovalRequest;
|
||||
use crate::features::Feature;
|
||||
use crate::function_tool::FunctionCallError;
|
||||
use crate::is_safe_command::is_known_safe_command;
|
||||
@@ -6,6 +7,9 @@ use crate::protocol::TerminalInteractionEvent;
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
use crate::shell::Shell;
|
||||
use crate::shell::get_shell_by_model_provided_path;
|
||||
use crate::skills::SkillLoadOutcome;
|
||||
use crate::skills::SkillMetadata;
|
||||
use crate::skills::detect_implicit_skill_executable_invocation_for_tokens;
|
||||
use crate::skills::maybe_emit_implicit_skill_invocation;
|
||||
use crate::tools::context::ToolInvocation;
|
||||
use crate::tools::context::ToolOutput;
|
||||
@@ -15,6 +19,7 @@ use crate::tools::handlers::normalize_and_validate_additional_permissions;
|
||||
use crate::tools::handlers::parse_arguments;
|
||||
use crate::tools::registry::ToolHandler;
|
||||
use crate::tools::registry::ToolKind;
|
||||
use crate::tools::sandboxing::ExecApprovalRequirement;
|
||||
use crate::unified_exec::ExecCommandRequest;
|
||||
use crate::unified_exec::UnifiedExecContext;
|
||||
use crate::unified_exec::UnifiedExecProcessManager;
|
||||
@@ -29,6 +34,9 @@ use std::sync::Arc;
|
||||
|
||||
pub struct UnifiedExecHandler;
|
||||
|
||||
const SKILL_EXEC_COMMAND_APPROVAL_REASON: &str =
|
||||
"This command runs a skill script and requires approval.";
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub(crate) struct ExecCommandArgs {
|
||||
cmd: String,
|
||||
@@ -78,6 +86,31 @@ fn default_tty() -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
impl UnifiedExecHandler {
|
||||
fn detect_skill_exec_command(
|
||||
outcome: &SkillLoadOutcome,
|
||||
command: &[String],
|
||||
) -> Option<SkillMetadata> {
|
||||
let [shell, ..] = command else {
|
||||
return None;
|
||||
};
|
||||
if !matches!(
|
||||
crate::shell_detect::detect_shell_type(&PathBuf::from(shell)),
|
||||
Some(crate::shell::ShellType::Zsh)
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let commands = crate::bash::parse_shell_lc_plain_commands(command)?;
|
||||
let [inner_command] = commands.as_slice() else {
|
||||
return None;
|
||||
};
|
||||
let (skill, _path) =
|
||||
detect_implicit_skill_executable_invocation_for_tokens(outcome, inner_command)?;
|
||||
Some(skill)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl ToolHandler for UnifiedExecHandler {
|
||||
fn kind(&self) -> ToolKind {
|
||||
@@ -215,6 +248,69 @@ impl ToolHandler for UnifiedExecHandler {
|
||||
return Ok(output);
|
||||
}
|
||||
|
||||
let (effective_sandbox_policy, exec_approval_requirement, additional_permissions) =
|
||||
if turn.features.enabled(Feature::SkillShellCommandSandbox) {
|
||||
let matched_skill = Self::detect_skill_exec_command(
|
||||
turn.turn_skills.outcome.as_ref(),
|
||||
&command,
|
||||
);
|
||||
let additional_permissions = matched_skill
|
||||
.as_ref()
|
||||
.and_then(|skill| skill.permission_profile.clone());
|
||||
let effective_sandbox_policy = matched_skill
|
||||
.as_ref()
|
||||
.and_then(|skill| skill.permissions.as_ref())
|
||||
.map(|permissions| permissions.sandbox_policy.get().clone())
|
||||
.unwrap_or_else(|| turn.sandbox_policy.get().clone());
|
||||
let exec_approval_requirement = if matched_skill.is_some() {
|
||||
ExecApprovalRequirement::NeedsApproval {
|
||||
reason: Some(SKILL_EXEC_COMMAND_APPROVAL_REASON.to_string()),
|
||||
proposed_execpolicy_amendment: None,
|
||||
}
|
||||
} else {
|
||||
session
|
||||
.services
|
||||
.exec_policy
|
||||
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
|
||||
command: &command,
|
||||
approval_policy: turn.approval_policy.value(),
|
||||
sandbox_policy: &effective_sandbox_policy,
|
||||
sandbox_permissions,
|
||||
prefix_rule: prefix_rule.clone(),
|
||||
})
|
||||
.await
|
||||
};
|
||||
(
|
||||
effective_sandbox_policy,
|
||||
exec_approval_requirement,
|
||||
additional_permissions,
|
||||
)
|
||||
} else {
|
||||
let effective_sandbox_policy = turn.sandbox_policy.get().clone();
|
||||
let exec_approval_requirement = session
|
||||
.services
|
||||
.exec_policy
|
||||
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
|
||||
command: &command,
|
||||
approval_policy: turn.approval_policy.value(),
|
||||
sandbox_policy: &effective_sandbox_policy,
|
||||
sandbox_permissions,
|
||||
prefix_rule: prefix_rule.clone(),
|
||||
})
|
||||
.await;
|
||||
(
|
||||
effective_sandbox_policy,
|
||||
exec_approval_requirement,
|
||||
normalized_additional_permissions,
|
||||
)
|
||||
};
|
||||
|
||||
tracing::debug!(
|
||||
?effective_sandbox_policy,
|
||||
?exec_approval_requirement,
|
||||
"resolved exec command sandbox policy and approval requirement"
|
||||
);
|
||||
|
||||
manager
|
||||
.exec_command(
|
||||
ExecCommandRequest {
|
||||
@@ -226,9 +322,10 @@ impl ToolHandler for UnifiedExecHandler {
|
||||
network: context.turn.network.clone(),
|
||||
tty,
|
||||
sandbox_permissions,
|
||||
additional_permissions: normalized_additional_permissions,
|
||||
additional_permissions,
|
||||
justification,
|
||||
prefix_rule,
|
||||
effective_sandbox_policy,
|
||||
exec_approval_requirement,
|
||||
},
|
||||
&context,
|
||||
)
|
||||
@@ -335,10 +432,28 @@ fn format_response(response: &UnifiedExecResponse) -> String {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::shell::ShellType;
|
||||
use crate::shell::default_user_shell;
|
||||
use pretty_assertions::assert_eq;
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
fn test_skill_metadata(path_to_skills_md: PathBuf) -> SkillMetadata {
|
||||
SkillMetadata {
|
||||
name: "test-skill".to_string(),
|
||||
description: "test".to_string(),
|
||||
short_description: None,
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md,
|
||||
scope: codex_protocol::protocol::SkillScope::Repo,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_command_uses_default_shell_when_unspecified() -> anyhow::Result<()> {
|
||||
let json = r#"{"cmd": "echo hello"}"#;
|
||||
@@ -420,4 +535,99 @@ mod tests {
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_skill_exec_command_matches_direct_absolute_executable() {
|
||||
let shell = Shell {
|
||||
shell_type: ShellType::Zsh,
|
||||
shell_path: PathBuf::from("/bin/zsh"),
|
||||
shell_snapshot: crate::shell::empty_shell_snapshot_receiver(),
|
||||
};
|
||||
let skill = test_skill_metadata(PathBuf::from("/tmp/skill-test/SKILL.md"));
|
||||
let outcome = SkillLoadOutcome {
|
||||
implicit_skills_by_scripts_dir: Arc::new(HashMap::from([(
|
||||
PathBuf::from("/tmp/skill-test/scripts"),
|
||||
skill,
|
||||
)])),
|
||||
..Default::default()
|
||||
};
|
||||
let command = shell.derive_exec_args("/tmp/skill-test/scripts/run-tool", true);
|
||||
|
||||
let found = UnifiedExecHandler::detect_skill_exec_command(&outcome, &command);
|
||||
|
||||
assert_eq!(
|
||||
found.map(|skill| skill.name),
|
||||
Some("test-skill".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_skill_exec_command_ignores_multi_command_shell_script() {
|
||||
let shell = Shell {
|
||||
shell_type: ShellType::Zsh,
|
||||
shell_path: PathBuf::from("/bin/zsh"),
|
||||
shell_snapshot: crate::shell::empty_shell_snapshot_receiver(),
|
||||
};
|
||||
let skill = test_skill_metadata(PathBuf::from("/tmp/skill-test/SKILL.md"));
|
||||
let outcome = SkillLoadOutcome {
|
||||
implicit_skills_by_scripts_dir: Arc::new(HashMap::from([(
|
||||
PathBuf::from("/tmp/skill-test/scripts"),
|
||||
skill,
|
||||
)])),
|
||||
..Default::default()
|
||||
};
|
||||
let command = shell.derive_exec_args("/tmp/skill-test/scripts/run-tool && echo done", true);
|
||||
|
||||
let found = UnifiedExecHandler::detect_skill_exec_command(&outcome, &command);
|
||||
|
||||
assert_eq!(found, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_skill_exec_command_requires_absolute_executable() {
|
||||
let shell = Shell {
|
||||
shell_type: ShellType::Zsh,
|
||||
shell_path: PathBuf::from("/bin/zsh"),
|
||||
shell_snapshot: crate::shell::empty_shell_snapshot_receiver(),
|
||||
};
|
||||
let skill = test_skill_metadata(PathBuf::from("/tmp/skill-test/SKILL.md"));
|
||||
let outcome = SkillLoadOutcome {
|
||||
implicit_skills_by_scripts_dir: Arc::new(HashMap::from([(
|
||||
PathBuf::from("/tmp/skill-test/scripts"),
|
||||
skill,
|
||||
)])),
|
||||
..Default::default()
|
||||
};
|
||||
let command = shell.derive_exec_args("scripts/run-tool", true);
|
||||
|
||||
let found = UnifiedExecHandler::detect_skill_exec_command(&outcome, &command);
|
||||
|
||||
assert_eq!(found, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_skill_exec_command_requires_zsh_shell() {
|
||||
let skill = test_skill_metadata(PathBuf::from("/tmp/skill-test/SKILL.md"));
|
||||
let outcome = SkillLoadOutcome {
|
||||
implicit_skills_by_scripts_dir: Arc::new(HashMap::from([(
|
||||
PathBuf::from("/tmp/skill-test/scripts"),
|
||||
skill,
|
||||
)])),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
for (shell_type, shell_path) in [(ShellType::Bash, "/bin/bash"), (ShellType::Sh, "/bin/sh")]
|
||||
{
|
||||
let shell = Shell {
|
||||
shell_type,
|
||||
shell_path: PathBuf::from(shell_path),
|
||||
shell_snapshot: crate::shell::empty_shell_snapshot_receiver(),
|
||||
};
|
||||
let command = shell.derive_exec_args("/tmp/skill-test/scripts/run-tool", true);
|
||||
|
||||
let found = UnifiedExecHandler::detect_skill_exec_command(&outcome, &command);
|
||||
|
||||
assert_eq!(found, None);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,12 +113,13 @@ impl ToolOrchestrator {
|
||||
let otel_ci = &tool_ctx.call_id;
|
||||
let otel_user = ToolDecisionSource::User;
|
||||
let otel_cfg = ToolDecisionSource::Config;
|
||||
let effective_sandbox_policy = tool.sandbox_policy(req, turn_ctx);
|
||||
|
||||
// 1) Approval
|
||||
let mut already_approved = false;
|
||||
|
||||
let requirement = tool.exec_approval_requirement(req).unwrap_or_else(|| {
|
||||
default_exec_approval_requirement(approval_policy, &turn_ctx.sandbox_policy)
|
||||
default_exec_approval_requirement(approval_policy, effective_sandbox_policy)
|
||||
});
|
||||
match requirement {
|
||||
ExecApprovalRequirement::Skip { .. } => {
|
||||
@@ -169,7 +170,7 @@ impl ToolOrchestrator {
|
||||
let initial_sandbox = match tool.sandbox_mode_for_first_attempt(req) {
|
||||
SandboxOverride::BypassSandboxFirstAttempt => crate::exec::SandboxType::None,
|
||||
SandboxOverride::NoOverride => self.sandbox.select_initial(
|
||||
&turn_ctx.sandbox_policy,
|
||||
effective_sandbox_policy,
|
||||
tool.sandbox_preference(),
|
||||
turn_ctx.windows_sandbox_level,
|
||||
has_managed_network_requirements,
|
||||
@@ -181,7 +182,7 @@ impl ToolOrchestrator {
|
||||
let use_linux_sandbox_bwrap = turn_ctx.features.enabled(Feature::UseLinuxSandboxBwrap);
|
||||
let initial_attempt = SandboxAttempt {
|
||||
sandbox: initial_sandbox,
|
||||
policy: &turn_ctx.sandbox_policy,
|
||||
policy: effective_sandbox_policy,
|
||||
enforce_managed_network: has_managed_network_requirements,
|
||||
manager: &self.sandbox,
|
||||
sandbox_cwd: &turn_ctx.cwd,
|
||||
@@ -238,7 +239,7 @@ impl ToolOrchestrator {
|
||||
&& matches!(
|
||||
default_exec_approval_requirement(
|
||||
approval_policy,
|
||||
&turn_ctx.sandbox_policy
|
||||
effective_sandbox_policy
|
||||
),
|
||||
ExecApprovalRequirement::NeedsApproval { .. }
|
||||
);
|
||||
@@ -295,7 +296,7 @@ impl ToolOrchestrator {
|
||||
|
||||
let escalated_attempt = SandboxAttempt {
|
||||
sandbox: crate::exec::SandboxType::None,
|
||||
policy: &turn_ctx.sandbox_policy,
|
||||
policy: effective_sandbox_policy,
|
||||
enforce_managed_network: has_managed_network_requirements,
|
||||
manager: &self.sandbox,
|
||||
sandbox_cwd: &turn_ctx.cwd,
|
||||
|
||||
@@ -11,6 +11,7 @@ use crate::command_canonicalization::canonicalize_command_for_approval;
|
||||
use crate::exec::ExecToolCallOutput;
|
||||
use crate::features::Feature;
|
||||
use crate::powershell::prefix_powershell_script_with_utf8;
|
||||
use crate::protocol::SandboxPolicy;
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
use crate::sandboxing::execute_env;
|
||||
use crate::shell::ShellType;
|
||||
@@ -49,6 +50,7 @@ pub struct ShellRequest {
|
||||
pub additional_permissions: Option<PermissionProfile>,
|
||||
pub justification: Option<String>,
|
||||
pub exec_approval_requirement: ExecApprovalRequirement,
|
||||
pub effective_sandbox_policy: SandboxPolicy,
|
||||
}
|
||||
|
||||
/// Selects `ShellRuntime` behavior for different callers.
|
||||
@@ -180,6 +182,14 @@ impl Approvable<ShellRequest> for ShellRuntime {
|
||||
}
|
||||
|
||||
impl ToolRuntime<ShellRequest, ExecToolCallOutput> for ShellRuntime {
|
||||
fn sandbox_policy<'a>(
|
||||
&self,
|
||||
req: &'a ShellRequest,
|
||||
_turn_ctx: &'a crate::codex::TurnContext,
|
||||
) -> &'a SandboxPolicy {
|
||||
&req.effective_sandbox_policy
|
||||
}
|
||||
|
||||
fn network_approval_spec(
|
||||
&self,
|
||||
req: &ShellRequest,
|
||||
|
||||
@@ -10,6 +10,7 @@ use crate::error::SandboxErr;
|
||||
use crate::exec::ExecExpiration;
|
||||
use crate::features::Feature;
|
||||
use crate::powershell::prefix_powershell_script_with_utf8;
|
||||
use crate::protocol::SandboxPolicy;
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
use crate::shell::ShellType;
|
||||
use crate::tools::network_approval::NetworkApprovalMode;
|
||||
@@ -49,6 +50,7 @@ pub struct UnifiedExecRequest {
|
||||
pub sandbox_permissions: SandboxPermissions,
|
||||
pub additional_permissions: Option<PermissionProfile>,
|
||||
pub justification: Option<String>,
|
||||
pub effective_sandbox_policy: SandboxPolicy,
|
||||
pub exec_approval_requirement: ExecApprovalRequirement,
|
||||
}
|
||||
|
||||
@@ -144,6 +146,14 @@ impl Approvable<UnifiedExecRequest> for UnifiedExecRuntime<'_> {
|
||||
}
|
||||
|
||||
impl<'a> ToolRuntime<UnifiedExecRequest, UnifiedExecProcess> for UnifiedExecRuntime<'a> {
|
||||
fn sandbox_policy<'b>(
|
||||
&self,
|
||||
req: &'b UnifiedExecRequest,
|
||||
_turn_ctx: &'b crate::codex::TurnContext,
|
||||
) -> &'b SandboxPolicy {
|
||||
&req.effective_sandbox_policy
|
||||
}
|
||||
|
||||
fn network_approval_spec(
|
||||
&self,
|
||||
req: &UnifiedExecRequest,
|
||||
|
||||
@@ -303,6 +303,10 @@ pub(crate) enum ToolError {
|
||||
}
|
||||
|
||||
pub(crate) trait ToolRuntime<Req, Out>: Approvable<Req> + Sandboxable {
|
||||
fn sandbox_policy<'a>(&self, _req: &'a Req, turn_ctx: &'a TurnContext) -> &'a SandboxPolicy {
|
||||
turn_ctx.sandbox_policy.get()
|
||||
}
|
||||
|
||||
fn network_approval_spec(&self, _req: &Req, _ctx: &ToolCtx) -> Option<NetworkApprovalSpec> {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -36,7 +36,9 @@ use tokio::sync::Mutex;
|
||||
|
||||
use crate::codex::Session;
|
||||
use crate::codex::TurnContext;
|
||||
use crate::protocol::SandboxPolicy;
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
use crate::tools::sandboxing::ExecApprovalRequirement;
|
||||
|
||||
mod async_watcher;
|
||||
mod errors;
|
||||
@@ -92,7 +94,8 @@ pub(crate) struct ExecCommandRequest {
|
||||
pub sandbox_permissions: SandboxPermissions,
|
||||
pub additional_permissions: Option<PermissionProfile>,
|
||||
pub justification: Option<String>,
|
||||
pub prefix_rule: Option<Vec<String>>,
|
||||
pub effective_sandbox_policy: SandboxPolicy,
|
||||
pub exec_approval_requirement: ExecApprovalRequirement,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@@ -233,7 +236,12 @@ mod tests {
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
additional_permissions: None,
|
||||
justification: None,
|
||||
prefix_rule: None,
|
||||
effective_sandbox_policy: turn.sandbox_policy.get().clone(),
|
||||
exec_approval_requirement:
|
||||
crate::tools::sandboxing::ExecApprovalRequirement::Skip {
|
||||
bypass_sandbox: false,
|
||||
proposed_execpolicy_amendment: None,
|
||||
},
|
||||
},
|
||||
&context,
|
||||
)
|
||||
|
||||
@@ -13,7 +13,6 @@ use tokio::time::Instant;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
use crate::exec_env::create_env;
|
||||
use crate::exec_policy::ExecApprovalRequest;
|
||||
use crate::protocol::ExecCommandSource;
|
||||
use crate::sandboxing::ExecRequest;
|
||||
use crate::tools::events::ToolEmitter;
|
||||
@@ -570,18 +569,6 @@ impl UnifiedExecProcessManager {
|
||||
));
|
||||
let mut orchestrator = ToolOrchestrator::new();
|
||||
let mut runtime = UnifiedExecRuntime::new(self);
|
||||
let exec_approval_requirement = context
|
||||
.session
|
||||
.services
|
||||
.exec_policy
|
||||
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
|
||||
command: &request.command,
|
||||
approval_policy: context.turn.approval_policy.value(),
|
||||
sandbox_policy: context.turn.sandbox_policy.get(),
|
||||
sandbox_permissions: request.sandbox_permissions,
|
||||
prefix_rule: request.prefix_rule.clone(),
|
||||
})
|
||||
.await;
|
||||
let req = UnifiedExecToolRequest {
|
||||
command: request.command.clone(),
|
||||
cwd,
|
||||
@@ -592,7 +579,8 @@ impl UnifiedExecProcessManager {
|
||||
sandbox_permissions: request.sandbox_permissions,
|
||||
additional_permissions: request.additional_permissions.clone(),
|
||||
justification: request.justification.clone(),
|
||||
exec_approval_requirement,
|
||||
effective_sandbox_policy: request.effective_sandbox_policy.clone(),
|
||||
exec_approval_requirement: request.exec_approval_requirement.clone(),
|
||||
};
|
||||
let tool_ctx = ToolCtx {
|
||||
session: context.session.clone(),
|
||||
|
||||
@@ -1494,6 +1494,7 @@ mod tests {
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: PathBuf::from("test-skill"),
|
||||
scope: SkillScope::User,
|
||||
|
||||
@@ -190,6 +190,7 @@ fn protocol_skill_to_core(skill: &ProtocolSkillMetadata) -> SkillMetadata {
|
||||
.collect(),
|
||||
}),
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: skill.path.clone(),
|
||||
scope: skill.scope,
|
||||
|
||||
@@ -930,6 +930,7 @@ async fn submission_prefers_selected_duplicate_skill_path() {
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: repo_skill_path,
|
||||
scope: SkillScope::Repo,
|
||||
@@ -941,6 +942,7 @@ async fn submission_prefers_selected_duplicate_skill_path() {
|
||||
interface: None,
|
||||
dependencies: None,
|
||||
policy: None,
|
||||
permission_profile: None,
|
||||
permissions: None,
|
||||
path_to_skills_md: user_skill_path.clone(),
|
||||
scope: SkillScope::User,
|
||||
|
||||
Reference in New Issue
Block a user