fix(approval): make "always" auto-approve work for credentialed HTTP requests (#1257)

The HTTP tool returned `ApprovalRequirement::Always` for requests with
credentials, but `Always` is hardcoded to ignore the session auto-approve
set. This meant users who clicked "always" were re-prompted on every
subsequent HTTP call — the UI offered "always" but the backend ignored it.

Two fixes:
1. HTTP credentialed requests now return `UnlessAutoApproved` instead of
   `Always`, so the session auto-approve set is respected.
2. `StatusUpdate::ApprovalNeeded` now carries `allow_always: bool`. All
   channel UIs (Telegram, Slack, Signal, REPL, Web) conditionally hide
   the "always" option when a tool truly requires per-invocation approval
   (`ApprovalRequirement::Always`, e.g. destructive shell commands).

Also boxes `PendingApproval` in `AgenticLoopResult::NeedApproval` to fix
a pre-existing clippy `large_enum_variant` warning.

Regression tests included (test_credentialed_requests_respect_auto_approve,
test_allow_always_matches_approval_requirement) but CI heuristic cannot
detect them in cross-fork PR diffs.

[skip-regression-check]

Co-authored-by: Tyler <[email protected]>
This commit is contained in:
nearfamiliarcow
2026-03-19 11:45:32 -07:00
committed by GitHub
co-authored by Tyler
parent 71f41dd123
commit 09e1c97a27
13 changed files with 199 additions and 51 deletions
+20 -8
View File
@@ -506,7 +506,8 @@ impl Agent {
let tool_name = pending.tool_name.clone();
let description = pending.description.clone();
let parameters = pending.display_parameters.clone();
thread.await_approval(pending);
let allow_always = pending.allow_always;
thread.await_approval(*pending);
let _ = self
.channels
.send_status(
@@ -516,6 +517,7 @@ impl Agent {
tool_name: tool_name.clone(),
description: description.clone(),
parameters: parameters.clone(),
allow_always,
},
&message.metadata,
)
@@ -525,6 +527,7 @@ impl Agent {
tool_name,
description,
parameters,
allow_always,
})
}
Err(e) => {
@@ -1069,28 +1072,31 @@ impl Agent {
usize,
crate::llm::ToolCall,
Arc<dyn crate::tools::Tool>,
bool, // allow_always
)> = None;
for (idx, tc) in deferred_tool_calls.iter().enumerate() {
if let Some(tool) = self.tools().get(&tc.name).await {
// Match dispatcher.rs: when auto_approve_tools is true, skip
// all approval checks (including ApprovalRequirement::Always).
let needs_approval = if self.config.auto_approve_tools {
false
let (needs_approval, allow_always) = if self.config.auto_approve_tools {
(false, true)
} else {
use crate::tools::ApprovalRequirement;
match tool.requires_approval(&tc.arguments) {
let requirement = tool.requires_approval(&tc.arguments);
let needs = match requirement {
ApprovalRequirement::Never => false,
ApprovalRequirement::UnlessAutoApproved => {
let sess = session.lock().await;
!sess.is_tool_auto_approved(&tc.name)
}
ApprovalRequirement::Always => true,
}
};
(needs, !matches!(requirement, ApprovalRequirement::Always))
};
if needs_approval {
approval_needed = Some((idx, tc.clone(), tool));
approval_needed = Some((idx, tc.clone(), tool, allow_always));
break; // remaining tools stay deferred
}
}
@@ -1298,7 +1304,7 @@ impl Agent {
}
// Handle approval if a tool needed it
if let Some((approval_idx, tc, tool)) = approval_needed {
if let Some((approval_idx, tc, tool, allow_always)) = approval_needed {
let new_pending = PendingApproval {
request_id: Uuid::new_v4(),
tool_name: tc.name.clone(),
@@ -1310,6 +1316,7 @@ impl Agent {
deferred_tool_calls: deferred_tool_calls[approval_idx + 1..].to_vec(),
// Carry forward the resolved timezone from the original pending approval
user_timezone: pending.user_timezone.clone(),
allow_always,
};
let request_id = new_pending.request_id;
@@ -1333,6 +1340,7 @@ impl Agent {
tool_name: tool_name.clone(),
description: description.clone(),
parameters: parameters.clone(),
allow_always,
},
&message.metadata,
)
@@ -1343,6 +1351,7 @@ impl Agent {
tool_name,
description,
parameters,
allow_always,
});
}
@@ -1411,7 +1420,8 @@ impl Agent {
let tool_name = new_pending.tool_name.clone();
let description = new_pending.description.clone();
let parameters = new_pending.display_parameters.clone();
thread.await_approval(new_pending);
let allow_always = new_pending.allow_always;
thread.await_approval(*new_pending);
let _ = self
.channels
.send_status(
@@ -1421,6 +1431,7 @@ impl Agent {
tool_name: tool_name.clone(),
description: description.clone(),
parameters: parameters.clone(),
allow_always,
},
&message.metadata,
)
@@ -1430,6 +1441,7 @@ impl Agent {
tool_name,
description,
parameters,
allow_always,
})
}
Err(e) => {