mirror of
https://github.com/openai/codex.git
synced 2026-10-31 19:28:49 +00:00
feat: support permissions-scoped exec rules
This commit is contained in:
@@ -3447,6 +3447,7 @@ mod requirements_exec_policy_tests {
|
||||
use codex_config::RequirementsExecPolicyToml;
|
||||
use codex_execpolicy::Decision;
|
||||
use codex_execpolicy::Evaluation;
|
||||
use codex_execpolicy::MatchOptions;
|
||||
use codex_execpolicy::RuleMatch;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use pretty_assertions::assert_eq;
|
||||
@@ -3504,6 +3505,7 @@ prefix_rules = [
|
||||
}],
|
||||
decision: Some(RequirementsExecPolicyDecisionToml::Forbidden),
|
||||
justification: None,
|
||||
permissions: None,
|
||||
}],
|
||||
}
|
||||
);
|
||||
@@ -3533,6 +3535,7 @@ prefix_rules = [
|
||||
}],
|
||||
decision: Some(RequirementsExecPolicyDecisionToml::Forbidden),
|
||||
justification: None,
|
||||
permissions: None,
|
||||
},
|
||||
RequirementsExecPolicyPrefixRuleToml {
|
||||
pattern: vec![
|
||||
@@ -3547,6 +3550,7 @@ prefix_rules = [
|
||||
],
|
||||
decision: Some(RequirementsExecPolicyDecisionToml::Prompt),
|
||||
justification: Some("review changes before push or commit".to_string()),
|
||||
permissions: None,
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -3582,6 +3586,58 @@ prefix_rules = [
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn converts_rules_toml_permissions_scope() -> anyhow::Result<()> {
|
||||
let toml_str = r#"
|
||||
prefix_rules = [
|
||||
{ pattern = [{ token = "git" }, { token = "status" }], decision = "prompt", permissions = "strict" },
|
||||
]
|
||||
"#;
|
||||
|
||||
let parsed: RequirementsExecPolicyToml = from_str(toml_str)?;
|
||||
let policy = parsed.to_policy()?;
|
||||
let command = tokens(&["git", "status"]);
|
||||
|
||||
assert_eq!(
|
||||
policy.check_with_options(
|
||||
&command,
|
||||
&|_| Decision::Allow,
|
||||
&MatchOptions {
|
||||
active_permission_profile: Some("strict".to_string()),
|
||||
..MatchOptions::default()
|
||||
}
|
||||
),
|
||||
Evaluation {
|
||||
decision: Decision::Prompt,
|
||||
matched_rules: vec![RuleMatch::PrefixRuleMatch {
|
||||
matched_prefix: command.clone(),
|
||||
decision: Decision::Prompt,
|
||||
resolved_program: None,
|
||||
justification: None,
|
||||
}],
|
||||
}
|
||||
);
|
||||
assert_eq!(
|
||||
policy.check_with_options(
|
||||
&command,
|
||||
&|_| Decision::Allow,
|
||||
&MatchOptions {
|
||||
active_permission_profile: Some("relaxed".to_string()),
|
||||
..MatchOptions::default()
|
||||
}
|
||||
),
|
||||
Evaluation {
|
||||
decision: Decision::Allow,
|
||||
matched_rules: vec![RuleMatch::HeuristicsRuleMatch {
|
||||
command,
|
||||
decision: Decision::Allow,
|
||||
}],
|
||||
}
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn head_any_of_expands_into_multiple_program_rules() -> anyhow::Result<()> {
|
||||
let toml_str = r#"
|
||||
|
||||
@@ -6,8 +6,11 @@ use std::sync::Arc;
|
||||
use arc_swap::ArcSwap;
|
||||
|
||||
use codex_app_server_protocol::ConfigLayerSource;
|
||||
use codex_config::CONFIG_TOML_FILE;
|
||||
use codex_config::ConfigLayerEntry;
|
||||
use codex_config::ConfigLayerStack;
|
||||
use codex_config::ConfigLayerStackOrdering;
|
||||
use codex_config::RequirementsExecPolicyParseError;
|
||||
use codex_execpolicy::AmendError;
|
||||
use codex_execpolicy::Decision;
|
||||
use codex_execpolicy::Error as ExecPolicyRuleError;
|
||||
@@ -135,7 +138,9 @@ struct ExecPolicyCommands {
|
||||
}
|
||||
|
||||
pub(crate) fn child_uses_parent_exec_policy(parent_config: &Config, child_config: &Config) -> bool {
|
||||
fn exec_policy_config_folders(config: &Config) -> Vec<AbsolutePathBuf> {
|
||||
fn exec_policy_layer_inputs(
|
||||
config: &Config,
|
||||
) -> Vec<(Option<AbsolutePathBuf>, Option<toml::Value>)> {
|
||||
config
|
||||
.config_layer_stack
|
||||
.get_layers(
|
||||
@@ -143,11 +148,18 @@ pub(crate) fn child_uses_parent_exec_policy(parent_config: &Config, child_config
|
||||
/*include_disabled*/ false,
|
||||
)
|
||||
.into_iter()
|
||||
.filter_map(codex_config::ConfigLayerEntry::config_folder)
|
||||
.filter(|layer| {
|
||||
!should_ignore_layer_exec_policy_rules(&config.config_layer_stack, layer)
|
||||
})
|
||||
.filter_map(|layer| {
|
||||
let config_folder = layer.config_folder();
|
||||
let rules = layer.config.get("rules").cloned();
|
||||
(config_folder.is_some() || rules.is_some()).then_some((config_folder, rules))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
exec_policy_config_folders(parent_config) == exec_policy_config_folders(child_config)
|
||||
exec_policy_layer_inputs(parent_config) == exec_policy_layer_inputs(child_config)
|
||||
&& parent_config
|
||||
.config_layer_stack
|
||||
.ignore_user_and_project_exec_policy_rules()
|
||||
@@ -215,6 +227,18 @@ pub enum ExecPolicyError {
|
||||
path: String,
|
||||
source: codex_execpolicy::Error,
|
||||
},
|
||||
|
||||
#[error("failed to parse rules in {path}: {source}")]
|
||||
ParseConfigRules {
|
||||
path: String,
|
||||
source: toml::de::Error,
|
||||
},
|
||||
|
||||
#[error("failed to load rules in {path}: {source}")]
|
||||
InvalidConfigRules {
|
||||
path: String,
|
||||
source: RequirementsExecPolicyParseError,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
@@ -241,6 +265,7 @@ pub(crate) struct ExecApprovalRequest<'a> {
|
||||
pub(crate) command: &'a [String],
|
||||
pub(crate) approval_policy: AskForApproval,
|
||||
pub(crate) permission_profile: PermissionProfile,
|
||||
pub(crate) active_permission_profile: Option<String>,
|
||||
pub(crate) windows_sandbox_level: WindowsSandboxLevel,
|
||||
pub(crate) sandbox_permissions: SandboxPermissions,
|
||||
pub(crate) prefix_rule: Option<Vec<String>>,
|
||||
@@ -275,6 +300,7 @@ impl ExecPolicyManager {
|
||||
command,
|
||||
approval_policy,
|
||||
permission_profile,
|
||||
active_permission_profile,
|
||||
windows_sandbox_level,
|
||||
sandbox_permissions,
|
||||
prefix_rule,
|
||||
@@ -304,6 +330,7 @@ impl ExecPolicyManager {
|
||||
};
|
||||
let match_options = MatchOptions {
|
||||
resolve_host_executables: true,
|
||||
active_permission_profile,
|
||||
};
|
||||
let evaluation = exec_policy.check_multiple_with_options(
|
||||
commands.iter(),
|
||||
@@ -404,6 +431,7 @@ impl ExecPolicyManager {
|
||||
let current_policy = self.current();
|
||||
let match_options = MatchOptions {
|
||||
resolve_host_executables: true,
|
||||
active_permission_profile: None,
|
||||
};
|
||||
let existing_evaluation = current_policy.check_multiple_with_options(
|
||||
[&amendment.command],
|
||||
@@ -551,6 +579,12 @@ pub fn format_exec_policy_error_with_source(error: &ExecPolicyError) -> String {
|
||||
None => format!("{path}: {message}"),
|
||||
}
|
||||
}
|
||||
ExecPolicyError::ParseConfigRules { path, source } => {
|
||||
format!("{path}: failed to parse [rules]: {source}")
|
||||
}
|
||||
ExecPolicyError::InvalidConfigRules { path, source } => {
|
||||
format!("{path}: failed to load [rules]: {source}")
|
||||
}
|
||||
_ => error.to_string(),
|
||||
}
|
||||
}
|
||||
@@ -560,34 +594,86 @@ async fn load_exec_policy_with_warning(
|
||||
) -> Result<(Policy, Option<ExecPolicyError>), ExecPolicyError> {
|
||||
match load_exec_policy(config_stack).await {
|
||||
Ok(policy) => Ok((policy, None)),
|
||||
Err(err @ ExecPolicyError::ParsePolicy { .. }) => Ok((Policy::empty(), Some(err))),
|
||||
Err(
|
||||
err @ (ExecPolicyError::ParsePolicy { .. }
|
||||
| ExecPolicyError::ParseConfigRules { .. }
|
||||
| ExecPolicyError::InvalidConfigRules { .. }),
|
||||
) => Ok((Policy::empty(), Some(err))),
|
||||
Err(err) => Err(err),
|
||||
}
|
||||
}
|
||||
|
||||
fn should_ignore_layer_exec_policy_rules(
|
||||
config_stack: &ConfigLayerStack,
|
||||
layer: &ConfigLayerEntry,
|
||||
) -> bool {
|
||||
config_stack.ignore_user_and_project_exec_policy_rules()
|
||||
&& matches!(
|
||||
layer.name,
|
||||
ConfigLayerSource::User { .. } | ConfigLayerSource::Project { .. }
|
||||
)
|
||||
}
|
||||
|
||||
fn config_layer_rules_path(layer: &ConfigLayerEntry) -> String {
|
||||
codex_config::format_config_layer_source(&layer.name, CONFIG_TOML_FILE)
|
||||
}
|
||||
|
||||
fn config_layer_rules_policy(layer: &ConfigLayerEntry) -> Result<Option<Policy>, ExecPolicyError> {
|
||||
let Some(rules) = layer.config.get("rules") else {
|
||||
return Ok(None);
|
||||
};
|
||||
let path = config_layer_rules_path(layer);
|
||||
let rules: codex_config::RequirementsExecPolicyToml =
|
||||
rules
|
||||
.clone()
|
||||
.try_into()
|
||||
.map_err(|source| ExecPolicyError::ParseConfigRules {
|
||||
path: path.clone(),
|
||||
source,
|
||||
})?;
|
||||
rules
|
||||
.to_config_policy()
|
||||
.map(Some)
|
||||
.map_err(|source| ExecPolicyError::InvalidConfigRules { path, source })
|
||||
}
|
||||
|
||||
pub async fn load_exec_policy(config_stack: &ConfigLayerStack) -> Result<Policy, ExecPolicyError> {
|
||||
// Disabled project layers already represent the trust decision, so hooks
|
||||
// and exec-policy loading can reuse the normal trusted-layer view.
|
||||
// Iterate the layers in increasing order of precedence, adding the *.rules
|
||||
// from each layer, so that higher-precedence layers can override
|
||||
// rules defined in lower-precedence ones.
|
||||
let mut parser = PolicyParser::new();
|
||||
let mut policy_paths = Vec::new();
|
||||
for layer in config_stack.get_layers(
|
||||
ConfigLayerStackOrdering::LowestPrecedenceFirst,
|
||||
/*include_disabled*/ false,
|
||||
) {
|
||||
if config_stack.ignore_user_and_project_exec_policy_rules()
|
||||
&& matches!(
|
||||
layer.name,
|
||||
ConfigLayerSource::User { .. } | ConfigLayerSource::Project { .. }
|
||||
)
|
||||
{
|
||||
if should_ignore_layer_exec_policy_rules(config_stack, layer) {
|
||||
continue;
|
||||
}
|
||||
if let Some(config_folder) = layer.config_folder() {
|
||||
let policy_dir = config_folder.join(RULES_DIR_NAME);
|
||||
let layer_policy_paths = collect_policy_files(&policy_dir).await?;
|
||||
policy_paths.extend(layer_policy_paths);
|
||||
for policy_path in layer_policy_paths {
|
||||
let contents = fs::read_to_string(&policy_path).await.map_err(|source| {
|
||||
ExecPolicyError::ReadFile {
|
||||
path: policy_path.clone(),
|
||||
source,
|
||||
}
|
||||
})?;
|
||||
let identifier = policy_path.to_string_lossy().to_string();
|
||||
parser.parse(&identifier, &contents).map_err(|source| {
|
||||
ExecPolicyError::ParsePolicy {
|
||||
path: identifier,
|
||||
source,
|
||||
}
|
||||
})?;
|
||||
policy_paths.push(policy_path);
|
||||
}
|
||||
}
|
||||
if let Some(policy) = config_layer_rules_policy(layer)? {
|
||||
parser.merge_overlay(&policy);
|
||||
}
|
||||
}
|
||||
tracing::trace!(
|
||||
@@ -595,24 +681,6 @@ pub async fn load_exec_policy(config_stack: &ConfigLayerStack) -> Result<Policy,
|
||||
"loaded exec policies"
|
||||
);
|
||||
|
||||
let mut parser = PolicyParser::new();
|
||||
for policy_path in &policy_paths {
|
||||
let contents =
|
||||
fs::read_to_string(policy_path)
|
||||
.await
|
||||
.map_err(|source| ExecPolicyError::ReadFile {
|
||||
path: policy_path.clone(),
|
||||
source,
|
||||
})?;
|
||||
let identifier = policy_path.to_string_lossy().to_string();
|
||||
parser
|
||||
.parse(&identifier, &contents)
|
||||
.map_err(|source| ExecPolicyError::ParsePolicy {
|
||||
path: identifier,
|
||||
source,
|
||||
})?;
|
||||
}
|
||||
|
||||
let policy = parser.build();
|
||||
tracing::debug!("loaded rules from {} files", policy_paths.len());
|
||||
tracing::trace!(rules = ?policy, "exec policy rules loaded");
|
||||
|
||||
@@ -208,6 +208,44 @@ async fn child_does_not_use_parent_exec_policy_when_requirements_exec_policy_dif
|
||||
));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn child_does_not_use_parent_exec_policy_when_config_toml_rules_differ() -> anyhow::Result<()>
|
||||
{
|
||||
let (_home, parent_config) = test_config().await;
|
||||
let mut child_config = parent_config.clone();
|
||||
let mut layers: Vec<_> = child_config
|
||||
.config_layer_stack
|
||||
.get_layers(
|
||||
ConfigLayerStackOrdering::LowestPrecedenceFirst,
|
||||
/*include_disabled*/ true,
|
||||
)
|
||||
.into_iter()
|
||||
.cloned()
|
||||
.collect();
|
||||
layers.push(ConfigLayerEntry::new(
|
||||
ConfigLayerSource::SessionFlags,
|
||||
toml::from_str::<TomlValue>(
|
||||
r#"
|
||||
[rules]
|
||||
prefix_rules = [
|
||||
{ pattern = [{ token = "echo" }], decision = "allow" },
|
||||
]
|
||||
"#,
|
||||
)?,
|
||||
));
|
||||
child_config.config_layer_stack = ConfigLayerStack::new(
|
||||
layers,
|
||||
child_config.config_layer_stack.requirements().clone(),
|
||||
child_config.config_layer_stack.requirements_toml().clone(),
|
||||
)?;
|
||||
|
||||
assert!(!child_uses_parent_exec_policy(
|
||||
&parent_config,
|
||||
&child_config
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn returns_empty_policy_when_no_policy_files_exist() {
|
||||
let temp_dir = tempdir().expect("create temp dir");
|
||||
@@ -452,6 +490,87 @@ async fn ignores_policies_outside_policy_dir() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn loads_config_toml_rules_with_starlark_rules() -> anyhow::Result<()> {
|
||||
let temp_dir = tempdir()?;
|
||||
let policy_dir = temp_dir.path().join(RULES_DIR_NAME);
|
||||
fs::create_dir_all(&policy_dir)?;
|
||||
fs::write(
|
||||
policy_dir.join("deny.rules"),
|
||||
r#"prefix_rule(pattern=["rm"], decision="forbidden")"#,
|
||||
)?;
|
||||
let dot_codex_folder =
|
||||
AbsolutePathBuf::from_absolute_path(temp_dir.path()).expect("absolute dot_codex_folder");
|
||||
let layer = ConfigLayerEntry::new(
|
||||
ConfigLayerSource::Project { dot_codex_folder },
|
||||
toml::from_str::<TomlValue>(
|
||||
r#"
|
||||
[rules]
|
||||
prefix_rules = [
|
||||
{ pattern = [{ token = "echo" }], decision = "allow" },
|
||||
]
|
||||
"#,
|
||||
)?,
|
||||
);
|
||||
let config_stack = ConfigLayerStack::new(
|
||||
vec![layer],
|
||||
ConfigRequirements::default(),
|
||||
ConfigRequirementsToml::default(),
|
||||
)?;
|
||||
|
||||
let policy = load_exec_policy(&config_stack).await?;
|
||||
|
||||
assert_eq!(
|
||||
policy
|
||||
.check_multiple([vec!["rm".to_string()]].iter(), &|_| Decision::Allow)
|
||||
.decision,
|
||||
Decision::Forbidden
|
||||
);
|
||||
assert_eq!(
|
||||
policy
|
||||
.check_multiple(
|
||||
[vec!["echo".to_string(), "hello".to_string()]].iter(),
|
||||
&|_| { Decision::Forbidden }
|
||||
)
|
||||
.decision,
|
||||
Decision::Allow
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn loads_config_toml_rules_from_user_config_file() -> anyhow::Result<()> {
|
||||
let codex_home = tempdir()?;
|
||||
fs::write(
|
||||
codex_home.path().join(CONFIG_TOML_FILE),
|
||||
r#"
|
||||
[rules]
|
||||
prefix_rules = [
|
||||
{ pattern = [{ token = "echo" }], decision = "allow" },
|
||||
]
|
||||
"#,
|
||||
)?;
|
||||
let config = ConfigBuilder::without_managed_config_for_tests()
|
||||
.codex_home(codex_home.path().to_path_buf())
|
||||
.build()
|
||||
.await?;
|
||||
|
||||
let policy = load_exec_policy(&config.config_layer_stack).await?;
|
||||
|
||||
assert_eq!(
|
||||
policy
|
||||
.check_multiple(
|
||||
[vec!["echo".to_string(), "hello".to_string()]].iter(),
|
||||
&|_| { Decision::Forbidden }
|
||||
)
|
||||
.decision,
|
||||
Decision::Allow
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ignores_policy_files_when_config_stack_disables_exec_policy_rules() {
|
||||
let temp_dir = tempdir().expect("create temp dir");
|
||||
@@ -479,6 +598,42 @@ async fn ignores_policy_files_when_config_stack_disables_exec_policy_rules() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ignores_config_toml_rules_when_config_stack_disables_exec_policy_rules()
|
||||
-> anyhow::Result<()> {
|
||||
let temp_dir = tempdir()?;
|
||||
let dot_codex_folder = AbsolutePathBuf::from_absolute_path(temp_dir.path())?;
|
||||
let layer = ConfigLayerEntry::new(
|
||||
ConfigLayerSource::Project { dot_codex_folder },
|
||||
toml::from_str::<TomlValue>(
|
||||
r#"
|
||||
[rules]
|
||||
prefix_rules = [
|
||||
{ pattern = [{ token = "curl" }], decision = "allow" },
|
||||
]
|
||||
"#,
|
||||
)?,
|
||||
);
|
||||
let config_stack = ConfigLayerStack::new(
|
||||
vec![layer],
|
||||
ConfigRequirements::default(),
|
||||
ConfigRequirementsToml::default(),
|
||||
)?
|
||||
.with_user_and_project_exec_policy_rules_ignored(
|
||||
/*ignore_user_and_project_exec_policy_rules*/ true,
|
||||
);
|
||||
|
||||
let policy = load_exec_policy(&config_stack).await?;
|
||||
|
||||
assert_eq!(
|
||||
policy
|
||||
.check_multiple([vec!["curl".to_string()]].iter(), &|_| Decision::Forbidden)
|
||||
.decision,
|
||||
Decision::Forbidden,
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn ignore_user_project_rules_keeps_system_policy_files() {
|
||||
let temp_dir = tempdir().expect("create temp dir");
|
||||
@@ -1335,6 +1490,7 @@ async fn mixed_rule_and_sandbox_prompt_prioritizes_rule_for_rejection_decision()
|
||||
mcp_elicitations: true,
|
||||
}),
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
sandbox_permissions: SandboxPermissions::RequireEscalated,
|
||||
prefix_rule: None,
|
||||
@@ -1372,6 +1528,7 @@ async fn mixed_rule_and_sandbox_prompt_rejects_when_granular_rules_are_disabled(
|
||||
mcp_elicitations: true,
|
||||
}),
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
sandbox_permissions: SandboxPermissions::RequireEscalated,
|
||||
prefix_rule: None,
|
||||
@@ -1396,6 +1553,7 @@ async fn exec_approval_requirement_falls_back_to_heuristics() {
|
||||
command: &command,
|
||||
approval_policy: AskForApproval::UnlessTrusted,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
@@ -1411,6 +1569,55 @@ async fn exec_approval_requirement_falls_back_to_heuristics() {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn exec_approval_requirement_honors_permissions_scoped_rule() {
|
||||
let policy_src = r#"prefix_rule(pattern=["git"], decision="prompt", permissions="strict")"#;
|
||||
let mut parser = PolicyParser::new();
|
||||
parser
|
||||
.parse("test.rules", policy_src)
|
||||
.expect("parse policy");
|
||||
let command = vec!["git".to_string(), "status".to_string()];
|
||||
let manager = ExecPolicyManager::new(Arc::new(parser.build()));
|
||||
|
||||
let scoped_requirement = manager
|
||||
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
|
||||
command: &command,
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: Some("strict".to_string()),
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
})
|
||||
.await;
|
||||
assert_eq!(
|
||||
scoped_requirement,
|
||||
ExecApprovalRequirement::NeedsApproval {
|
||||
reason: Some("`git status` requires approval by policy".to_string()),
|
||||
proposed_execpolicy_amendment: None,
|
||||
}
|
||||
);
|
||||
|
||||
let unscoped_requirement = manager
|
||||
.create_exec_approval_requirement_for_command(ExecApprovalRequest {
|
||||
command: &command,
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
})
|
||||
.await;
|
||||
assert_eq!(
|
||||
unscoped_requirement,
|
||||
ExecApprovalRequirement::Skip {
|
||||
bypass_sandbox: false,
|
||||
proposed_execpolicy_amendment: Some(ExecPolicyAmendment::new(command)),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn empty_bash_lc_script_falls_back_to_original_command() {
|
||||
let command = vec!["bash".to_string(), "-lc".to_string(), "".to_string()];
|
||||
@@ -1421,6 +1628,7 @@ async fn empty_bash_lc_script_falls_back_to_original_command() {
|
||||
command: &command,
|
||||
approval_policy: AskForApproval::UnlessTrusted,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
@@ -1450,6 +1658,7 @@ async fn whitespace_bash_lc_script_falls_back_to_original_command() {
|
||||
command: &command,
|
||||
approval_policy: AskForApproval::UnlessTrusted,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
@@ -1479,6 +1688,7 @@ async fn request_rule_uses_prefix_rule() {
|
||||
command: &command,
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
sandbox_permissions: SandboxPermissions::RequireEscalated,
|
||||
prefix_rule: Some(vec!["cargo".to_string(), "install".to_string()]),
|
||||
@@ -1511,6 +1721,7 @@ async fn request_rule_falls_back_when_prefix_rule_does_not_approve_all_commands(
|
||||
command: &command,
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: PermissionProfile::Disabled,
|
||||
active_permission_profile: None,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
sandbox_permissions: SandboxPermissions::RequireEscalated,
|
||||
prefix_rule: Some(vec!["cargo".to_string(), "install".to_string()]),
|
||||
@@ -1550,6 +1761,7 @@ async fn heuristics_apply_when_other_commands_match_policy() {
|
||||
command: &command,
|
||||
approval_policy: AskForApproval::UnlessTrusted,
|
||||
permission_profile: PermissionProfile::Disabled,
|
||||
active_permission_profile: None,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
@@ -2031,6 +2243,7 @@ async fn verify_approval_requirement_for_unsafe_powershell_command() {
|
||||
command: &sneaky_command,
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
sandbox_permissions: permissions,
|
||||
prefix_rule: None,
|
||||
@@ -2055,6 +2268,7 @@ async fn verify_approval_requirement_for_unsafe_powershell_command() {
|
||||
command: &dangerous_command,
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
sandbox_permissions: permissions,
|
||||
prefix_rule: None,
|
||||
@@ -2075,6 +2289,7 @@ async fn verify_approval_requirement_for_unsafe_powershell_command() {
|
||||
command: &dangerous_command,
|
||||
approval_policy: AskForApproval::Never,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
sandbox_permissions: permissions,
|
||||
prefix_rule: None,
|
||||
@@ -2170,6 +2385,7 @@ async fn exec_approval_requirement_for_command(
|
||||
command: &command,
|
||||
approval_policy,
|
||||
permission_profile,
|
||||
active_permission_profile: None,
|
||||
windows_sandbox_level: WindowsSandboxLevel::RestrictedToken,
|
||||
sandbox_permissions,
|
||||
prefix_rule,
|
||||
|
||||
@@ -9807,6 +9807,7 @@ async fn rejects_escalated_permissions_when_policy_not_on_request() {
|
||||
command: &command,
|
||||
approval_policy: turn_context.approval_policy.value(),
|
||||
permission_profile: turn_context.permission_profile(),
|
||||
active_permission_profile: None,
|
||||
windows_sandbox_level: turn_context.windows_sandbox_level,
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
prefix_rule: None,
|
||||
|
||||
@@ -167,6 +167,11 @@ async fn run_exec_like(args: RunExecLikeArgs) -> Result<FunctionToolOutput, Func
|
||||
command: &exec_params.command,
|
||||
approval_policy: turn.approval_policy.value(),
|
||||
permission_profile: turn.permission_profile(),
|
||||
active_permission_profile: turn
|
||||
.config
|
||||
.permissions
|
||||
.active_permission_profile()
|
||||
.map(|profile| profile.id),
|
||||
windows_sandbox_level: turn.windows_sandbox_level,
|
||||
sandbox_permissions: if effective_additional_permissions.permissions_preapproved {
|
||||
codex_protocol::models::SandboxPermissions::UseDefault
|
||||
|
||||
@@ -224,6 +224,12 @@ pub(super) async fn try_run_zsh_fork(
|
||||
tool_name: GuardianCommandSource::Shell,
|
||||
approval_policy: ctx.turn.approval_policy.value(),
|
||||
permission_profile: command_executor.permission_profile.clone(),
|
||||
active_permission_profile: ctx
|
||||
.turn
|
||||
.config
|
||||
.permissions
|
||||
.active_permission_profile()
|
||||
.map(|profile| profile.id),
|
||||
file_system_sandbox_policy: command_executor.file_system_sandbox_policy.clone(),
|
||||
sandbox_permissions: req.sandbox_permissions,
|
||||
approval_sandbox_permissions,
|
||||
@@ -296,6 +302,12 @@ pub(crate) async fn prepare_unified_exec_zsh_fork(
|
||||
tool_name: GuardianCommandSource::UnifiedExec,
|
||||
approval_policy: ctx.turn.approval_policy.value(),
|
||||
permission_profile: exec_request.permission_profile.clone(),
|
||||
active_permission_profile: ctx
|
||||
.turn
|
||||
.config
|
||||
.permissions
|
||||
.active_permission_profile()
|
||||
.map(|profile| profile.id),
|
||||
file_system_sandbox_policy: exec_request.file_system_sandbox_policy.clone(),
|
||||
sandbox_permissions: req.sandbox_permissions,
|
||||
approval_sandbox_permissions: approval_sandbox_permissions(
|
||||
@@ -330,6 +342,7 @@ struct CoreShellActionProvider {
|
||||
tool_name: GuardianCommandSource,
|
||||
approval_policy: AskForApproval,
|
||||
permission_profile: PermissionProfile,
|
||||
active_permission_profile: Option<String>,
|
||||
file_system_sandbox_policy: FileSystemSandboxPolicy,
|
||||
sandbox_permissions: SandboxPermissions,
|
||||
approval_sandbox_permissions: SandboxPermissions,
|
||||
@@ -619,6 +632,7 @@ impl CoreShellActionProvider {
|
||||
InterceptedExecPolicyContext {
|
||||
approval_policy: self.approval_policy,
|
||||
permission_profile: self.permission_profile.clone(),
|
||||
active_permission_profile: self.active_permission_profile.clone(),
|
||||
windows_sandbox_level: self.turn.windows_sandbox_level,
|
||||
sandbox_permissions: self.approval_sandbox_permissions,
|
||||
enable_shell_wrapper_parsing:
|
||||
@@ -688,6 +702,7 @@ fn evaluate_intercepted_exec_policy(
|
||||
let InterceptedExecPolicyContext {
|
||||
approval_policy,
|
||||
permission_profile,
|
||||
active_permission_profile,
|
||||
windows_sandbox_level,
|
||||
sandbox_permissions,
|
||||
enable_shell_wrapper_parsing,
|
||||
@@ -728,6 +743,7 @@ fn evaluate_intercepted_exec_policy(
|
||||
&fallback,
|
||||
&MatchOptions {
|
||||
resolve_host_executables: true,
|
||||
active_permission_profile,
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -736,6 +752,7 @@ fn evaluate_intercepted_exec_policy(
|
||||
struct InterceptedExecPolicyContext {
|
||||
approval_policy: AskForApproval,
|
||||
permission_profile: PermissionProfile,
|
||||
active_permission_profile: Option<String>,
|
||||
windows_sandbox_level: WindowsSandboxLevel,
|
||||
sandbox_permissions: SandboxPermissions,
|
||||
enable_shell_wrapper_parsing: bool,
|
||||
|
||||
@@ -428,6 +428,7 @@ async fn preapproved_additional_permissions_escalate_intercepted_exec() -> anyho
|
||||
tool_name: GuardianCommandSource::Shell,
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: permission_profile.clone(),
|
||||
active_permission_profile: None,
|
||||
file_system_sandbox_policy: read_only_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::WithAdditionalPermissions,
|
||||
approval_sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
@@ -563,6 +564,7 @@ async fn execve_permission_request_hook_short_circuits_prompt() -> anyhow::Resul
|
||||
tool_name: GuardianCommandSource::Shell,
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
file_system_sandbox_policy: read_only_file_system_sandbox_policy(),
|
||||
sandbox_permissions: SandboxPermissions::RequireEscalated,
|
||||
approval_sandbox_permissions: SandboxPermissions::RequireEscalated,
|
||||
@@ -628,6 +630,7 @@ fn evaluate_intercepted_exec_policy_uses_wrapper_command_when_shell_wrapper_pars
|
||||
InterceptedExecPolicyContext {
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
enable_shell_wrapper_parsing: enable_intercepted_exec_policy_shell_wrapper_parsing,
|
||||
@@ -679,6 +682,7 @@ fn evaluate_intercepted_exec_policy_matches_inner_shell_commands_when_enabled()
|
||||
InterceptedExecPolicyContext {
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
enable_shell_wrapper_parsing: enable_intercepted_exec_policy_shell_wrapper_parsing,
|
||||
@@ -721,6 +725,7 @@ host_executable(name = "git", paths = ["{git_path_literal}"])
|
||||
InterceptedExecPolicyContext {
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
enable_shell_wrapper_parsing: false,
|
||||
@@ -773,6 +778,7 @@ prefix_rule(pattern = ["{cat_path_literal}"], decision = "allow")
|
||||
tool_name: GuardianCommandSource::Shell,
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile,
|
||||
active_permission_profile: None,
|
||||
file_system_sandbox_policy,
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
approval_sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
@@ -815,6 +821,7 @@ async fn denied_reads_keep_granular_sandbox_rejection_for_escalation() -> anyhow
|
||||
mcp_elicitations: true,
|
||||
}),
|
||||
permission_profile,
|
||||
active_permission_profile: None,
|
||||
file_system_sandbox_policy,
|
||||
sandbox_permissions: SandboxPermissions::RequireEscalated,
|
||||
approval_sandbox_permissions: SandboxPermissions::RequireEscalated,
|
||||
@@ -854,6 +861,7 @@ fn intercepted_exec_policy_treats_preapproved_additional_permissions_as_default(
|
||||
InterceptedExecPolicyContext {
|
||||
approval_policy,
|
||||
permission_profile: permission_profile.clone(),
|
||||
active_permission_profile: None,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
sandbox_permissions: super::approval_sandbox_permissions(
|
||||
SandboxPermissions::WithAdditionalPermissions,
|
||||
@@ -869,6 +877,7 @@ fn intercepted_exec_policy_treats_preapproved_additional_permissions_as_default(
|
||||
InterceptedExecPolicyContext {
|
||||
approval_policy,
|
||||
permission_profile,
|
||||
active_permission_profile: None,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
sandbox_permissions: SandboxPermissions::WithAdditionalPermissions,
|
||||
enable_shell_wrapper_parsing: false,
|
||||
@@ -902,6 +911,7 @@ host_executable(name = "git", paths = ["{allowed_git_literal}"])
|
||||
InterceptedExecPolicyContext {
|
||||
approval_policy: AskForApproval::OnRequest,
|
||||
permission_profile: PermissionProfile::read_only(),
|
||||
active_permission_profile: None,
|
||||
windows_sandbox_level: WindowsSandboxLevel::Disabled,
|
||||
sandbox_permissions: SandboxPermissions::UseDefault,
|
||||
enable_shell_wrapper_parsing: false,
|
||||
|
||||
@@ -1051,6 +1051,12 @@ impl UnifiedExecProcessManager {
|
||||
command: &request.command,
|
||||
approval_policy: context.turn.approval_policy.value(),
|
||||
permission_profile: context.turn.permission_profile(),
|
||||
active_permission_profile: context
|
||||
.turn
|
||||
.config
|
||||
.permissions
|
||||
.active_permission_profile()
|
||||
.map(|profile| profile.id),
|
||||
windows_sandbox_level: context.turn.windows_sandbox_level,
|
||||
sandbox_permissions: if request.additional_permissions_preapproved {
|
||||
crate::sandboxing::SandboxPermissions::UseDefault
|
||||
|
||||
Reference in New Issue
Block a user