Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions crates/tui/src/core/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1145,12 +1145,17 @@ impl Engine {
.await;

match self.await_tool_approval(&tool_id).await {
Ok(ApprovalResult::Approved) => {
Ok(approval @ (ApprovalResult::Approved | ApprovalResult::ApprovedByMode)) => {
// #3790: a `!`-command can be auto-approved by the active
// mode (e.g. YOLO) rather than by an individual click;
// stamp it honestly so the model is not told the user
// approved a call they were never prompted for.
let by_mode = matches!(approval, ApprovalResult::ApprovedByMode);
emit_tool_audit(json!({
"event": "tool.approval_decision",
"tool_id": tool_id.clone(),
"tool_name": tool_name.clone(),
"decision": "approved",
"decision": if by_mode { "auto_approved_by_mode" } else { "approved" },
"source": "composer_bang",
}));
let mut result = Self::execute_tool_with_lock(
Expand All @@ -1169,7 +1174,11 @@ impl Engine {
if let Ok(tool_result) = result.as_mut() {
stamp_tool_result_approval(
tool_result,
ToolApprovalStamp::ApprovedByUser,
if by_mode {
ToolApprovalStamp::AutoApprovedByMode
} else {
ToolApprovalStamp::ApprovedByUser
},
);
}
result
Expand Down Expand Up @@ -3801,6 +3810,10 @@ impl MockEngineHandle {
pub(crate) async fn recv_approval_event(&mut self) -> Option<MockApprovalEvent> {
match self.rx_approval.recv().await? {
ApprovalDecision::Approved { id } => Some(MockApprovalEvent::Approved { id }),
// #3790: a mode auto-approval surfaces as Approved to the mock
// recorder; tests assert the honest "auto-approved by mode" note on
// the tool result itself rather than on this channel event.
ApprovalDecision::ApprovedByMode { id } => Some(MockApprovalEvent::Approved { id }),
ApprovalDecision::Denied { id } => Some(MockApprovalEvent::Denied { id }),
ApprovalDecision::RetryWithPolicy { id, policy } => {
Some(MockApprovalEvent::RetryWithPolicy { id, policy })
Expand Down
11 changes: 11 additions & 0 deletions crates/tui/src/core/engine/approval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,12 @@ pub(super) enum ApprovalDecision {
Approved {
id: String,
},
/// Auto-approved on the active mode's authority (e.g. YOLO/Bypass), not by
/// an individual user decision. Stamped honestly so the model is never told
/// the user approved something they were never prompted for (#3790).
ApprovedByMode {
id: String,
},
Denied {
id: String,
},
Expand All @@ -46,6 +52,8 @@ pub(super) enum UserInputDecision {
pub(super) enum ApprovalResult {
/// User approved the tool execution.
Approved,
/// The active mode auto-approved the tool execution (no user prompt).
ApprovedByMode,
/// User denied the tool execution.
Denied,
/// User requested retry with an elevated sandbox policy.
Expand Down Expand Up @@ -93,6 +101,9 @@ impl Engine {
ApprovalDecision::Approved { id } if id == tool_id => {
return Ok(ApprovalResult::Approved);
}
ApprovalDecision::ApprovedByMode { id } if id == tool_id => {
return Ok(ApprovalResult::ApprovedByMode);
}
ApprovalDecision::Denied { id } if id == tool_id => {
return Ok(ApprovalResult::Denied);
}
Expand Down
8 changes: 8 additions & 0 deletions crates/tui/src/core/engine/dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,13 +76,18 @@ pub(super) struct ParallelToolResult {
pub(super) enum ToolApprovalStamp {
ApprovedByUser,
ApprovedWithPolicy,
/// Auto-approved on the active mode's authority (e.g. YOLO), with no user
/// prompt. Kept distinct from `ApprovedByUser` so the model is never told a
/// user approved a call they were never shown (#3790).
AutoApprovedByMode,
}

impl ToolApprovalStamp {
fn decision(self) -> &'static str {
match self {
Self::ApprovedByUser => "approved_by_user",
Self::ApprovedWithPolicy => "approved_with_policy",
Self::AutoApprovedByMode => "auto_approved_by_mode",
}
}

Expand All @@ -94,6 +99,9 @@ impl ToolApprovalStamp {
Self::ApprovedWithPolicy => {
"[approval] This tool call required approval and was approved by the user with an adjusted execution policy before execution."
}
Self::AutoApprovedByMode => {
"[approval] This tool call would require approval in Agent mode; the active mode (e.g. YOLO) auto-approved it and ran it without a user prompt."
}
}
}
}
Expand Down
11 changes: 11 additions & 0 deletions crates/tui/src/core/engine/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,17 @@ impl EngineHandle {
Ok(())
}

/// Auto-approve a pending tool call on the active mode's authority (e.g.
/// YOLO), as opposed to an individual user decision. The result is stamped
/// as auto-approved-by-mode so the model is told the truth about why the
/// tool ran without a prompt (#3790).
pub async fn approve_tool_call_by_mode(&self, id: impl Into<String>) -> Result<()> {
self.tx_approval
.send(ApprovalDecision::ApprovedByMode { id: id.into() })
.await?;
Ok(())
}

/// Deny a pending tool call
pub async fn deny_tool_call(&self, id: impl Into<String>) -> Result<()> {
self.tx_approval
Expand Down
Loading
Loading