mirror of
https://github.com/openai/codex.git
synced 2026-05-01 03:42:05 +03:00
Add Smart Approvals guardian review across core, app-server, and TUI (#13860)
## Summary
- add `approvals_reviewer = "user" | "guardian_subagent"` as the runtime
control for who reviews approval requests
- route Smart Approvals guardian review through core for command
execution, file changes, managed-network approvals, MCP approvals, and
delegated/subagent approval flows
- expose guardian review in app-server with temporary unstable
`item/autoApprovalReview/{started,completed}` notifications carrying
`targetItemId`, `review`, and `action`
- update the TUI so Smart Approvals can be enabled from `/experimental`,
aligned with the matching `/approvals` mode, and surfaced clearly while
reviews are pending or resolved
## Runtime model
This PR does not introduce a new `approval_policy`.
Instead:
- `approval_policy` still controls when approval is needed
- `approvals_reviewer` controls who reviewable approval requests are
routed to:
- `user`
- `guardian_subagent`
`guardian_subagent` is a carefully prompted reviewer subagent that
gathers relevant context and applies a risk-based decision framework
before approving or denying the request.
The `smart_approvals` feature flag is a rollout/UI gate. Core runtime
behavior keys off `approvals_reviewer`.
When Smart Approvals is enabled from the TUI, it also switches the
current `/approvals` settings to the matching Smart Approvals mode so
users immediately see guardian review in the active thread:
- `approval_policy = on-request`
- `approvals_reviewer = guardian_subagent`
- `sandbox_mode = workspace-write`
Users can still change `/approvals` afterward.
Config-load behavior stays intentionally narrow:
- plain `smart_approvals = true` in `config.toml` remains just the
rollout/UI gate and does not auto-set `approvals_reviewer`
- the deprecated `guardian_approval = true` alias migration does
backfill `approvals_reviewer = "guardian_subagent"` in the same scope
when that reviewer is not already configured there, so old configs
preserve their original guardian-enabled behavior
ARC remains a separate safety check. For MCP tool approvals, ARC
escalations now flow into the configured reviewer instead of always
bypassing guardian and forcing manual review.
## Config stability
The runtime reviewer override is stable, but the config-backed
app-server protocol shape is still settling.
- `thread/start`, `thread/resume`, and `turn/start` keep stable
`approvalsReviewer` overrides
- the config-backed `approvals_reviewer` exposure returned via
`config/read` (including profile-level config) is now marked
`[UNSTABLE]` / experimental in the app-server protocol until we are more
confident in that config surface
## App-server surface
This PR intentionally keeps the guardian app-server shape narrow and
temporary.
It adds generic unstable lifecycle notifications:
- `item/autoApprovalReview/started`
- `item/autoApprovalReview/completed`
with payloads of the form:
- `{ threadId, turnId, targetItemId, review, action? }`
`review` is currently:
- `{ status, riskScore?, riskLevel?, rationale? }`
- where `status` is one of `inProgress`, `approved`, `denied`, or
`aborted`
`action` carries the guardian action summary payload from core when
available. This lets clients render temporary standalone pending-review
UI, including parallel reviews, even when the underlying tool item has
not been emitted yet.
These notifications are explicitly documented as `[UNSTABLE]` and
expected to change soon.
This PR does **not** persist guardian review state onto `thread/read`
tool items. The intended follow-up is to attach guardian review state to
the reviewed tool item lifecycle instead, which would improve
consistency with manual approvals and allow thread history / reconnect
flows to replay guardian review state directly.
## TUI behavior
- `/experimental` exposes the rollout gate as `Smart Approvals`
- enabling it in the TUI enables the feature and switches the current
session to the matching Smart Approvals `/approvals` mode
- disabling it in the TUI clears the persisted `approvals_reviewer`
override when appropriate and returns the session to default manual
review when the effective reviewer changes
- `/approvals` still exposes the reviewer choice directly
- the TUI renders:
- pending guardian review state in the live status footer, including
parallel review aggregation
- resolved approval/denial state in history
## Scope notes
This PR includes the supporting core/runtime work needed to make Smart
Approvals usable end-to-end:
- shell / unified-exec / apply_patch / managed-network / MCP guardian
review
- delegated/subagent approval routing into guardian review
- guardian review risk metadata and action summaries for app-server/TUI
- config/profile/TUI handling for `smart_approvals`, `guardian_approval`
alias migration, and `approvals_reviewer`
- a small internal cleanup of delegated approval forwarding to dedupe
fallback paths and simplify guardian-vs-parent approval waiting (no
intended behavior change)
Out of scope for this PR:
- redesigning the existing manual approval protocol shapes
- persisting guardian review state onto app-server `ThreadItem`s
- delegated MCP elicitation auto-review (the current delegated MCP
guardian shim only covers the legacy `RequestUserInput` path)
---------
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
committed by
GitHub
parent
e3cbf913e8
commit
bc24017d64
@@ -8,8 +8,10 @@ use codex_protocol::protocol::ApplyPatchApprovalRequestEvent;
|
||||
use codex_protocol::protocol::Event;
|
||||
use codex_protocol::protocol::EventMsg;
|
||||
use codex_protocol::protocol::ExecApprovalRequestEvent;
|
||||
use codex_protocol::protocol::McpInvocation;
|
||||
use codex_protocol::protocol::Op;
|
||||
use codex_protocol::protocol::RequestUserInputEvent;
|
||||
use codex_protocol::protocol::ReviewDecision;
|
||||
use codex_protocol::protocol::SessionSource;
|
||||
use codex_protocol::protocol::SubAgentSource;
|
||||
use codex_protocol::protocol::Submission;
|
||||
@@ -20,8 +22,11 @@ use codex_protocol::request_permissions::RequestPermissionsResponse;
|
||||
use codex_protocol::request_user_input::RequestUserInputArgs;
|
||||
use codex_protocol::request_user_input::RequestUserInputResponse;
|
||||
use codex_protocol::user_input::UserInput;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use serde_json::Value;
|
||||
use std::time::Duration;
|
||||
use tokio::sync::Mutex;
|
||||
use tokio::sync::oneshot;
|
||||
use tokio::time::timeout;
|
||||
use tokio_util::sync::CancellationToken;
|
||||
|
||||
@@ -34,6 +39,15 @@ use crate::codex::Session;
|
||||
use crate::codex::TurnContext;
|
||||
use crate::config::Config;
|
||||
use crate::error::CodexErr;
|
||||
use crate::guardian::GuardianApprovalRequest;
|
||||
use crate::guardian::review_approval_request_with_cancel;
|
||||
use crate::guardian::routes_approval_to_guardian;
|
||||
use crate::mcp_tool_call::MCP_TOOL_APPROVAL_ACCEPT;
|
||||
use crate::mcp_tool_call::MCP_TOOL_APPROVAL_ACCEPT_FOR_SESSION;
|
||||
use crate::mcp_tool_call::MCP_TOOL_APPROVAL_DECLINE_SYNTHETIC;
|
||||
use crate::mcp_tool_call::build_guardian_mcp_tool_review_request;
|
||||
use crate::mcp_tool_call::is_mcp_tool_approval_question_id;
|
||||
use crate::mcp_tool_call::lookup_mcp_tool_metadata;
|
||||
use crate::models_manager::manager::ModelsManager;
|
||||
use codex_protocol::protocol::InitialHistory;
|
||||
|
||||
@@ -88,12 +102,17 @@ pub(crate) async fn run_codex_thread_interactive(
|
||||
let parent_session_clone = Arc::clone(&parent_session);
|
||||
let parent_ctx_clone = Arc::clone(&parent_ctx);
|
||||
let codex_for_events = Arc::clone(&codex);
|
||||
// Cache delegated MCP invocations so guardian can recover the full tool call
|
||||
// context when the later legacy RequestUserInput approval event only carries
|
||||
// a call_id plus approval question metadata.
|
||||
let pending_mcp_invocations = Arc::new(Mutex::new(HashMap::<String, McpInvocation>::new()));
|
||||
tokio::spawn(async move {
|
||||
forward_events(
|
||||
codex_for_events,
|
||||
tx_sub,
|
||||
parent_session_clone,
|
||||
parent_ctx_clone,
|
||||
pending_mcp_invocations,
|
||||
cancel_token_events,
|
||||
)
|
||||
.await;
|
||||
@@ -200,6 +219,7 @@ async fn forward_events(
|
||||
tx_sub: Sender<Event>,
|
||||
parent_session: Arc<Session>,
|
||||
parent_ctx: Arc<TurnContext>,
|
||||
pending_mcp_invocations: Arc<Mutex<HashMap<String, McpInvocation>>>,
|
||||
cancel_token: CancellationToken,
|
||||
) {
|
||||
let cancelled = cancel_token.cancelled();
|
||||
@@ -285,18 +305,57 @@ async fn forward_events(
|
||||
id,
|
||||
&parent_session,
|
||||
&parent_ctx,
|
||||
&pending_mcp_invocations,
|
||||
event,
|
||||
&cancel_token,
|
||||
)
|
||||
.await;
|
||||
}
|
||||
Event {
|
||||
id,
|
||||
msg: EventMsg::McpToolCallBegin(event),
|
||||
} => {
|
||||
pending_mcp_invocations
|
||||
.lock()
|
||||
.await
|
||||
.insert(event.call_id.clone(), event.invocation.clone());
|
||||
if !forward_event_or_shutdown(
|
||||
&codex,
|
||||
&tx_sub,
|
||||
&cancel_token,
|
||||
Event {
|
||||
id,
|
||||
msg: EventMsg::McpToolCallBegin(event),
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
Event {
|
||||
id,
|
||||
msg: EventMsg::McpToolCallEnd(event),
|
||||
} => {
|
||||
pending_mcp_invocations.lock().await.remove(&event.call_id);
|
||||
if !forward_event_or_shutdown(
|
||||
&codex,
|
||||
&tx_sub,
|
||||
&cancel_token,
|
||||
Event {
|
||||
id,
|
||||
msg: EventMsg::McpToolCallEnd(event),
|
||||
},
|
||||
)
|
||||
.await
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
other => {
|
||||
match tx_sub.send(other).or_cancel(&cancel_token).await {
|
||||
Ok(Ok(())) => {}
|
||||
_ => {
|
||||
shutdown_delegate(&codex).await;
|
||||
break;
|
||||
}
|
||||
if !forward_event_or_shutdown(&codex, &tx_sub, &cancel_token, other).await
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -323,6 +382,21 @@ async fn shutdown_delegate(codex: &Codex) {
|
||||
.await;
|
||||
}
|
||||
|
||||
async fn forward_event_or_shutdown(
|
||||
codex: &Codex,
|
||||
tx_sub: &Sender<Event>,
|
||||
cancel_token: &CancellationToken,
|
||||
event: Event,
|
||||
) -> bool {
|
||||
match tx_sub.send(event).or_cancel(cancel_token).await {
|
||||
Ok(Ok(())) => true,
|
||||
_ => {
|
||||
shutdown_delegate(codex).await;
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Forward ops from a caller to a sub-agent, respecting cancellation.
|
||||
async fn forward_ops(
|
||||
codex: Arc<Codex>,
|
||||
@@ -342,8 +416,8 @@ async fn forward_ops(
|
||||
async fn handle_exec_approval(
|
||||
codex: &Codex,
|
||||
turn_id: String,
|
||||
parent_session: &Session,
|
||||
parent_ctx: &TurnContext,
|
||||
parent_session: &Arc<Session>,
|
||||
parent_ctx: &Arc<TurnContext>,
|
||||
event: ExecApprovalRequestEvent,
|
||||
cancel_token: &CancellationToken,
|
||||
) {
|
||||
@@ -361,27 +435,56 @@ async fn handle_exec_approval(
|
||||
available_decisions,
|
||||
..
|
||||
} = event;
|
||||
// Race approval with cancellation and timeout to avoid hangs.
|
||||
let approval_fut = parent_session.request_command_approval(
|
||||
parent_ctx,
|
||||
call_id,
|
||||
approval_id,
|
||||
command,
|
||||
cwd,
|
||||
reason,
|
||||
network_approval_context,
|
||||
proposed_execpolicy_amendment,
|
||||
additional_permissions,
|
||||
skill_metadata,
|
||||
available_decisions,
|
||||
);
|
||||
let decision = await_approval_with_cancel(
|
||||
approval_fut,
|
||||
parent_session,
|
||||
&approval_id_for_op,
|
||||
cancel_token,
|
||||
)
|
||||
.await;
|
||||
let decision = if routes_approval_to_guardian(parent_ctx) {
|
||||
let review_cancel = cancel_token.child_token();
|
||||
let review_rx = spawn_guardian_review(
|
||||
Arc::clone(parent_session),
|
||||
Arc::clone(parent_ctx),
|
||||
GuardianApprovalRequest::Shell {
|
||||
id: call_id.clone(),
|
||||
command,
|
||||
cwd,
|
||||
sandbox_permissions: if additional_permissions.is_some() {
|
||||
crate::sandboxing::SandboxPermissions::WithAdditionalPermissions
|
||||
} else {
|
||||
crate::sandboxing::SandboxPermissions::UseDefault
|
||||
},
|
||||
additional_permissions,
|
||||
justification: None,
|
||||
},
|
||||
reason,
|
||||
review_cancel.clone(),
|
||||
);
|
||||
await_approval_with_cancel(
|
||||
async move { review_rx.await.unwrap_or_default() },
|
||||
parent_session,
|
||||
&approval_id_for_op,
|
||||
cancel_token,
|
||||
Some(&review_cancel),
|
||||
)
|
||||
.await
|
||||
} else {
|
||||
await_approval_with_cancel(
|
||||
parent_session.request_command_approval(
|
||||
parent_ctx,
|
||||
call_id,
|
||||
approval_id,
|
||||
command,
|
||||
cwd,
|
||||
reason,
|
||||
network_approval_context,
|
||||
proposed_execpolicy_amendment,
|
||||
additional_permissions,
|
||||
skill_metadata,
|
||||
available_decisions,
|
||||
),
|
||||
parent_session,
|
||||
&approval_id_for_op,
|
||||
cancel_token,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
};
|
||||
|
||||
let _ = codex
|
||||
.submit(Op::ExecApproval {
|
||||
@@ -396,8 +499,8 @@ async fn handle_exec_approval(
|
||||
async fn handle_patch_approval(
|
||||
codex: &Codex,
|
||||
_id: String,
|
||||
parent_session: &Session,
|
||||
parent_ctx: &TurnContext,
|
||||
parent_session: &Arc<Session>,
|
||||
parent_ctx: &Arc<TurnContext>,
|
||||
event: ApplyPatchApprovalRequestEvent,
|
||||
cancel_token: &CancellationToken,
|
||||
) {
|
||||
@@ -409,16 +512,85 @@ async fn handle_patch_approval(
|
||||
..
|
||||
} = event;
|
||||
let approval_id = call_id.clone();
|
||||
let decision_rx = parent_session
|
||||
.request_patch_approval(parent_ctx, call_id, changes, reason, grant_root)
|
||||
.await;
|
||||
let decision = await_approval_with_cancel(
|
||||
async move { decision_rx.await.unwrap_or_default() },
|
||||
parent_session,
|
||||
&approval_id,
|
||||
cancel_token,
|
||||
)
|
||||
.await;
|
||||
let guardian_decision = if routes_approval_to_guardian(parent_ctx) {
|
||||
let change_count = changes.len();
|
||||
let maybe_files = changes
|
||||
.keys()
|
||||
.map(|path| AbsolutePathBuf::from_absolute_path(parent_ctx.cwd.join(path)).ok())
|
||||
.collect::<Option<Vec<_>>>();
|
||||
if let Some(files) = maybe_files {
|
||||
let review_cancel = cancel_token.child_token();
|
||||
let patch = changes
|
||||
.iter()
|
||||
.map(|(path, change)| match change {
|
||||
codex_protocol::protocol::FileChange::Add { content } => {
|
||||
format!("*** Add File: {}\n{}", path.display(), content)
|
||||
}
|
||||
codex_protocol::protocol::FileChange::Delete { content } => {
|
||||
format!("*** Delete File: {}\n{}", path.display(), content)
|
||||
}
|
||||
codex_protocol::protocol::FileChange::Update {
|
||||
unified_diff,
|
||||
move_path,
|
||||
} => {
|
||||
if let Some(move_path) = move_path {
|
||||
format!(
|
||||
"*** Update File: {}\n*** Move to: {}\n{}",
|
||||
path.display(),
|
||||
move_path.display(),
|
||||
unified_diff
|
||||
)
|
||||
} else {
|
||||
format!("*** Update File: {}\n{}", path.display(), unified_diff)
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let review_rx = spawn_guardian_review(
|
||||
Arc::clone(parent_session),
|
||||
Arc::clone(parent_ctx),
|
||||
GuardianApprovalRequest::ApplyPatch {
|
||||
id: approval_id.clone(),
|
||||
cwd: parent_ctx.cwd.clone(),
|
||||
files,
|
||||
change_count,
|
||||
patch,
|
||||
},
|
||||
reason.clone(),
|
||||
review_cancel.clone(),
|
||||
);
|
||||
Some(
|
||||
await_approval_with_cancel(
|
||||
async move { review_rx.await.unwrap_or_default() },
|
||||
parent_session,
|
||||
&approval_id,
|
||||
cancel_token,
|
||||
Some(&review_cancel),
|
||||
)
|
||||
.await,
|
||||
)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let decision = if let Some(decision) = guardian_decision {
|
||||
decision
|
||||
} else {
|
||||
let decision_rx = parent_session
|
||||
.request_patch_approval(parent_ctx, call_id, changes, reason, grant_root)
|
||||
.await;
|
||||
await_approval_with_cancel(
|
||||
async move { decision_rx.await.unwrap_or_default() },
|
||||
parent_session,
|
||||
&approval_id,
|
||||
cancel_token,
|
||||
None,
|
||||
)
|
||||
.await
|
||||
};
|
||||
let _ = codex
|
||||
.submit(Op::PatchApproval {
|
||||
id: approval_id,
|
||||
@@ -430,11 +602,26 @@ async fn handle_patch_approval(
|
||||
async fn handle_request_user_input(
|
||||
codex: &Codex,
|
||||
id: String,
|
||||
parent_session: &Session,
|
||||
parent_ctx: &TurnContext,
|
||||
parent_session: &Arc<Session>,
|
||||
parent_ctx: &Arc<TurnContext>,
|
||||
pending_mcp_invocations: &Arc<Mutex<HashMap<String, McpInvocation>>>,
|
||||
event: RequestUserInputEvent,
|
||||
cancel_token: &CancellationToken,
|
||||
) {
|
||||
if routes_approval_to_guardian(parent_ctx)
|
||||
&& let Some(response) = maybe_auto_review_mcp_request_user_input(
|
||||
parent_session,
|
||||
parent_ctx,
|
||||
pending_mcp_invocations,
|
||||
&event,
|
||||
cancel_token,
|
||||
)
|
||||
.await
|
||||
{
|
||||
let _ = codex.submit(Op::UserInputAnswer { id, response }).await;
|
||||
return;
|
||||
}
|
||||
|
||||
let args = RequestUserInputArgs {
|
||||
questions: event.questions,
|
||||
};
|
||||
@@ -450,10 +637,115 @@ async fn handle_request_user_input(
|
||||
let _ = codex.submit(Op::UserInputAnswer { id, response }).await;
|
||||
}
|
||||
|
||||
/// Intercepts delegated legacy MCP approval prompts on the RequestUserInput
|
||||
/// compatibility path and, when guardian is active, answers them
|
||||
/// programmatically after running the guardian review.
|
||||
///
|
||||
/// The RequestUserInput event only carries `call_id` plus approval question
|
||||
/// metadata, so this helper joins it back to the cached `McpToolCallBegin`
|
||||
/// invocation in order to rebuild the full guardian review request.
|
||||
async fn maybe_auto_review_mcp_request_user_input(
|
||||
parent_session: &Arc<Session>,
|
||||
parent_ctx: &Arc<TurnContext>,
|
||||
pending_mcp_invocations: &Arc<Mutex<HashMap<String, McpInvocation>>>,
|
||||
event: &RequestUserInputEvent,
|
||||
cancel_token: &CancellationToken,
|
||||
) -> Option<RequestUserInputResponse> {
|
||||
// TODO(ccunningham): Support delegated MCP approval elicitations here too after
|
||||
// coordinating with @fouad. Today guardian only auto-reviews the RequestUserInput
|
||||
// compatibility path for delegated MCP approvals.
|
||||
let question = event
|
||||
.questions
|
||||
.iter()
|
||||
.find(|question| is_mcp_tool_approval_question_id(&question.id))?;
|
||||
let invocation = pending_mcp_invocations
|
||||
.lock()
|
||||
.await
|
||||
.get(&event.call_id)
|
||||
.cloned()?;
|
||||
let metadata = lookup_mcp_tool_metadata(
|
||||
parent_session.as_ref(),
|
||||
parent_ctx.as_ref(),
|
||||
&invocation.server,
|
||||
&invocation.tool,
|
||||
)
|
||||
.await;
|
||||
let review_cancel = cancel_token.child_token();
|
||||
let review_rx = spawn_guardian_review(
|
||||
Arc::clone(parent_session),
|
||||
Arc::clone(parent_ctx),
|
||||
build_guardian_mcp_tool_review_request(&event.call_id, &invocation, metadata.as_ref()),
|
||||
None,
|
||||
review_cancel.clone(),
|
||||
);
|
||||
let decision = await_approval_with_cancel(
|
||||
async move { review_rx.await.unwrap_or_default() },
|
||||
parent_session,
|
||||
&event.call_id,
|
||||
cancel_token,
|
||||
Some(&review_cancel),
|
||||
)
|
||||
.await;
|
||||
let selected_label = match decision {
|
||||
ReviewDecision::ApprovedForSession => question
|
||||
.options
|
||||
.as_ref()
|
||||
.and_then(|options| {
|
||||
options
|
||||
.iter()
|
||||
.find(|option| option.label == MCP_TOOL_APPROVAL_ACCEPT_FOR_SESSION)
|
||||
})
|
||||
.map(|option| option.label.clone())
|
||||
.unwrap_or_else(|| MCP_TOOL_APPROVAL_ACCEPT.to_string()),
|
||||
ReviewDecision::Approved
|
||||
| ReviewDecision::ApprovedExecpolicyAmendment { .. }
|
||||
| ReviewDecision::NetworkPolicyAmendment { .. } => MCP_TOOL_APPROVAL_ACCEPT.to_string(),
|
||||
ReviewDecision::Denied | ReviewDecision::Abort => {
|
||||
MCP_TOOL_APPROVAL_DECLINE_SYNTHETIC.to_string()
|
||||
}
|
||||
};
|
||||
Some(RequestUserInputResponse {
|
||||
answers: HashMap::from([(
|
||||
question.id.clone(),
|
||||
codex_protocol::request_user_input::RequestUserInputAnswer {
|
||||
answers: vec![selected_label],
|
||||
},
|
||||
)]),
|
||||
})
|
||||
}
|
||||
|
||||
fn spawn_guardian_review(
|
||||
session: Arc<Session>,
|
||||
turn: Arc<TurnContext>,
|
||||
request: GuardianApprovalRequest,
|
||||
retry_reason: Option<String>,
|
||||
cancel_token: CancellationToken,
|
||||
) -> oneshot::Receiver<ReviewDecision> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
std::thread::spawn(move || {
|
||||
let Ok(runtime) = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
else {
|
||||
let _ = tx.send(ReviewDecision::Denied);
|
||||
return;
|
||||
};
|
||||
let decision = runtime.block_on(review_approval_request_with_cancel(
|
||||
&session,
|
||||
&turn,
|
||||
request,
|
||||
retry_reason,
|
||||
cancel_token,
|
||||
));
|
||||
let _ = tx.send(decision);
|
||||
});
|
||||
rx
|
||||
}
|
||||
|
||||
async fn handle_request_permissions(
|
||||
codex: &Codex,
|
||||
parent_session: &Session,
|
||||
parent_ctx: &TurnContext,
|
||||
parent_session: &Arc<Session>,
|
||||
parent_ctx: &Arc<TurnContext>,
|
||||
event: RequestPermissionsEvent,
|
||||
cancel_token: &CancellationToken,
|
||||
) {
|
||||
@@ -534,6 +826,7 @@ async fn await_approval_with_cancel<F>(
|
||||
parent_session: &Session,
|
||||
approval_id: &str,
|
||||
cancel_token: &CancellationToken,
|
||||
review_cancel_token: Option<&CancellationToken>,
|
||||
) -> codex_protocol::protocol::ReviewDecision
|
||||
where
|
||||
F: core::future::Future<Output = codex_protocol::protocol::ReviewDecision>,
|
||||
@@ -541,6 +834,9 @@ where
|
||||
tokio::select! {
|
||||
biased;
|
||||
_ = cancel_token.cancelled() => {
|
||||
if let Some(review_cancel_token) = review_cancel_token {
|
||||
review_cancel_token.cancel();
|
||||
}
|
||||
parent_session
|
||||
.notify_approval(approval_id, codex_protocol::protocol::ReviewDecision::Abort)
|
||||
.await;
|
||||
|
||||
Reference in New Issue
Block a user