mirror of
https://github.com/openai/codex.git
synced 2026-02-28 03:33:57 +00:00
Compare commits
2 Commits
latest-alp
...
pr13064
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f375477147 | ||
|
|
acf683899c |
@@ -13,6 +13,7 @@ use codex_execpolicy::AmendError;
|
||||
use codex_execpolicy::Decision;
|
||||
use codex_execpolicy::Error as ExecPolicyRuleError;
|
||||
use codex_execpolicy::Evaluation;
|
||||
use codex_execpolicy::MatchOptions;
|
||||
use codex_execpolicy::NetworkRuleProtocol;
|
||||
use codex_execpolicy::Policy;
|
||||
use codex_execpolicy::PolicyParser;
|
||||
@@ -221,7 +222,14 @@ impl ExecPolicyManager {
|
||||
used_complex_parsing,
|
||||
)
|
||||
};
|
||||
let evaluation = exec_policy.check_multiple(commands.iter(), &exec_policy_fallback);
|
||||
let match_options = MatchOptions {
|
||||
resolve_host_executables: true,
|
||||
};
|
||||
let evaluation = exec_policy.check_multiple_with_options(
|
||||
commands.iter(),
|
||||
&exec_policy_fallback,
|
||||
&match_options,
|
||||
);
|
||||
|
||||
let requested_amendment = derive_requested_execpolicy_amendment_from_prefix_rule(
|
||||
prefix_rule.as_ref(),
|
||||
@@ -229,6 +237,7 @@ impl ExecPolicyManager {
|
||||
exec_policy.as_ref(),
|
||||
&commands,
|
||||
&exec_policy_fallback,
|
||||
&match_options,
|
||||
);
|
||||
|
||||
match evaluation.decision {
|
||||
@@ -630,6 +639,7 @@ fn derive_requested_execpolicy_amendment_from_prefix_rule(
|
||||
exec_policy: &Policy,
|
||||
commands: &[Vec<String>],
|
||||
exec_policy_fallback: &impl Fn(&[String]) -> Decision,
|
||||
match_options: &MatchOptions,
|
||||
) -> Option<ExecPolicyAmendment> {
|
||||
let prefix_rule = prefix_rule?;
|
||||
if prefix_rule.is_empty() {
|
||||
@@ -656,6 +666,7 @@ fn derive_requested_execpolicy_amendment_from_prefix_rule(
|
||||
&amendment.command,
|
||||
commands,
|
||||
exec_policy_fallback,
|
||||
match_options,
|
||||
) {
|
||||
Some(amendment)
|
||||
} else {
|
||||
@@ -668,6 +679,7 @@ fn prefix_rule_would_approve_all_commands(
|
||||
prefix_rule: &[String],
|
||||
commands: &[Vec<String>],
|
||||
exec_policy_fallback: &impl Fn(&[String]) -> Decision,
|
||||
match_options: &MatchOptions,
|
||||
) -> bool {
|
||||
let mut policy_with_prefix_rule = exec_policy.clone();
|
||||
if policy_with_prefix_rule
|
||||
@@ -679,7 +691,7 @@ fn prefix_rule_would_approve_all_commands(
|
||||
|
||||
commands.iter().all(|command| {
|
||||
policy_with_prefix_rule
|
||||
.check(command, exec_policy_fallback)
|
||||
.check_with_options(command, exec_policy_fallback, match_options)
|
||||
.decision
|
||||
== Decision::Allow
|
||||
})
|
||||
@@ -849,6 +861,15 @@ mod tests {
|
||||
path.to_string_lossy().into_owned()
|
||||
}
|
||||
|
||||
fn host_program_path(name: &str) -> String {
|
||||
let executable_name = if cfg!(windows) {
|
||||
format!("{name}.exe")
|
||||
} else {
|
||||
name.to_string()
|
||||
};
|
||||
host_absolute_path(&["usr", "bin", &executable_name])
|
||||
}
|
||||
|
||||
fn starlark_string(value: &str) -> String {
|
||||
value.replace('\\', "\\\\").replace('"', "\\\"")
|
||||
}
|
||||
@@ -1398,6 +1419,115 @@ prefix_rule(
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn absolute_path_exec_approval_requirement_matches_host_executable_rules() {
|
||||
let git_path = host_program_path("git");
|
||||
let git_path_literal = starlark_string(&git_path);
|
||||
let policy_src = format!(
|
||||
r#"
|
||||
host_executable(name = "git", paths = ["{git_path_literal}"])
|
||||
prefix_rule(pattern=["git"], decision="allow")
|
||||
"#
|
||||
);
|
||||
let mut parser = PolicyParser::new();
|
||||
parser
|
||||
.parse("test.rules", &policy_src)
|
||||
.expect("parse policy");
|
||||
let manager = ExecPolicyManager::new(Arc::new(parser.build()));
|
||||
let command = vec![git_path, "status".to_string()];
|
||||
|
||||
let requirement = manager
|
||||
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
|
||||
command: &command,
|
||||
approval_policy: AskForApproval::UnlessTrusted,
|
||||
sandbox_policy: &SandboxPolicy::new_read_only_policy(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
requirement,
|
||||
ExecApprovalRequirement::Skip {
|
||||
bypass_sandbox: true,
|
||||
proposed_execpolicy_amendment: None,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn absolute_path_exec_approval_requirement_ignores_disallowed_host_executable_paths() {
|
||||
let allowed_git_path = host_program_path("git");
|
||||
let disallowed_git_path = host_absolute_path(&[
|
||||
"opt",
|
||||
"homebrew",
|
||||
"bin",
|
||||
if cfg!(windows) { "git.exe" } else { "git" },
|
||||
]);
|
||||
let allowed_git_path_literal = starlark_string(&allowed_git_path);
|
||||
let policy_src = format!(
|
||||
r#"
|
||||
host_executable(name = "git", paths = ["{allowed_git_path_literal}"])
|
||||
prefix_rule(pattern=["git"], decision="prompt")
|
||||
"#
|
||||
);
|
||||
let mut parser = PolicyParser::new();
|
||||
parser
|
||||
.parse("test.rules", &policy_src)
|
||||
.expect("parse policy");
|
||||
let manager = ExecPolicyManager::new(Arc::new(parser.build()));
|
||||
let command = vec![disallowed_git_path, "status".to_string()];
|
||||
|
||||
let requirement = manager
|
||||
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
|
||||
command: &command,
|
||||
approval_policy: AskForApproval::UnlessTrusted,
|
||||
sandbox_policy: &SandboxPolicy::new_read_only_policy(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
requirement,
|
||||
ExecApprovalRequirement::Skip {
|
||||
bypass_sandbox: false,
|
||||
proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(command)),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn requested_prefix_rule_can_approve_absolute_path_commands() {
|
||||
let command = vec![
|
||||
host_program_path("cargo"),
|
||||
"install".to_string(),
|
||||
"cargo-insta".to_string(),
|
||||
];
|
||||
let manager = ExecPolicyManager::default();
|
||||
|
||||
let requirement = manager
|
||||
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
|
||||
command: &command,
|
||||
approval_policy: AskForApproval::UnlessTrusted,
|
||||
sandbox_policy: &SandboxPolicy::new_read_only_policy(),
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: Some(vec!["cargo".to_string(), "install".to_string()]),
|
||||
})
|
||||
.await;
|
||||
|
||||
assert_eq!(
|
||||
requirement,
|
||||
ExecApprovalRequirement::NeedsApproval {
|
||||
reason: None,
|
||||
proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(vec![
|
||||
"cargo".to_string(),
|
||||
"install".to_string(),
|
||||
])),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn exec_approval_requirement_respects_approval_policy() {
|
||||
let policy_src = r#"prefix_rule(pattern=["rm"], decision="prompt")"#;
|
||||
@@ -1952,6 +2082,7 @@ prefix_rule(
|
||||
&Policy::empty(),
|
||||
&commands,
|
||||
&|_: &[String]| Decision::Allow,
|
||||
&MatchOptions::default(),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ use crate::tools::sandboxing::SandboxablePreference;
|
||||
use crate::tools::sandboxing::ToolCtx;
|
||||
use crate::tools::sandboxing::ToolError;
|
||||
use codex_execpolicy::Decision;
|
||||
use codex_execpolicy::Evaluation;
|
||||
use codex_execpolicy::MatchOptions;
|
||||
use codex_execpolicy::Policy;
|
||||
use codex_execpolicy::RuleMatch;
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
@@ -493,28 +495,16 @@ impl EscalationPolicy for CoreShellActionProvider {
|
||||
.await;
|
||||
}
|
||||
|
||||
let command = join_program_and_argv(program, argv);
|
||||
let (commands, used_complex_parsing) =
|
||||
if let Some(commands) = parse_shell_lc_plain_commands(&command) {
|
||||
(commands, false)
|
||||
} else if let Some(single_command) = parse_shell_lc_single_command_prefix(&command) {
|
||||
(vec![single_command], true)
|
||||
} else {
|
||||
(vec![command.clone()], false)
|
||||
};
|
||||
|
||||
let fallback = |cmd: &[String]| {
|
||||
crate::exec_policy::render_decision_for_unmatched_command(
|
||||
self.approval_policy,
|
||||
&self.sandbox_policy,
|
||||
cmd,
|
||||
self.sandbox_permissions,
|
||||
used_complex_parsing,
|
||||
)
|
||||
};
|
||||
let evaluation = {
|
||||
let policy = self.policy.read().await;
|
||||
policy.check_multiple(commands.iter(), &fallback)
|
||||
evaluate_intercepted_exec_policy(
|
||||
&policy,
|
||||
program,
|
||||
argv,
|
||||
self.approval_policy,
|
||||
&self.sandbox_policy,
|
||||
self.sandbox_permissions,
|
||||
)
|
||||
};
|
||||
// When true, means the Evaluation was due to *.rules, not the
|
||||
// fallback function.
|
||||
@@ -552,6 +542,56 @@ impl EscalationPolicy for CoreShellActionProvider {
|
||||
}
|
||||
}
|
||||
|
||||
fn evaluate_intercepted_exec_policy(
|
||||
policy: &Policy,
|
||||
program: &AbsolutePathBuf,
|
||||
argv: &[String],
|
||||
approval_policy: AskForApproval,
|
||||
sandbox_policy: &SandboxPolicy,
|
||||
sandbox_permissions: SandboxPermissions,
|
||||
) -> Evaluation {
|
||||
let (commands, used_complex_parsing) = commands_for_intercepted_exec_policy(program, argv);
|
||||
|
||||
let fallback = |cmd: &[String]| {
|
||||
crate::exec_policy::render_decision_for_unmatched_command(
|
||||
approval_policy,
|
||||
sandbox_policy,
|
||||
cmd,
|
||||
sandbox_permissions,
|
||||
used_complex_parsing,
|
||||
)
|
||||
};
|
||||
|
||||
policy.check_multiple_with_options(
|
||||
commands.iter(),
|
||||
&fallback,
|
||||
&MatchOptions {
|
||||
resolve_host_executables: true,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn commands_for_intercepted_exec_policy(
|
||||
program: &AbsolutePathBuf,
|
||||
argv: &[String],
|
||||
) -> (Vec<Vec<String>>, bool) {
|
||||
if let [_, flag, script] = argv {
|
||||
let shell_command = [
|
||||
program.to_string_lossy().to_string(),
|
||||
flag.clone(),
|
||||
script.clone(),
|
||||
];
|
||||
if let Some(commands) = parse_shell_lc_plain_commands(&shell_command) {
|
||||
return (commands, false);
|
||||
}
|
||||
if let Some(single_command) = parse_shell_lc_single_command_prefix(&shell_command) {
|
||||
return (vec![single_command], true);
|
||||
}
|
||||
}
|
||||
|
||||
(vec![join_program_and_argv(program, argv)], false)
|
||||
}
|
||||
|
||||
struct CoreShellCommandExecutor {
|
||||
command: Vec<String>,
|
||||
cwd: PathBuf,
|
||||
|
||||
@@ -2,6 +2,8 @@ use super::CoreShellActionProvider;
|
||||
#[cfg(target_os = "macos")]
|
||||
use super::CoreShellCommandExecutor;
|
||||
use super::ParsedShellCommand;
|
||||
use super::commands_for_intercepted_exec_policy;
|
||||
use super::evaluate_intercepted_exec_policy;
|
||||
use super::extract_shell_script;
|
||||
use super::join_program_and_argv;
|
||||
use super::map_exec_result;
|
||||
@@ -12,14 +14,16 @@ use crate::config::Permissions;
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::config::types::ShellEnvironmentPolicy;
|
||||
use crate::exec::SandboxType;
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::protocol::AskForApproval;
|
||||
use crate::protocol::ReadOnlyAccess;
|
||||
use crate::protocol::SandboxPolicy;
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::sandboxing::SandboxPermissions;
|
||||
#[cfg(target_os = "macos")]
|
||||
use crate::seatbelt::MACOS_PATH_TO_SEATBELT_EXECUTABLE;
|
||||
use codex_execpolicy::Decision;
|
||||
use codex_execpolicy::Evaluation;
|
||||
use codex_execpolicy::PolicyParser;
|
||||
use codex_execpolicy::RuleMatch;
|
||||
#[cfg(target_os = "macos")]
|
||||
use codex_protocol::config_types::WindowsSandboxLevel;
|
||||
use codex_protocol::models::FileSystemPermissions;
|
||||
@@ -36,8 +40,25 @@ use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
#[cfg(target_os = "macos")]
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
fn host_absolute_path(segments: &[&str]) -> String {
|
||||
let mut path = if cfg!(windows) {
|
||||
PathBuf::from(r"C:\")
|
||||
} else {
|
||||
PathBuf::from("/")
|
||||
};
|
||||
for segment in segments {
|
||||
path.push(segment);
|
||||
}
|
||||
path.to_string_lossy().into_owned()
|
||||
}
|
||||
|
||||
fn starlark_string(value: &str) -> String {
|
||||
value.replace('\\', "\\\\").replace('"', "\\\"")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn extract_shell_script_preserves_login_flag() {
|
||||
assert_eq!(
|
||||
@@ -126,6 +147,24 @@ fn join_program_and_argv_replaces_original_argv_zero() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn commands_for_intercepted_exec_policy_uses_program_path_for_shell_wrapper_parsing() {
|
||||
let program = AbsolutePathBuf::try_from(host_absolute_path(&["bin", "bash"])).unwrap();
|
||||
assert_eq!(
|
||||
commands_for_intercepted_exec_policy(
|
||||
&program,
|
||||
&["not-bash".into(), "-lc".into(), "git status && pwd".into()],
|
||||
),
|
||||
(
|
||||
vec![
|
||||
vec!["git".to_string(), "status".to_string()],
|
||||
vec!["pwd".to_string()],
|
||||
],
|
||||
false,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn map_exec_result_preserves_stdout_and_stderr() {
|
||||
let out = map_exec_result(
|
||||
@@ -203,6 +242,84 @@ fn shell_request_escalation_execution_is_explicit() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intercepted_exec_policy_uses_host_executable_mappings() {
|
||||
let git_path = host_absolute_path(&["usr", "bin", "git"]);
|
||||
let git_path_literal = starlark_string(&git_path);
|
||||
let policy_src = format!(
|
||||
r#"
|
||||
prefix_rule(pattern = ["git", "status"], decision = "prompt")
|
||||
host_executable(name = "git", paths = ["{git_path_literal}"])
|
||||
"#
|
||||
);
|
||||
let mut parser = PolicyParser::new();
|
||||
parser.parse("test.rules", &policy_src).unwrap();
|
||||
let policy = parser.build();
|
||||
let program = AbsolutePathBuf::try_from(git_path).unwrap();
|
||||
|
||||
let evaluation = evaluate_intercepted_exec_policy(
|
||||
&policy,
|
||||
&program,
|
||||
&["git".to_string(), "status".to_string()],
|
||||
AskForApproval::OnRequest,
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
SandboxPermissions::UseDefault,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
evaluation,
|
||||
Evaluation {
|
||||
decision: Decision::Prompt,
|
||||
matched_rules: vec![RuleMatch::PrefixRuleMatch {
|
||||
matched_prefix: vec!["git".to_string(), "status".to_string()],
|
||||
decision: Decision::Prompt,
|
||||
resolved_program: Some(program),
|
||||
justification: None,
|
||||
}],
|
||||
}
|
||||
);
|
||||
assert!(CoreShellActionProvider::decision_driven_by_policy(
|
||||
&evaluation.matched_rules,
|
||||
evaluation.decision
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn intercepted_exec_policy_rejects_disallowed_host_executable_mapping() {
|
||||
let allowed_git = host_absolute_path(&["usr", "bin", "git"]);
|
||||
let other_git = host_absolute_path(&["opt", "homebrew", "bin", "git"]);
|
||||
let allowed_git_literal = starlark_string(&allowed_git);
|
||||
let policy_src = format!(
|
||||
r#"
|
||||
prefix_rule(pattern = ["git", "status"], decision = "prompt")
|
||||
host_executable(name = "git", paths = ["{allowed_git_literal}"])
|
||||
"#
|
||||
);
|
||||
let mut parser = PolicyParser::new();
|
||||
parser.parse("test.rules", &policy_src).unwrap();
|
||||
let policy = parser.build();
|
||||
let program = AbsolutePathBuf::try_from(other_git.clone()).unwrap();
|
||||
|
||||
let evaluation = evaluate_intercepted_exec_policy(
|
||||
&policy,
|
||||
&program,
|
||||
&["git".to_string(), "status".to_string()],
|
||||
AskForApproval::OnRequest,
|
||||
&SandboxPolicy::new_read_only_policy(),
|
||||
SandboxPermissions::UseDefault,
|
||||
);
|
||||
|
||||
assert!(matches!(
|
||||
evaluation.matched_rules.as_slice(),
|
||||
[RuleMatch::HeuristicsRuleMatch { command, .. }]
|
||||
if command == &vec![other_git, "status".to_string()]
|
||||
));
|
||||
assert!(!CoreShellActionProvider::decision_driven_by_policy(
|
||||
&evaluation.matched_rules,
|
||||
evaluation.decision
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
#[tokio::test]
|
||||
async fn prepare_escalated_exec_turn_default_preserves_macos_seatbelt_extensions() {
|
||||
|
||||
Reference in New Issue
Block a user