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 -27
View File
@@ -9,6 +9,25 @@ use thiserror::Error;
use crate::context::JobContext;
/// How much approval a specific tool invocation requires.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ApprovalRequirement {
/// No approval needed.
Never,
/// Needs approval, but session auto-approve can bypass.
UnlessAutoApproved,
/// Always needs explicit approval (even if auto-approved).
Always,
}
impl ApprovalRequirement {
/// Whether this invocation requires approval in contexts where
/// auto-approve is irrelevant (e.g. autonomous worker/scheduler).
pub fn is_required(&self) -> bool {
!matches!(self, Self::Never)
}
}
/// Where a tool should execute: orchestrator process or inside a container.
///
/// Orchestrator tools run in the main agent process (memory access, job mgmt, etc).
@@ -160,31 +179,14 @@ pub trait Tool: Send + Sync {
true
}
/// Whether this tool requires explicit user approval before execution.
/// Whether this tool invocation requires user approval.
///
/// Returns false by default since most tools run in a sandboxed/virtualized
/// environment. Only tools that make external network calls or perform
/// destructive operations should return true.
///
/// When true, the agent will prompt the user for confirmation before
/// executing this tool.
fn requires_approval(&self) -> bool {
false
}
/// Whether this specific invocation should override auto-approval.
///
/// This method is called after checking `requires_approval()` and finding that
/// the tool is auto-approved for this session. Return `true` to force approval
/// for this specific invocation despite auto-approval (for example, for
/// destructive operations like `rm -rf` or `git push --force`).
///
/// Return `false` to allow auto-approval to proceed normally.
///
/// The default returns `false`. Override only if you need parameter-aware
/// approval gating.
fn requires_approval_for(&self, _params: &serde_json::Value) -> bool {
false
/// Returns `Never` by default (most tools run in a sandboxed environment).
/// Override to return `UnlessAutoApproved` for tools that need approval
/// but can be session-auto-approved, or `Always` for invocations that
/// must always prompt (e.g. destructive shell commands, HTTP with auth).
fn requires_approval(&self, _params: &serde_json::Value) -> ApprovalRequirement {
ApprovalRequirement::Never
}
/// Maximum time this tool is allowed to run before the caller kills it.
@@ -347,9 +349,15 @@ mod tests {
}
#[test]
fn test_requires_approval_for_default() {
fn test_requires_approval_default() {
let tool = EchoTool;
// Default requires_approval_for() returns false, allowing auto-approval.
assert!(!tool.requires_approval_for(&serde_json::json!({"message": "hi"})));
// Default requires_approval() returns Never.
assert_eq!(
tool.requires_approval(&serde_json::json!({"message": "hi"})),
ApprovalRequirement::Never
);
assert!(!ApprovalRequirement::Never.is_required());
assert!(ApprovalRequirement::UnlessAutoApproved.is_required());
assert!(ApprovalRequirement::Always.is_required());
}
}