//! Shared approvals and sandboxing traits used by tool runtimes.//!//! Consolidates the approval flow primitives (`ApprovalDecision`, `ApprovalStore`,//! `ApprovalCtx`, `Approvable`) together with the sandbox orchestration traits//! and helpers (`Sandboxable`, `ToolRuntime`, `SandboxAttempt`, etc.).use crate::sandboxing::ExecOptions;use crate::sandboxing::SandboxPermissions;use crate::session::session::Session;use crate::session::turn_context::TurnContext;use crate::state::SessionServices;use crate::tools::hook_names::HookToolName;use crate::tools::network_approval::NetworkApprovalSpec;use codex_network_proxy::NetworkProxy;use codex_protocol::approvals::ExecPolicyAmendment;use codex_protocol::approvals::NetworkApprovalContext;use codex_protocol::error::CodexErr;use codex_protocol::permissions::FileSystemSandboxKind;use codex_protocol::permissions::FileSystemSandboxPolicy;use codex_protocol::protocol::AskForApproval;use codex_protocol::protocol::ReviewDecision;use codex_sandboxing::SandboxCommand;use codex_sandboxing::SandboxManager;use codex_sandboxing::SandboxTransformRequest;use codex_sandboxing::SandboxType;use codex_sandboxing::SandboxablePreference;use codex_tools::ToolName;use codex_utils_absolute_path::AbsolutePathBuf;use codex_utils_path_uri::PathUri;use futures::Future;use futures::future::BoxFuture;use serde::Serialize;use std::collections::HashMap;use std::fmt::Debug;use std::hash::Hash;use std::sync::Arc;use tokio_util::sync::CancellationToken;#[derive(Clone, Default, Debug)]pub(crate) struct ApprovalStore { // Store serialized keys for generic caching across requests. map: HashMap<String, ReviewDecision>,}impl ApprovalStore { pub fn get<K>(&self, key: &K) -> Option<ReviewDecision> where K: Serialize, { let s = serde_json::to_string(key).ok()?; self.map.get(&s).cloned() } pub fn put<K>(&mut self, key: K, value: ReviewDecision) where K: Serialize, { if let Ok(s) = serde_json::to_string(&key) { self.map.insert(s, value); } }}/// Takes a vector of approval keys and returns a ReviewDecision./// There will be one key in most cases, but apply_patch can modify multiple files at once.////// - If all keys are already approved for session, we skip prompting./// - If the user approves for session, we store the decision for each key individually/// so future requests touching any subset can also skip prompting.pub(crate) async fn with_cached_approval<K, F, Fut>( services: &SessionServices, // Name of the tool, used for metrics collection. tool_name: &str, keys: Vec<K>, fetch: F,) -> ReviewDecisionwhere K: Serialize, F: FnOnce() -> Fut, Fut: Future<Output = ReviewDecision>,{ // To be defensive here, don't bother with checking the cache if keys are empty. if keys.is_empty() { return fetch().await; } let already_approved = { let store = services.tool_approvals.lock().await; keys.iter() .all(|key| matches!(store.get(key), Some(ReviewDecision::ApprovedForSession))) }; if already_approved { return ReviewDecision::ApprovedForSession; } let decision = fetch().await; services.session_telemetry.counter( "codex.approval.requested", /*inc*/ 1, &[ ("tool", tool_name), ("approved", decision.to_opaque_string()), ], ); if matches!(decision, ReviewDecision::ApprovedForSession) { let mut store = services.tool_approvals.lock().await; for key in keys { store.put(key, ReviewDecision::ApprovedForSession); } } decision}#[derive(Clone)]pub(crate) struct ApprovalCtx<'a> { pub session: &'a Arc<Session>, pub turn: &'a Arc<TurnContext>, pub call_id: &'a str, /// Guardian review lifecycle ID for this approval, when guardian is reviewing it. /// /// This is separate from `call_id`: `call_id` identifies the tool item under /// review, while this ID identifies the review itself. Keeping both lets /// denial handling, overrides, and app-server notifications refer to the /// review without overloading the tool call ID as a review ID. pub guardian_review_id: Option<String>, pub retry_reason: Option<String>, pub network_approval_context: Option<NetworkApprovalContext>,}#[derive(Clone, Debug, PartialEq, Eq)]pub(crate) struct PermissionRequestPayload { pub tool_name: HookToolName, pub tool_input: serde_json::Value,}impl PermissionRequestPayload { pub(crate) fn bash(command: String, description: Option<String>) -> Self { let mut tool_input = serde_json::Map::new(); tool_input.insert("command".to_string(), serde_json::Value::String(command)); if let Some(description) = description { tool_input.insert( "description".to_string(), serde_json::Value::String(description), ); } Self { tool_name: HookToolName::bash(), tool_input: serde_json::Value::Object(tool_input), } }}// Specifies what tool orchestrator should do with a given tool call.#[derive(Clone, Debug, PartialEq, Eq)]pub(crate) enum ExecApprovalRequirement { /// No approval required for this tool call. Skip { /// The first attempt should skip sandboxing (e.g., when explicitly /// greenlit by policy). bypass_sandbox: bool, /// Proposed execpolicy amendment to skip future approvals for similar commands /// Only applies if the command fails to run in sandbox and codex prompts the user to run outside the sandbox. proposed_execpolicy_amendment: Option<ExecPolicyAmendment>, }, /// Approval required for this tool call. NeedsApproval { reason: Option<String>, /// Proposed execpolicy amendment to skip future approvals for similar commands /// See core/src/exec_policy.rs for more details on how proposed_execpolicy_amendment is determined. proposed_execpolicy_amendment: Option<ExecPolicyAmendment>, }, /// Execution forbidden for this tool call. Forbidden { reason: String },}impl ExecApprovalRequirement { pub fn proposed_execpolicy_amendment(&self) -> Option<&ExecPolicyAmendment> { match self { Self::NeedsApproval { proposed_execpolicy_amendment: Some(prefix), .. } => Some(prefix), Self::Skip { proposed_execpolicy_amendment: Some(prefix), .. } => Some(prefix), _ => None, } }}/// - Never, OnFailure: do not ask/// - OnRequest: ask unless filesystem access is unrestricted/// - Granular: ask unless filesystem access is unrestricted, but auto-reject/// when granular sandbox approval is disabled./// - UnlessTrusted: always askpub(crate) fn default_exec_approval_requirement( policy: AskForApproval, file_system_sandbox_policy: &FileSystemSandboxPolicy,) -> ExecApprovalRequirement { let needs_approval = match policy { AskForApproval::Never | AskForApproval::OnFailure => false, AskForApproval::OnRequest | AskForApproval::Granular(_) => { matches!( file_system_sandbox_policy.kind, FileSystemSandboxKind::Restricted ) } AskForApproval::UnlessTrusted => true, }; if needs_approval && matches!( policy, AskForApproval::Granular(granular_config) if !granular_config.allows_sandbox_approval() ) { ExecApprovalRequirement::Forbidden { reason: "approval policy disallowed sandbox approval prompt".to_string(), } } else if needs_approval { ExecApprovalRequirement::NeedsApproval { reason: None, proposed_execpolicy_amendment: None, } } else { ExecApprovalRequirement::Skip { bypass_sandbox: false, proposed_execpolicy_amendment: None, } }}#[derive(Clone, Copy, Debug, PartialEq, Eq)]pub(crate) enum SandboxOverride { NoOverride, BypassSandboxFirstAttempt,}pub(crate) fn sandbox_override_for_first_attempt( sandbox_permissions: SandboxPermissions, exec_approval_requirement: &ExecApprovalRequirement, file_system_sandbox_policy: &FileSystemSandboxPolicy,) -> SandboxOverride { // Deny-read restrictions are part of the active permission policy. Running // without a filesystem sandbox would discard them, even if the command was // otherwise approved by rules or explicit escalation. if !unsandboxed_execution_allowed(file_system_sandbox_policy) { return SandboxOverride::NoOverride; } // ExecPolicy `Allow` can intentionally imply full trust (Skip + bypass_sandbox=true), // which supersedes `with_additional_permissions` sandboxed execution hints. if matches!( exec_approval_requirement, ExecApprovalRequirement::Skip { bypass_sandbox: true, .. } ) { return SandboxOverride::BypassSandboxFirstAttempt; } if sandbox_permissions.requires_escalated_permissions() { SandboxOverride::BypassSandboxFirstAttempt } else { SandboxOverride::NoOverride }}/// Returns true when the active filesystem policy can be represented by/// running without a filesystem sandbox.////// Denied reads only exist inside the sandbox. If a policy contains any/// denied-read paths, bypassing the sandbox would silently grant those reads,/// so escalation must keep the command sandboxed with the denied reads intact.pub(crate) fn unsandboxed_execution_allowed( file_system_sandbox_policy: &FileSystemSandboxPolicy,) -> bool { !file_system_sandbox_policy.has_denied_read_restrictions()}pub(crate) fn sandbox_permissions_preserving_denied_reads( sandbox_permissions: SandboxPermissions, file_system_sandbox_policy: &FileSystemSandboxPolicy,) -> SandboxPermissions { if sandbox_permissions.requires_escalated_permissions() && !unsandboxed_execution_allowed(file_system_sandbox_policy) { // `RequireEscalated` normally asks the executor to bypass the sandbox. // When denied reads are active, that would drop the only mechanism that // enforces them, so fall back to the default sandboxed attempt instead. SandboxPermissions::UseDefault } else { sandbox_permissions }}pub(crate) fn managed_network_for_sandbox_permissions( network: Option<&NetworkProxy>, sandbox_permissions: SandboxPermissions,) -> Option<&NetworkProxy> { if sandbox_permissions.requires_escalated_permissions() { None } else { network }}pub(crate) trait Approvable<Req> { type ApprovalKey: Hash + Eq + Clone + Debug + Serialize; // In most cases (shell, unified_exec), a request will have a single approval key. // // However, apply_patch needs session "Allow, don't ask again" semantics that // apply to multiple atomic targets (e.g., apply_patch approves per file path). Returning // a list of keys lets the runtime treat the request as approved-for-session only if // *all* keys are already approved, while still caching approvals per-key so future // requests touching a subset can be auto-approved. fn approval_keys(&self, req: &Req) -> Vec<Self::ApprovalKey>; /// Return per-request sandbox permissions for first-attempt sandbox /// selection. Most tools use the ambient sandbox policy unchanged. fn sandbox_permissions(&self, _req: &Req) -> SandboxPermissions { SandboxPermissions::UseDefault } fn should_bypass_approval(&self, policy: AskForApproval, already_approved: bool) -> bool { if already_approved { // We do not ask one more time return true; } matches!(policy, AskForApproval::Never) } /// Return `Some(_)` to specify a custom exec approval requirement, or `None` /// to fall back to policy-based default. fn exec_approval_requirement(&self, _req: &Req) -> Option<ExecApprovalRequirement> { None } /// Return hook input for approval-time policy hooks when this runtime wants /// hook evaluation to run before guardian or user approval. fn permission_request_payload(&self, _req: &Req) -> Option<PermissionRequestPayload> { None } /// Decide we can request an approval for no-sandbox execution. fn wants_no_sandbox_approval(&self, policy: AskForApproval) -> bool { match policy { AskForApproval::OnFailure => true, AskForApproval::UnlessTrusted => true, AskForApproval::Never => false, AskForApproval::OnRequest => false, AskForApproval::Granular(granular_config) => granular_config.sandbox_approval, } } fn start_approval_async<'a>( &'a mut self, req: &'a Req, ctx: ApprovalCtx<'a>, ) -> BoxFuture<'a, ReviewDecision>;}pub(crate) trait Sandboxable { fn sandbox_preference(&self) -> SandboxablePreference; fn escalate_on_failure(&self) -> bool { true }}pub(crate) struct ToolCtx { pub session: Arc<Session>, pub turn: Arc<TurnContext>, pub call_id: String, pub tool_name: ToolName,}#[derive(Debug)]pub(crate) enum ToolError { Rejected(String), Codex(CodexErr),}pub(crate) trait ToolRuntime<Req, Out>: Approvable<Req> + Sandboxable { fn network_approval_spec(&self, _req: &Req, _ctx: &ToolCtx) -> Option<NetworkApprovalSpec> { None } fn sandbox_cwd<'a>(&self, _req: &'a Req) -> Option<&'a AbsolutePathBuf> { None } async fn run( &mut self, req: &Req, attempt: &SandboxAttempt<'_>, ctx: &ToolCtx, ) -> Result<Out, ToolError>;}pub(crate) struct SandboxAttempt<'a> { pub sandbox: SandboxType, pub permissions: &'a codex_protocol::models::PermissionProfile, pub enforce_managed_network: bool, pub(crate) manager: &'a SandboxManager, pub(crate) sandbox_cwd: &'a PathUri, pub(crate) workspace_roots: &'a [AbsolutePathBuf], pub codex_linux_sandbox_exe: Option<&'a std::path::PathBuf>, pub use_legacy_landlock: bool, pub windows_sandbox_level: codex_protocol::config_types::WindowsSandboxLevel, pub windows_sandbox_private_desktop: bool, pub network_denial_cancellation_token: Option<CancellationToken>,}impl<'a> SandboxAttempt<'a> { pub fn env_for( &self, command: SandboxCommand, options: ExecOptions, network: Option<&NetworkProxy>, ) -> Result<crate::sandboxing::ExecRequest, CodexErr> { let request = self .manager .transform(SandboxTransformRequest { command, permissions: self.permissions, sandbox: self.sandbox, enforce_managed_network: self.enforce_managed_network, network, sandbox_policy_cwd: self.sandbox_cwd, codex_linux_sandbox_exe: self .codex_linux_sandbox_exe .map(std::path::PathBuf::as_path), use_legacy_landlock: self.use_legacy_landlock, windows_sandbox_level: self.windows_sandbox_level, windows_sandbox_private_desktop: self.windows_sandbox_private_desktop, }) .map_err(CodexErr::from)?; Ok(crate::sandboxing::ExecRequest::from_sandbox_exec_request( request, options, self.workspace_roots.to_vec(), )) }}#[cfg(test)]#[path = "sandboxing_tests.rs"]mod tests;