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:
Illia Polosukhin
2026-02-21 01:28:23 +00:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 542268fde5
commit 2cdd1acb1e
21 changed files with 1113 additions and 148 deletions
+35 -22
View File
@@ -55,7 +55,9 @@ use tokio::process::Command;
use crate::context::JobContext;
use crate::sandbox::{SandboxManager, SandboxPolicy};
use crate::tools::tool::{Tool, ToolDomain, ToolError, ToolOutput, require_str};
use crate::tools::tool::{
ApprovalRequirement, Tool, ToolDomain, ToolError, ToolOutput, require_str,
};
/// Maximum output size before truncation (64KB).
const MAX_OUTPUT_SIZE: usize = 64 * 1024;
@@ -696,11 +698,7 @@ impl Tool for ShellTool {
Ok(ToolOutput::success(result, duration))
}
fn requires_approval(&self) -> bool {
true // Shell commands should require approval
}
fn requires_approval_for(&self, params: &serde_json::Value) -> bool {
fn requires_approval(&self, params: &serde_json::Value) -> ApprovalRequirement {
let cmd = params
.get("command")
.and_then(|c| c.as_str().map(String::from))
@@ -714,10 +712,10 @@ impl Tool for ShellTool {
if let Some(ref cmd) = cmd
&& requires_explicit_approval(cmd)
{
return true;
return ApprovalRequirement::Always;
}
false
ApprovalRequirement::UnlessAutoApproved
}
fn requires_sanitization(&self) -> bool {
@@ -861,31 +859,46 @@ mod tests {
}
#[test]
fn test_requires_approval_for_destructive_command() {
fn test_requires_approval_destructive_command() {
use crate::tools::tool::ApprovalRequirement;
let tool = ShellTool::new();
// Destructive commands must return true even though shell already
// requires base approval -- the distinction matters for auto-approve override.
assert!(tool.requires_approval_for(&serde_json::json!({"command": "rm -rf /tmp"})));
assert!(tool.requires_approval_for(
&serde_json::json!({"command": "git push --force origin main"})
));
assert!(tool.requires_approval_for(&serde_json::json!({"command": "DROP TABLE users;"})));
// Destructive commands must return Always to bypass auto-approve.
assert_eq!(
tool.requires_approval(&serde_json::json!({"command": "rm -rf /tmp"})),
ApprovalRequirement::Always
);
assert_eq!(
tool.requires_approval(&serde_json::json!({"command": "git push --force origin main"})),
ApprovalRequirement::Always
);
assert_eq!(
tool.requires_approval(&serde_json::json!({"command": "DROP TABLE users;"})),
ApprovalRequirement::Always
);
}
#[test]
fn test_requires_approval_for_safe_command() {
fn test_requires_approval_safe_command() {
use crate::tools::tool::ApprovalRequirement;
let tool = ShellTool::new();
// Safe commands should not override auto-approval; only destructive ones do.
assert!(!tool.requires_approval_for(&serde_json::json!({"command": "cargo build"})));
assert!(!tool.requires_approval_for(&serde_json::json!({"command": "echo hello"})));
// Safe commands return UnlessAutoApproved (can be auto-approved).
assert_eq!(
tool.requires_approval(&serde_json::json!({"command": "cargo build"})),
ApprovalRequirement::UnlessAutoApproved
);
assert_eq!(
tool.requires_approval(&serde_json::json!({"command": "echo hello"})),
ApprovalRequirement::UnlessAutoApproved
);
}
#[test]
fn test_requires_approval_for_string_encoded_args() {
fn test_requires_approval_string_encoded_args() {
use crate::tools::tool::ApprovalRequirement;
let tool = ShellTool::new();
// When arguments are string-encoded JSON (rare LLM behavior).
let args = serde_json::Value::String(r#"{"command": "rm -rf /tmp/stuff"}"#.to_string());
assert!(tool.requires_approval_for(&args));
assert_eq!(tool.requires_approval(&args), ApprovalRequirement::Always);
}
#[test]