mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-30 08:17:53 +00:00
refactor: consolidate tool approval into single param-aware method (#274)
* refactor: consolidate tool approval into single param-aware method Replace the two confusing approval methods (requires_approval() and requires_approval_for()) with a single requires_approval(&self, params) returning a 3-variant ApprovalRequirement enum (Never, UnlessAutoApproved, Always). This enables param-aware approval decisions: HTTP calls without auth headers now skip approval entirely, while authenticated requests always require it. Shell tool merges its destructive-command detection into the same method. Co-Authored-By: Claude Opus 4.6 <[email protected]> * feat: add credential injection to built-in HTTP tool Wire the WASM credential injection system into the built-in HTTP tool so credentials are auto-injected at the boundary (zero-exposure model). - Add SharedCredentialRegistry: thread-safe, append-only registry of credential mappings populated by WASM tools at registration time - Add credential_detect module with broad auth detection for headers (12 exact + 5 substring matches), header values (7 auth scheme prefixes), and URL query params (17 exact + 5 substring matches) - HttpTool now accepts optional credential registry + secrets store, auto-injects matching credentials in execute(), and uses broader auth detection in requires_approval() - ToolRegistry passes credential registry to HttpTool at startup and populates it when WASM tools register - Remove old hardcoded AUTH_HEADER_NAMES / has_auth_headers in favor of the new params_contain_manual_credentials() Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix: address PR #274 review comments (query param injection, lock poisoning, visibility) - Fix injected query params not being sent on outbound HTTP requests by also calling .query() on the RequestBuilder alongside parsed_url mutation - Recover from poisoned RwLock in SharedCredentialRegistry instead of silently ignoring failures, with tracing::warn for visibility - Narrow inject_credential and host_matches_pattern to pub(crate) to avoid committing to them as stable public API Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
542268fde5
commit
2cdd1acb1e
+26
-29
@@ -284,32 +284,8 @@ impl Agent {
|
||||
for (idx, original_tc) in tool_calls.iter().enumerate() {
|
||||
let mut tc = original_tc.clone();
|
||||
|
||||
// Check if tool requires approval (skipped when auto_approve_tools is set)
|
||||
if !self.config.auto_approve_tools
|
||||
&& let Some(tool) = self.tools().get(&tc.name).await
|
||||
&& tool.requires_approval()
|
||||
{
|
||||
let mut is_auto_approved = {
|
||||
let sess = session.lock().await;
|
||||
sess.is_tool_auto_approved(&tc.name)
|
||||
};
|
||||
|
||||
// Override auto-approval for destructive parameters
|
||||
if is_auto_approved && tool.requires_approval_for(&tc.arguments) {
|
||||
tracing::info!(
|
||||
tool = %tc.name,
|
||||
"Parameters require explicit approval despite auto-approve"
|
||||
);
|
||||
is_auto_approved = false;
|
||||
}
|
||||
|
||||
if !is_auto_approved {
|
||||
approval_needed = Some((idx, tc, tool));
|
||||
break; // remaining tools are deferred
|
||||
}
|
||||
}
|
||||
|
||||
// Hook: BeforeToolCall
|
||||
// Hook: BeforeToolCall (runs before approval so hooks can
|
||||
// modify parameters — approval is checked on final params)
|
||||
let event = crate::hooks::HookEvent::ToolCall {
|
||||
tool_name: tc.name.clone(),
|
||||
parameters: tc.arguments.clone(),
|
||||
@@ -352,6 +328,27 @@ impl Agent {
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Check if tool requires approval on the final (post-hook)
|
||||
// parameters. Skipped when auto_approve_tools is set.
|
||||
if !self.config.auto_approve_tools
|
||||
&& let Some(tool) = self.tools().get(&tc.name).await
|
||||
{
|
||||
use crate::tools::ApprovalRequirement;
|
||||
let needs_approval = match tool.requires_approval(&tc.arguments) {
|
||||
ApprovalRequirement::Never => false,
|
||||
ApprovalRequirement::UnlessAutoApproved => {
|
||||
let sess = session.lock().await;
|
||||
!sess.is_tool_auto_approved(&tc.name)
|
||||
}
|
||||
ApprovalRequirement::Always => true,
|
||||
};
|
||||
|
||||
if needs_approval {
|
||||
approval_needed = Some((idx, tc, tool));
|
||||
break; // remaining tools are deferred
|
||||
}
|
||||
}
|
||||
|
||||
let preflight_idx = preflight.len();
|
||||
preflight.push((tc.clone(), PreflightOutcome::Runnable));
|
||||
runnable.push((preflight_idx, tc));
|
||||
@@ -910,9 +907,9 @@ mod tests {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shell_destructive_command_requires_approval_for() {
|
||||
// ShellTool::requires_approval_for should detect destructive commands.
|
||||
// This exercises the same code path used inline in run_agentic_loop.
|
||||
fn test_shell_destructive_command_requires_explicit_approval() {
|
||||
// requires_explicit_approval() detects destructive commands that
|
||||
// should return ApprovalRequirement::Always from ShellTool.
|
||||
use crate::tools::builtin::shell::requires_explicit_approval;
|
||||
|
||||
let destructive_cmds = [
|
||||
|
||||
@@ -357,7 +357,7 @@ impl Scheduler {
|
||||
.into());
|
||||
}
|
||||
|
||||
if tool.requires_approval() {
|
||||
if tool.requires_approval(¶ms).is_required() {
|
||||
return Err(crate::error::ToolError::AuthRequired {
|
||||
name: tool_name.to_string(),
|
||||
}
|
||||
|
||||
+9
-10
@@ -746,19 +746,18 @@ impl Agent {
|
||||
)> = None;
|
||||
|
||||
for (idx, tc) in deferred_tool_calls.iter().enumerate() {
|
||||
if let Some(tool) = self.tools().get(&tc.name).await
|
||||
&& tool.requires_approval()
|
||||
{
|
||||
let is_auto_approved = {
|
||||
let sess = session.lock().await;
|
||||
let mut approved = sess.is_tool_auto_approved(&tc.name);
|
||||
if approved && tool.requires_approval_for(&tc.arguments) {
|
||||
approved = false;
|
||||
if let Some(tool) = self.tools().get(&tc.name).await {
|
||||
use crate::tools::ApprovalRequirement;
|
||||
let needs_approval = match tool.requires_approval(&tc.arguments) {
|
||||
ApprovalRequirement::Never => false,
|
||||
ApprovalRequirement::UnlessAutoApproved => {
|
||||
let sess = session.lock().await;
|
||||
!sess.is_tool_auto_approved(&tc.name)
|
||||
}
|
||||
approved
|
||||
ApprovalRequirement::Always => true,
|
||||
};
|
||||
|
||||
if !is_auto_approved {
|
||||
if needs_approval {
|
||||
approval_needed = Some((idx, tc.clone(), tool));
|
||||
break; // remaining tools stay deferred
|
||||
}
|
||||
|
||||
+1
-1
@@ -432,7 +432,7 @@ Report when the job is complete or if you encounter issues you cannot resolve."#
|
||||
})?;
|
||||
|
||||
// Tools requiring approval are blocked in autonomous jobs
|
||||
if tool.requires_approval() {
|
||||
if tool.requires_approval(params).is_required() {
|
||||
return Err(crate::error::ToolError::AuthRequired {
|
||||
name: tool_name.to_string(),
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user