diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index cc314a66e9..74a032a783 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -312,6 +312,7 @@ use crate::tasks::ReviewTask; use crate::tasks::SessionTask; use crate::tasks::SessionTaskContext; use crate::tools::ToolRouter; +use crate::tools::approval::ApprovalStore; use crate::tools::context::SharedTurnDiffTracker; use crate::tools::js_repl::JsReplHandle; use crate::tools::js_repl::resolve_compatible_node; @@ -320,7 +321,6 @@ use crate::tools::network_approval::build_blocked_request_observer; use crate::tools::network_approval::build_network_policy_decider; use crate::tools::parallel::ToolCallRuntime; use crate::tools::router::ToolRouterParams; -use crate::tools::sandboxing::ApprovalStore; use crate::turn_diff_tracker::TurnDiffTracker; use crate::turn_timing::TurnTimingState; use crate::turn_timing::record_turn_ttfm_metric; diff --git a/codex-rs/core/src/state/service.rs b/codex-rs/core/src/state/service.rs index 5db38f7b72..e4aca304d2 100644 --- a/codex-rs/core/src/state/service.rs +++ b/codex-rs/core/src/state/service.rs @@ -12,9 +12,9 @@ use crate::guardian::GuardianRejection; use crate::mcp::McpManager; use crate::plugins::PluginsManager; use crate::skills_watcher::SkillsWatcher; +use crate::tools::approval::ApprovalStore; use crate::tools::code_mode::CodeModeService; use crate::tools::network_approval::NetworkApprovalService; -use crate::tools::sandboxing::ApprovalStore; use crate::unified_exec::UnifiedExecProcessManager; use codex_analytics::AnalyticsEventsClient; use codex_exec_server::Environment; diff --git a/codex-rs/core/src/tools/approval.rs b/codex-rs/core/src/tools/approval.rs new file mode 100644 index 0000000000..d9bdc74823 --- /dev/null +++ b/codex-rs/core/src/tools/approval.rs @@ -0,0 +1,149 @@ +//! Shared approval coordination helpers used across tool and non-tool flows. + +use crate::codex::Session; +use crate::codex::TurnContext; +use crate::guardian::GuardianApprovalRequest; +use crate::guardian::new_guardian_review_id; +use crate::guardian::review_approval_request; +use crate::guardian::routes_approval_to_guardian; +use crate::state::SessionServices; +use codex_protocol::protocol::ReviewDecision; +use futures::Future; +use serde::Serialize; +use std::collections::HashMap; +use std::sync::Arc; + +#[derive(Clone, Default, Debug)] +pub(crate) struct ApprovalStore { + map: HashMap, +} + +impl ApprovalStore { + pub fn get(&self, key: &K) -> Option + where + K: Serialize, + { + let serialized_key = serde_json::to_string(key).ok()?; + self.map.get(&serialized_key).cloned() + } + + pub fn put(&mut self, key: K, value: ReviewDecision) + where + K: Serialize, + { + if let Ok(serialized_key) = serde_json::to_string(&key) { + self.map.insert(serialized_key, value); + } + } +} + +pub(crate) struct ApprovalOutcome { + pub decision: ReviewDecision, + pub guardian_review_id: Option, +} + +pub(crate) fn guardian_review_id_for_turn(turn: &TurnContext) -> Option { + routes_approval_to_guardian(turn).then(new_guardian_review_id) +} + +/// Returns an approve-for-session decision when every key is already cached, +/// otherwise calls `fetch` and stores any new session approval per key. +pub(crate) async fn with_cached_approval( + services: &SessionServices, + tool_name: &str, + keys: Vec, + fetch: F, +) -> ReviewDecision +where + K: Serialize, + F: FnOnce() -> Fut, + Fut: Future, +{ + 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) async fn request_approval( + session: &Arc, + turn: &Arc, + guardian_review_id: Option, + guardian_request: GuardianApprovalRequest, + retry_reason: Option, + request_user: F, +) -> ApprovalOutcome +where + F: FnOnce() -> UserFut, + UserFut: Future, +{ + if let Some(review_id) = guardian_review_id.clone() { + return ApprovalOutcome { + decision: review_approval_request( + session, + turn, + review_id, + guardian_request, + retry_reason, + ) + .await, + guardian_review_id, + }; + } + + ApprovalOutcome { + decision: request_user().await, + guardian_review_id: None, + } +} + +pub(crate) async fn request_approval_for_turn( + session: &Arc, + turn: &Arc, + guardian_request: GuardianApprovalRequest, + retry_reason: Option, + request_user: F, +) -> ApprovalOutcome +where + F: FnOnce() -> UserFut, + UserFut: Future, +{ + request_approval( + session, + turn, + guardian_review_id_for_turn(turn), + guardian_request, + retry_reason, + request_user, + ) + .await +} diff --git a/codex-rs/core/src/tools/mod.rs b/codex-rs/core/src/tools/mod.rs index 06c20d6922..ba70a6510c 100644 --- a/codex-rs/core/src/tools/mod.rs +++ b/codex-rs/core/src/tools/mod.rs @@ -1,3 +1,4 @@ +pub(crate) mod approval; pub(crate) mod code_mode; pub(crate) mod context; pub(crate) mod events; diff --git a/codex-rs/core/src/tools/network_approval.rs b/codex-rs/core/src/tools/network_approval.rs index c91b6df7ca..212d0e0186 100644 --- a/codex-rs/core/src/tools/network_approval.rs +++ b/codex-rs/core/src/tools/network_approval.rs @@ -2,10 +2,8 @@ use crate::codex::Session; use crate::guardian::GuardianApprovalRequest; use crate::guardian::guardian_rejection_message; use crate::guardian::guardian_timeout_message; -use crate::guardian::new_guardian_review_id; -use crate::guardian::review_approval_request; -use crate::guardian::routes_approval_to_guardian; use crate::network_policy_decision::denied_network_policy_message; +use crate::tools::approval::request_approval_for_turn; use crate::tools::sandboxing::ToolError; use codex_network_proxy::BlockedRequest; use codex_network_proxy::BlockedRequestObserver; @@ -370,46 +368,40 @@ impl NetworkApprovalService { protocol, }; let owner_call = self.resolve_single_active_call().await; - let guardian_approval_id = Self::approval_id_for_key(&key); - let use_guardian = routes_approval_to_guardian(&turn_context); - let guardian_review_id = use_guardian.then(new_guardian_review_id); - let approval_decision = if let Some(review_id) = guardian_review_id.clone() { - review_approval_request( - &session, - &turn_context, - review_id, - GuardianApprovalRequest::NetworkAccess { - id: guardian_approval_id.clone(), - turn_id: owner_call - .as_ref() - .map_or_else(|| turn_context.sub_id.clone(), |call| call.turn_id.clone()), - target, - host: request.host, - protocol, - port: key.port, - }, - Some(policy_denial_message.clone()), - ) - .await - } else { - let approval_id = Self::approval_id_for_key(&key); - let prompt_command = vec!["network-access".to_string(), target.clone()]; - let available_decisions = None; - session - .request_command_approval( - turn_context.as_ref(), - approval_id, - /*approval_id*/ None, - prompt_command, - turn_context.cwd.clone(), - Some(prompt_reason), - Some(network_approval_context.clone()), - /*proposed_execpolicy_amendment*/ None, - /*additional_permissions*/ None, - available_decisions, - ) - .await - }; + let approval_outcome = request_approval_for_turn( + &session, + &turn_context, + GuardianApprovalRequest::NetworkAccess { + id: Self::approval_id_for_key(&key), + turn_id: owner_call + .as_ref() + .map_or_else(|| turn_context.sub_id.clone(), |call| call.turn_id.clone()), + target: target.clone(), + host: request.host.clone(), + protocol, + port: key.port, + }, + Some(policy_denial_message.clone()), + || async { + session + .request_command_approval( + turn_context.as_ref(), + Self::approval_id_for_key(&key), + /*approval_id*/ None, + vec!["network-access".to_string(), target.clone()], + turn_context.cwd.clone(), + Some(prompt_reason), + Some(network_approval_context.clone()), + /*proposed_execpolicy_amendment*/ None, + /*additional_permissions*/ None, + None, + ) + .await + }, + ) + .await; + let guardian_review_id = approval_outcome.guardian_review_id; + let approval_decision = approval_outcome.decision; let mut cache_session_deny = false; let resolved = match approval_decision { diff --git a/codex-rs/core/src/tools/runtimes/apply_patch.rs b/codex-rs/core/src/tools/runtimes/apply_patch.rs index 1d5c8a1e36..f346f6b7dd 100644 --- a/codex-rs/core/src/tools/runtimes/apply_patch.rs +++ b/codex-rs/core/src/tools/runtimes/apply_patch.rs @@ -5,7 +5,8 @@ //! sandboxing enforced by the explicit filesystem sandbox context. use crate::exec::is_likely_sandbox_denied; use crate::guardian::GuardianApprovalRequest; -use crate::guardian::review_approval_request; +use crate::tools::approval::request_approval; +use crate::tools::approval::with_cached_approval; use crate::tools::sandboxing::Approvable; use crate::tools::sandboxing::ApprovalCtx; use crate::tools::sandboxing::ExecApprovalRequirement; @@ -14,7 +15,6 @@ use crate::tools::sandboxing::Sandboxable; use crate::tools::sandboxing::ToolCtx; use crate::tools::sandboxing::ToolError; use crate::tools::sandboxing::ToolRuntime; -use crate::tools::sandboxing::with_cached_approval; use codex_apply_patch::ApplyPatchAction; use codex_exec_server::FileSystemSandboxContext; use codex_protocol::error::CodexErr; @@ -127,42 +127,54 @@ impl Approvable for ApplyPatchRuntime { let retry_reason = ctx.retry_reason.clone(); let approval_keys = self.approval_keys(req); let changes = req.changes.clone(); - let guardian_review_id = ctx.guardian_review_id.clone(); Box::pin(async move { if req.permissions_preapproved && retry_reason.is_none() { return ReviewDecision::Approved; } - if let Some(review_id) = guardian_review_id { - let action = ApplyPatchRuntime::build_guardian_review_request(req, ctx.call_id); - return review_approval_request(session, turn, review_id, action, retry_reason) - .await; - } - if let Some(reason) = retry_reason { - let rx_approve = session - .request_patch_approval( - turn, - call_id, - changes.clone(), - Some(reason), - /*grant_root*/ None, - ) - .await; - return rx_approve.await.unwrap_or_default(); + if let Some(reason) = retry_reason.clone() { + return request_approval( + session, + turn, + ctx.guardian_review_id.clone(), + ApplyPatchRuntime::build_guardian_review_request(req, ctx.call_id), + Some(reason.clone()), + || async move { + let rx_approve = session + .request_patch_approval( + turn, + call_id, + changes.clone(), + Some(reason), + /*grant_root*/ None, + ) + .await; + rx_approve.await.unwrap_or_default() + }, + ) + .await + .decision; } - with_cached_approval( - &session.services, - "apply_patch", - approval_keys, - || async move { - let rx_approve = session - .request_patch_approval( - turn, call_id, changes, /*reason*/ None, /*grant_root*/ None, - ) - .await; - rx_approve.await.unwrap_or_default() - }, - ) + with_cached_approval(&session.services, "apply_patch", approval_keys, || async { + request_approval( + session, + turn, + ctx.guardian_review_id.clone(), + ApplyPatchRuntime::build_guardian_review_request(req, ctx.call_id), + retry_reason, + || async move { + let rx_approve = session + .request_patch_approval( + turn, call_id, changes, /*reason*/ None, + /*grant_root*/ None, + ) + .await; + rx_approve.await.unwrap_or_default() + }, + ) + .await + .decision + }) .await }) } diff --git a/codex-rs/core/src/tools/runtimes/shell.rs b/codex-rs/core/src/tools/runtimes/shell.rs index 203553bf87..34350d3b3d 100644 --- a/codex-rs/core/src/tools/runtimes/shell.rs +++ b/codex-rs/core/src/tools/runtimes/shell.rs @@ -11,11 +11,12 @@ pub(crate) mod zsh_fork_backend; use crate::command_canonicalization::canonicalize_command_for_approval; use crate::exec::ExecCapturePolicy; use crate::guardian::GuardianApprovalRequest; -use crate::guardian::review_approval_request; use crate::sandboxing::ExecOptions; use crate::sandboxing::SandboxPermissions; use crate::sandboxing::execute_env; use crate::shell::ShellType; +use crate::tools::approval::request_approval; +use crate::tools::approval::with_cached_approval; use crate::tools::network_approval::NetworkApprovalMode; use crate::tools::network_approval::NetworkApprovalSpec; use crate::tools::runtimes::build_sandbox_command; @@ -30,7 +31,6 @@ use crate::tools::sandboxing::ToolCtx; use crate::tools::sandboxing::ToolError; use crate::tools::sandboxing::ToolRuntime; use crate::tools::sandboxing::sandbox_override_for_first_attempt; -use crate::tools::sandboxing::with_cached_approval; use codex_network_proxy::NetworkProxy; use codex_protocol::exec_output::ExecToolCallOutput; use codex_protocol::models::PermissionProfile; @@ -151,43 +151,42 @@ impl Approvable for ShellRuntime { let session = ctx.session; let turn = ctx.turn; let call_id = ctx.call_id.to_string(); - let guardian_review_id = ctx.guardian_review_id.clone(); Box::pin(async move { - if let Some(review_id) = guardian_review_id { - return review_approval_request( + with_cached_approval(&session.services, "shell", keys, || async { + request_approval( session, turn, - review_id, + ctx.guardian_review_id.clone(), GuardianApprovalRequest::Shell { - id: call_id, - command, + id: call_id.clone(), + command: command.clone(), cwd: cwd.clone(), sandbox_permissions: req.sandbox_permissions, additional_permissions: req.additional_permissions.clone(), justification: req.justification.clone(), }, retry_reason, + || async move { + session + .request_command_approval( + turn, + call_id, + /*approval_id*/ None, + command, + cwd, + reason, + ctx.network_approval_context.clone(), + req.exec_approval_requirement + .proposed_execpolicy_amendment() + .cloned(), + req.additional_permissions.clone(), + None, + ) + .await + }, ) - .await; - } - with_cached_approval(&session.services, "shell", keys, move || async move { - let available_decisions = None; - session - .request_command_approval( - turn, - call_id, - /*approval_id*/ None, - command, - cwd, - reason, - ctx.network_approval_context.clone(), - req.exec_approval_requirement - .proposed_execpolicy_amendment() - .cloned(), - req.additional_permissions.clone(), - available_decisions, - ) - .await + .await + .decision }) .await }) diff --git a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs index dd1e714104..5f89fa6451 100644 --- a/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs +++ b/codex-rs/core/src/tools/runtimes/shell/unix_escalation.rs @@ -5,13 +5,11 @@ use crate::exec::is_likely_sandbox_denied; use crate::guardian::GuardianApprovalRequest; use crate::guardian::guardian_rejection_message; use crate::guardian::guardian_timeout_message; -use crate::guardian::new_guardian_review_id; -use crate::guardian::review_approval_request; -use crate::guardian::routes_approval_to_guardian; use crate::sandboxing::ExecOptions; use crate::sandboxing::ExecRequest; use crate::sandboxing::SandboxPermissions; use crate::shell::ShellType; +use crate::tools::approval::request_approval_for_turn; use crate::tools::runtimes::build_sandbox_command; use crate::tools::sandboxing::SandboxAttempt; use crate::tools::sandboxing::ToolCtx; @@ -392,47 +390,43 @@ impl CoreShellActionProvider { let call_id = self.call_id.clone(); let approval_id = Some(Uuid::new_v4().to_string()); let source = self.tool_name; - let guardian_review_id = routes_approval_to_guardian(&turn).then(new_guardian_review_id); Ok(stopwatch .pause_for(async move { - if let Some(review_id) = guardian_review_id.clone() { - let decision = review_approval_request( - &session, - &turn, - review_id, - GuardianApprovalRequest::Execve { - id: call_id.clone(), - source, - program: program.to_string_lossy().into_owned(), - argv: argv.to_vec(), - cwd: workdir.clone(), - additional_permissions, - }, - /*retry_reason*/ None, - ) - .await; - return PromptDecision { - decision, - guardian_review_id, - }; - } - let decision = session - .request_command_approval( - &turn, - call_id, - approval_id, - command, - workdir.clone(), - /*reason*/ None, - /*network_approval_context*/ None, - /*proposed_execpolicy_amendment*/ None, - additional_permissions, - Some(vec![ReviewDecision::Approved, ReviewDecision::Abort]), - ) - .await; + let prompt_session = session.clone(); + let prompt_turn = turn.clone(); + let outcome = request_approval_for_turn( + &session, + &turn, + GuardianApprovalRequest::Execve { + id: call_id.clone(), + source, + program: program.to_string_lossy().into_owned(), + argv: argv.to_vec(), + cwd: workdir.clone(), + additional_permissions: additional_permissions.clone(), + }, + None, + || async move { + prompt_session + .request_command_approval( + &prompt_turn, + call_id, + approval_id, + command, + workdir.clone(), + /*reason*/ None, + /*network_approval_context*/ None, + /*proposed_execpolicy_amendment*/ None, + additional_permissions, + Some(vec![ReviewDecision::Approved, ReviewDecision::Abort]), + ) + .await + }, + ) + .await; PromptDecision { - decision, - guardian_review_id: None, + decision: outcome.decision, + guardian_review_id: outcome.guardian_review_id, } }) .await) diff --git a/codex-rs/core/src/tools/runtimes/unified_exec.rs b/codex-rs/core/src/tools/runtimes/unified_exec.rs index b241d47389..7604bffbc9 100644 --- a/codex-rs/core/src/tools/runtimes/unified_exec.rs +++ b/codex-rs/core/src/tools/runtimes/unified_exec.rs @@ -8,11 +8,12 @@ use crate::command_canonicalization::canonicalize_command_for_approval; use crate::exec::ExecCapturePolicy; use crate::exec::ExecExpiration; use crate::guardian::GuardianApprovalRequest; -use crate::guardian::review_approval_request; use crate::sandboxing::ExecOptions; use crate::sandboxing::ExecServerEnvConfig; use crate::sandboxing::SandboxPermissions; use crate::shell::ShellType; +use crate::tools::approval::request_approval; +use crate::tools::approval::with_cached_approval; use crate::tools::network_approval::NetworkApprovalMode; use crate::tools::network_approval::NetworkApprovalSpec; use crate::tools::runtimes::build_sandbox_command; @@ -28,7 +29,6 @@ use crate::tools::sandboxing::ToolCtx; use crate::tools::sandboxing::ToolError; use crate::tools::sandboxing::ToolRuntime; use crate::tools::sandboxing::sandbox_override_for_first_attempt; -use crate::tools::sandboxing::with_cached_approval; use crate::unified_exec::NoopSpawnLifecycle; use crate::unified_exec::UnifiedExecError; use crate::unified_exec::UnifiedExecProcess; @@ -129,16 +129,15 @@ impl Approvable for UnifiedExecRuntime<'_> { let cwd = req.cwd.clone(); let retry_reason = ctx.retry_reason.clone(); let reason = retry_reason.clone().or_else(|| req.justification.clone()); - let guardian_review_id = ctx.guardian_review_id.clone(); Box::pin(async move { - if let Some(review_id) = guardian_review_id { - return review_approval_request( + with_cached_approval(&session.services, "unified_exec", keys, || async { + request_approval( session, turn, - review_id, + ctx.guardian_review_id.clone(), GuardianApprovalRequest::ExecCommand { - id: call_id, - command, + id: call_id.clone(), + command: command.clone(), cwd: cwd.clone(), sandbox_permissions: req.sandbox_permissions, additional_permissions: req.additional_permissions.clone(), @@ -146,27 +145,27 @@ impl Approvable for UnifiedExecRuntime<'_> { tty: req.tty, }, retry_reason, + || async move { + session + .request_command_approval( + turn, + call_id, + /*approval_id*/ None, + command, + cwd.clone(), + reason, + ctx.network_approval_context.clone(), + req.exec_approval_requirement + .proposed_execpolicy_amendment() + .cloned(), + req.additional_permissions.clone(), + None, + ) + .await + }, ) - .await; - } - with_cached_approval(&session.services, "unified_exec", keys, || async move { - let available_decisions = None; - session - .request_command_approval( - turn, - call_id, - /*approval_id*/ None, - command, - cwd.clone(), - reason, - ctx.network_approval_context.clone(), - req.exec_approval_requirement - .proposed_execpolicy_amendment() - .cloned(), - req.additional_permissions.clone(), - available_decisions, - ) - .await + .await + .decision }) .await }) diff --git a/codex-rs/core/src/tools/sandboxing.rs b/codex-rs/core/src/tools/sandboxing.rs index 48543bdcd3..4c32397b80 100644 --- a/codex-rs/core/src/tools/sandboxing.rs +++ b/codex-rs/core/src/tools/sandboxing.rs @@ -1,14 +1,13 @@ //! 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.). +//! Consolidates the sandbox orchestration traits and helpers used by tool +//! runtimes (`ApprovalCtx`, `Approvable`, `Sandboxable`, `ToolRuntime`, +//! `SandboxAttempt`, etc.). use crate::codex::Session; use crate::codex::TurnContext; use crate::sandboxing::ExecOptions; use crate::sandboxing::SandboxPermissions; -use crate::state::SessionServices; use crate::tools::network_approval::NetworkApprovalSpec; use codex_network_proxy::NetworkProxy; use codex_protocol::approvals::ExecPolicyAmendment; @@ -28,93 +27,12 @@ use codex_sandboxing::SandboxTransformRequest; use codex_sandboxing::SandboxType; use codex_sandboxing::SandboxablePreference; use codex_utils_absolute_path::AbsolutePathBuf; -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; -#[derive(Clone, Default, Debug)] -pub(crate) struct ApprovalStore { - // Store serialized keys for generic caching across requests. - map: HashMap, -} - -impl ApprovalStore { - pub fn get(&self, key: &K) -> Option - where - K: Serialize, - { - let s = serde_json::to_string(key).ok()?; - self.map.get(&s).cloned() - } - - pub fn put(&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( - services: &SessionServices, - // Name of the tool, used for metrics collection. - tool_name: &str, - keys: Vec, - fetch: F, -) -> ReviewDecision -where - K: Serialize, - F: FnOnce() -> Fut, - Fut: Future, -{ - // 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,