feat: support permissions-scoped exec rules

This commit is contained in:
Michael Bolin
2026-06-22 16:45:02 -07:00
parent 94427aaf46
commit a2d559b84c
16 changed files with 699 additions and 43 deletions
@@ -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#"
+97 -29
View File
@@ -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");
+216
View File
@@ -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,
+1
View File
@@ -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