Download codex-rs/core/src/tools/sandboxing.rs from SaylorTwift/codex: direct link, hf CLI and curl.
- Browser
- Download file 19.3 kB
-
https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/core/src/tools/sandboxing.rs
- Command line
-
hf download hf://SaylorTwift/codex/codex-rs/core/src/tools/sandboxing.rs
-
curl -L -o sandboxing.rs https://huggingface.co/SaylorTwift/codex/resolve/main/codex-rs/core/src/tools/sandboxing.rs
19.3 kB
| //! Shared approvals and sandboxing traits used by tool runtimes. | |
| //! | |
| //! Consolidates the approval flow primitives (`ApprovalDecision`, `ApprovalStore`, | |
| //! `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::step_context::StepContext; | |
| use crate::session::turn_context::TurnEnvironment; | |
| use crate::state::SessionServices; | |
| use crate::tools::hook_names::HookToolName; | |
| use crate::tools::network_approval::NetworkApprovalSpec; | |
| use codex_file_system::FileSystemSandboxContext; | |
| use codex_file_system::WindowsSandboxSelection; | |
| use codex_network_proxy::NetworkProxy; | |
| use codex_protocol::approvals::ExecPolicyAmendment; | |
| use codex_protocol::config_types::WindowsSandboxLevel; | |
| 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_sandboxing::policy_transforms::effective_permission_profile; | |
| use codex_tools::ToolName; | |
| use codex_utils_path_uri::PathConvention; | |
| use codex_utils_path_uri::PathUri; | |
| use futures::Future; | |
| use serde::Serialize; | |
| use std::collections::HashMap; | |
| use std::sync::Arc; | |
| use tokio_util::sync::CancellationToken; | |
| 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, | |
| ) -> ReviewDecision | |
| where | |
| 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 | |
| } | |
| pub(crate) struct ApprovalRequestReasons { | |
| pub(crate) approval: Option<String>, | |
| pub(crate) retry: Option<String>, | |
| } | |
| pub(crate) use super::approvals::ApprovalAction; | |
| 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. | |
| 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: 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 ask | |
| pub(crate) fn default_exec_approval_requirement( | |
| policy: AskForApproval, | |
| file_system_sandbox_policy: &FileSystemSandboxPolicy, | |
| ) -> ExecApprovalRequirement { | |
| let needs_approval = match policy { | |
| AskForApproval::Never => 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, | |
| } | |
| } | |
| } | |
| 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> { | |
| /// 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 | |
| } | |
| /// Decide we can request an approval for no-sandbox execution. | |
| fn wants_no_sandbox_approval(&self, policy: AskForApproval) -> bool { | |
| match policy { | |
| AskForApproval::UnlessTrusted => true, | |
| AskForApproval::Never => false, | |
| AskForApproval::OnRequest => false, | |
| AskForApproval::Granular(granular_config) => granular_config.sandbox_approval, | |
| } | |
| } | |
| fn approval_action(&self, req: &Req, call_id: &str) -> std::io::Result<ApprovalAction>; | |
| } | |
| 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 step_context: Arc<StepContext>, | |
| pub cancellation_token: CancellationToken, | |
| pub call_id: String, | |
| pub tool_name: ToolName, | |
| } | |
| pub(crate) enum ToolError { | |
| Rejected(String), | |
| Codex(CodexErr), | |
| } | |
| pub(crate) trait ToolRuntime<Req, Out>: Approvable<Req> + Sandboxable { | |
| fn turn_environment<'a>(&self, req: &'a Req) -> &'a TurnEnvironment; | |
| fn uses_executor_managed_process_sandbox(&self, _req: &Req) -> bool { | |
| false | |
| } | |
| fn network_approval_spec(&self, _req: &Req, _ctx: &ToolCtx) -> Option<NetworkApprovalSpec> { | |
| None | |
| } | |
| fn sandbox_cwd<'a>(&self, _req: &'a Req) -> Option<&'a PathUri> { | |
| None | |
| } | |
| async fn run( | |
| &mut self, | |
| req: &Req, | |
| attempt: &SandboxAttempt<'_>, | |
| ctx: &ToolCtx, | |
| ) -> Result<Out, ToolError>; | |
| } | |
| pub(crate) struct SandboxAttempt<'a> { | |
| pub sandbox: SandboxType, | |
| /// Whether policy requested sandboxing, independent of this host's concrete wrapper. | |
| pub sandbox_requested: bool, | |
| pub permissions: &'a codex_protocol::models::PermissionProfile, | |
| /// Canonical permissions before this host materializes workspace roots. | |
| pub exec_server_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 [PathUri], | |
| pub sandbox_exe: Option<&'a std::path::PathBuf>, | |
| // TODO(anp): Reconcile these attempt settings with TurnEnvironment::sandbox_context | |
| // so process execution and patch writes honor the selected environment's backend. | |
| 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>, | |
| pub(crate) network_proxy: Option<&'a NetworkProxy>, | |
| } | |
| pub(crate) fn executor_windows_sandbox_level( | |
| windows_sandbox_level: WindowsSandboxLevel, | |
| cwd: &PathUri, | |
| ) -> WindowsSandboxLevel { | |
| if windows_sandbox_level == WindowsSandboxLevel::Disabled | |
| && cwd.infer_path_convention() == Some(PathConvention::Windows) | |
| { | |
| WindowsSandboxLevel::RestrictedToken | |
| } else { | |
| windows_sandbox_level | |
| } | |
| } | |
| pub(crate) fn executor_windows_sandbox_selection( | |
| windows_sandbox_level: WindowsSandboxLevel, | |
| cwd: &PathUri, | |
| ) -> WindowsSandboxSelection { | |
| if cwd.infer_path_convention() == Some(PathConvention::Windows) { | |
| executor_windows_sandbox_level(windows_sandbox_level, cwd).into() | |
| } else { | |
| WindowsSandboxSelection::Disabled | |
| } | |
| } | |
| impl<'a> SandboxAttempt<'a> { | |
| /// Whether this attempt bypasses sandboxing required by its ambient policy. | |
| /// Use the requested policy, not the controller's wrapper, for remote executors. | |
| pub(crate) fn is_escalated(&self) -> bool { | |
| !self.sandbox_requested | |
| && self.manager.should_sandbox( | |
| self.permissions, | |
| SandboxablePreference::Auto, | |
| self.enforce_managed_network, | |
| ) | |
| } | |
| pub(crate) fn network_proxy<'b>( | |
| &'b self, | |
| fallback: Option<&'b NetworkProxy>, | |
| ) -> Option<&'b NetworkProxy> { | |
| // Execution-only proxies need no fallback; offline attempts must not revive one. | |
| if self.enforce_managed_network { | |
| self.network_proxy.or(fallback) | |
| } else { | |
| None | |
| } | |
| } | |
| pub fn env_for( | |
| &self, | |
| command: SandboxCommand, | |
| options: ExecOptions, | |
| network: Option<&NetworkProxy>, | |
| environment_id: Option<&str>, | |
| ) -> Result<crate::sandboxing::ExecRequest, CodexErr> { | |
| let network = self.network_proxy(network); | |
| let request = self | |
| .manager | |
| .transform(SandboxTransformRequest { | |
| command, | |
| permissions: self.permissions, | |
| sandbox: self.sandbox, | |
| enforce_managed_network: self.enforce_managed_network, | |
| environment_id, | |
| network, | |
| sandbox_policy_cwd: self.sandbox_cwd, | |
| sandbox_exe: self.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)?; | |
| let workspace_roots = self | |
| .workspace_roots | |
| .iter() | |
| .map(PathUri::to_abs_path) | |
| .collect::<std::io::Result<Vec<_>>>()?; | |
| crate::sandboxing::ExecRequest::from_sandbox_exec_request(request, options, workspace_roots) | |
| } | |
| pub fn env_for_exec_server( | |
| &self, | |
| command: SandboxCommand, | |
| options: ExecOptions, | |
| ) -> Result<crate::sandboxing::ExecRequest, CodexErr> { | |
| let managed_network = command.managed_network.clone(); | |
| let exec_server_permissions = effective_permission_profile( | |
| self.exec_server_permissions, | |
| command.additional_permissions.as_ref(), | |
| ); | |
| let request = self | |
| .manager | |
| .transform(SandboxTransformRequest { | |
| command, | |
| permissions: self.permissions, | |
| // The exec-server must receive the native command, not this host's wrapper. | |
| sandbox: SandboxType::None, | |
| enforce_managed_network: self.enforce_managed_network, | |
| environment_id: None, | |
| network: None, | |
| sandbox_policy_cwd: self.sandbox_cwd, | |
| sandbox_exe: None, | |
| 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)?; | |
| let mut exec_request = crate::sandboxing::ExecRequest::from_sandbox_exec_request( | |
| request, | |
| options, | |
| Vec::new(), | |
| )?; | |
| exec_request.exec_server_managed_network = managed_network; | |
| if self.sandbox_requested { | |
| exec_request.exec_server_sandbox = Some(FileSystemSandboxContext { | |
| permissions: exec_server_permissions.into(), | |
| cwd: Some(exec_request.windows_sandbox_policy_cwd.clone()), | |
| workspace_roots: self.workspace_roots.to_vec(), | |
| user_home_dir: None, | |
| temporary_directories: None, | |
| windows_sandbox_selection: executor_windows_sandbox_selection( | |
| self.windows_sandbox_level, | |
| self.sandbox_cwd, | |
| ), | |
| windows_sandbox_private_desktop: self.windows_sandbox_private_desktop, | |
| windows_sandbox_proxy_settings_mode: None, | |
| use_legacy_landlock: self.use_legacy_landlock, | |
| }); | |
| exec_request.exec_server_enforce_managed_network = self.enforce_managed_network; | |
| } | |
| Ok(exec_request) | |
| } | |
| } | |
| mod tests; | |