fix(core) RequestPermissions + ApplyPatch (#14055)

## Summary
The apply_patch tool should also respect AdditionalPermissions

## Testing
- [x] Added unit tests

---------

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Dylan Hurd
2026-03-09 16:11:19 -07:00
committed by GitHub
parent 66e71cce11
commit c1defcc98c
3 changed files with 191 additions and 2 deletions

View File

@@ -18,6 +18,7 @@ use crate::tools::context::ToolOutput;
use crate::tools::context::ToolPayload;
use crate::tools::events::ToolEmitter;
use crate::tools::events::ToolEventCtx;
use crate::tools::handlers::apply_granted_turn_permissions;
use crate::tools::handlers::parse_arguments;
use crate::tools::orchestrator::ToolOrchestrator;
use crate::tools::registry::ToolHandler;
@@ -30,7 +31,10 @@ use crate::tools::spec::JsonSchema;
use async_trait::async_trait;
use codex_apply_patch::ApplyPatchAction;
use codex_apply_patch::ApplyPatchFileChange;
use codex_protocol::models::FileSystemPermissions;
use codex_protocol::models::PermissionProfile;
use codex_utils_absolute_path::AbsolutePathBuf;
use std::collections::BTreeSet;
use std::sync::Arc;
pub struct ApplyPatchHandler;
@@ -61,6 +65,31 @@ fn to_abs_path(cwd: &Path, path: &Path) -> Option<AbsolutePathBuf> {
AbsolutePathBuf::resolve_path_against_base(path, cwd).ok()
}
fn write_permissions_for_paths(file_paths: &[AbsolutePathBuf]) -> Option<PermissionProfile> {
let write_paths = file_paths
.iter()
.map(|path| {
path.parent()
.unwrap_or_else(|| path.clone())
.into_path_buf()
})
.collect::<BTreeSet<_>>()
.into_iter()
.map(AbsolutePathBuf::from_absolute_path)
.collect::<Result<Vec<_>, _>>()
.ok()?;
let permissions = (!write_paths.is_empty()).then_some(PermissionProfile {
file_system: Some(FileSystemPermissions {
read: Some(vec![]),
write: Some(write_paths),
}),
..Default::default()
})?;
crate::sandboxing::normalize_additional_permissions(permissions).ok()
}
#[async_trait]
impl ToolHandler for ApplyPatchHandler {
fn kind(&self) -> ToolKind {
@@ -119,6 +148,12 @@ impl ToolHandler for ApplyPatchHandler {
InternalApplyPatchInvocation::DelegateToExec(apply) => {
let changes = convert_apply_patch_to_protocol(&apply.action);
let file_paths = file_paths_for_action(&apply.action);
let effective_additional_permissions = apply_granted_turn_permissions(
session.as_ref(),
crate::sandboxing::SandboxPermissions::UseDefault,
write_permissions_for_paths(&file_paths),
)
.await;
let emitter =
ToolEmitter::apply_patch(changes.clone(), apply.auto_approved);
let event_ctx = ToolEventCtx::new(
@@ -134,6 +169,12 @@ impl ToolHandler for ApplyPatchHandler {
file_paths,
changes,
exec_approval_requirement: apply.exec_approval_requirement,
sandbox_permissions: effective_additional_permissions
.sandbox_permissions,
additional_permissions: effective_additional_permissions
.additional_permissions,
permissions_preapproved: effective_additional_permissions
.permissions_preapproved,
timeout_ms: None,
codex_exe: turn.codex_linux_sandbox_exe.clone(),
};
@@ -222,6 +263,12 @@ pub(crate) async fn intercept_apply_patch(
InternalApplyPatchInvocation::DelegateToExec(apply) => {
let changes = convert_apply_patch_to_protocol(&apply.action);
let approval_keys = file_paths_for_action(&apply.action);
let effective_additional_permissions = apply_granted_turn_permissions(
session.as_ref(),
crate::sandboxing::SandboxPermissions::UseDefault,
write_permissions_for_paths(&approval_keys),
)
.await;
let emitter = ToolEmitter::apply_patch(changes.clone(), apply.auto_approved);
let event_ctx = ToolEventCtx::new(
session.as_ref(),
@@ -236,6 +283,11 @@ pub(crate) async fn intercept_apply_patch(
file_paths: approval_keys,
changes,
exec_approval_requirement: apply.exec_approval_requirement,
sandbox_permissions: effective_additional_permissions.sandbox_permissions,
additional_permissions: effective_additional_permissions
.additional_permissions,
permissions_preapproved: effective_additional_permissions
.permissions_preapproved,
timeout_ms,
codex_exe: turn.codex_linux_sandbox_exe.clone(),
};

View File

@@ -23,6 +23,7 @@ use crate::tools::sandboxing::ToolRuntime;
use crate::tools::sandboxing::with_cached_approval;
use codex_apply_patch::ApplyPatchAction;
use codex_apply_patch::CODEX_CORE_APPLY_PATCH_ARG1;
use codex_protocol::models::PermissionProfile;
use codex_protocol::protocol::AskForApproval;
use codex_protocol::protocol::FileChange;
use codex_protocol::protocol::ReviewDecision;
@@ -37,6 +38,9 @@ pub struct ApplyPatchRequest {
pub file_paths: Vec<AbsolutePathBuf>,
pub changes: std::collections::HashMap<PathBuf, FileChange>,
pub exec_approval_requirement: ExecApprovalRequirement,
pub sandbox_permissions: SandboxPermissions,
pub additional_permissions: Option<PermissionProfile>,
pub permissions_preapproved: bool,
pub timeout_ms: Option<u64>,
pub codex_exe: Option<PathBuf>,
}
@@ -87,8 +91,8 @@ impl ApplyPatchRuntime {
expiration: req.timeout_ms.into(),
// Run apply_patch with a minimal environment for determinism and to avoid leaks.
env: HashMap::new(),
sandbox_permissions: SandboxPermissions::UseDefault,
additional_permissions: None,
sandbox_permissions: req.sandbox_permissions,
additional_permissions: req.additional_permissions.clone(),
justification: None,
})
}
@@ -134,6 +138,9 @@ impl Approvable<ApplyPatchRequest> for ApplyPatchRuntime {
let action = ApplyPatchRuntime::build_guardian_review_request(req);
return review_approval_request(session, turn, action, retry_reason).await;
}
if req.permissions_preapproved && retry_reason.is_none() {
return ReviewDecision::Approved;
}
if let Some(reason) = retry_reason {
let rx_approve = session
.request_patch_approval(turn, call_id, changes.clone(), Some(reason), None)
@@ -244,6 +251,9 @@ mod tests {
reason: None,
proposed_execpolicy_amendment: None,
},
sandbox_permissions: SandboxPermissions::UseDefault,
additional_permissions: None,
permissions_preapproved: false,
timeout_ms: None,
codex_exe: None,
};