fix(subagents) share execpolicy by default (#13702)

## Summary
If a subagent requests approval, and the user persists that approval to
the execpolicy, it should (by default) propagate. We'll need to rethink
this a bit in light of coming Permissions changes, though I think this
is closer to the end state that we'd want, which is that execpolicy
changes to one permissions profile should be synced across threads.

## Testing
- [x] Added integration test

---------

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Dylan Hurd
2026-03-17 23:42:26 -07:00
committed by GitHub
parent a3613035f3
commit 84f4e7b39d
10 changed files with 427 additions and 13 deletions

View File

@@ -32,8 +32,10 @@ use tracing::instrument;
use crate::bash::parse_shell_lc_plain_commands;
use crate::bash::parse_shell_lc_single_command_prefix;
use crate::config::Config;
use crate::sandboxing::SandboxPermissions;
use crate::tools::sandboxing::ExecApprovalRequirement;
use codex_utils_absolute_path::AbsolutePathBuf;
use shlex::try_join as shlex_try_join;
const PROMPT_CONFLICT_REASON: &str =
@@ -94,6 +96,24 @@ static BANNED_PREFIX_SUGGESTIONS: &[&[&str]] = &[
&["osascript"],
];
pub(crate) fn child_uses_parent_exec_policy(parent_config: &Config, child_config: &Config) -> bool {
fn exec_policy_config_folders(config: &Config) -> Vec<AbsolutePathBuf> {
config
.config_layer_stack
.get_layers(
ConfigLayerStackOrdering::LowestPrecedenceFirst,
/*include_disabled*/ false,
)
.into_iter()
.filter_map(codex_config::ConfigLayerEntry::config_folder)
.collect()
}
exec_policy_config_folders(parent_config) == exec_policy_config_folders(child_config)
&& parent_config.config_layer_stack.requirements().exec_policy
== child_config.config_layer_stack.requirements().exec_policy
}
fn is_policy_match(rule_match: &RuleMatch) -> bool {
match rule_match {
RuleMatch::PrefixRuleMatch { .. } => true,
@@ -170,6 +190,7 @@ pub enum ExecPolicyUpdateError {
pub(crate) struct ExecPolicyManager {
policy: ArcSwap<Policy>,
update_lock: tokio::sync::Mutex<()>,
}
pub(crate) struct ExecApprovalRequest<'a> {
@@ -185,6 +206,7 @@ impl ExecPolicyManager {
pub(crate) fn new(policy: Arc<Policy>) -> Self {
Self {
policy: ArcSwap::from(policy),
update_lock: tokio::sync::Mutex::new(()),
}
}
@@ -292,11 +314,11 @@ impl ExecPolicyManager {
codex_home: &Path,
amendment: &ExecPolicyAmendment,
) -> Result<(), ExecPolicyUpdateError> {
let _update_guard = self.update_lock.lock().await;
let policy_path = default_policy_path(codex_home);
let prefix = amendment.command.clone();
spawn_blocking({
let policy_path = policy_path.clone();
let prefix = prefix.clone();
let prefix = amendment.command.clone();
move || blocking_append_allow_prefix_rule(&policy_path, &prefix)
})
.await
@@ -306,8 +328,25 @@ impl ExecPolicyManager {
source,
})?;
let mut updated_policy = self.current().as_ref().clone();
updated_policy.add_prefix_rule(&prefix, Decision::Allow)?;
let current_policy = self.current();
let match_options = MatchOptions {
resolve_host_executables: true,
};
let existing_evaluation = current_policy.check_multiple_with_options(
[&amendment.command],
&|_| Decision::Forbidden,
&match_options,
);
let already_allowed = existing_evaluation.decision == Decision::Allow
&& existing_evaluation.matched_rules.iter().any(|rule_match| {
is_policy_match(rule_match) && rule_match.decision() == Decision::Allow
});
if already_allowed {
return Ok(());
}
let mut updated_policy = current_policy.as_ref().clone();
updated_policy.add_prefix_rule(&amendment.command, Decision::Allow)?;
self.policy.store(Arc::new(updated_policy));
Ok(())
}
@@ -320,6 +359,7 @@ impl ExecPolicyManager {
decision: Decision,
justification: Option<String>,
) -> Result<(), ExecPolicyUpdateError> {
let _update_guard = self.update_lock.lock().await;
let policy_path = default_policy_path(codex_home);
let host = host.to_string();
spawn_blocking({