mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-09-01 09:09:19 +00:00
Add generic host-verified /webhook/tools/{tool} ingress (#757)
* Add generic host-verified webhook ingress for tools * Stabilize trace E2E test rig and approval behavior * Fix webhook security issues from review feedback - Reject tools without webhook_capability() (was unauthenticated RCE) - Remove secret-in-query-string fallback (leak via logs/referrers) - Require approval for event_emit tool (escalation via routine triggers) - Simplify header_value() (HeaderMap already case-insensitive) - Redact internal errors from webhook HTTP responses - Remove unused hmac_timestamp_tolerance_secs field - Add regression test for tool without webhook capability [skip-regression-check] Co-Authored-By: Claude Opus 4.6 <[email protected]> * Harden webhook ingress: require auth mechanism, body limit layer, health check - Reject webhook capabilities that declare no auth mechanism (empty WebhookCapability would previously allow unauthenticated access) - Add DefaultBodyLimit layer to reject oversized payloads before buffering - Health check (GET) now verifies tool has webhook_capability(), not just existence - Add regression tests for all three fixes [skip-regression-check] Co-Authored-By: Claude Opus 4.6 <[email protected]> * Fix auto_approve_tools inconsistency between dispatcher and thread_ops dispatcher.rs skips all approval checks (including Always) when auto_approve_tools is true, but thread_ops.rs still required approval for Always tools. This caused deferred tool calls to unexpectedly halt in test rigs and auto-approve configurations. Match dispatcher behavior: short-circuit all approval when auto_approve_tools is enabled. [skip-regression-check] 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
55b5a462a2
commit
369741fc60
@@ -328,6 +328,14 @@ pub trait Tool: Send + Sync {
|
||||
None
|
||||
}
|
||||
|
||||
/// Optional host-side webhook verification configuration for this tool.
|
||||
///
|
||||
/// When present, `/webhook/tools/{tool}` validates shared secret/signatures
|
||||
/// before invoking the tool. Tools should then only handle payload normalization.
|
||||
fn webhook_capability(&self) -> Option<crate::tools::wasm::WebhookCapability> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Get the tool schema for LLM function calling.
|
||||
fn schema(&self) -> ToolSchema {
|
||||
ToolSchema {
|
||||
|
||||
@@ -32,6 +32,8 @@ pub struct Capabilities {
|
||||
pub tool_invoke: Option<ToolInvokeCapability>,
|
||||
/// Check if secrets exist.
|
||||
pub secrets: Option<SecretsCapability>,
|
||||
/// Webhook authentication and signature verification.
|
||||
pub webhook: Option<WebhookCapability>,
|
||||
}
|
||||
|
||||
impl Capabilities {
|
||||
@@ -308,6 +310,25 @@ impl SecretsCapability {
|
||||
/// WASM capabilities use it to configure per-tool HTTP request limits.
|
||||
pub use crate::tools::tool::ToolRateLimitConfig as RateLimitConfig;
|
||||
|
||||
/// Webhook auth/signature capability configuration for tools.
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct WebhookCapability {
|
||||
/// Optional header name for shared-secret validation.
|
||||
pub secret_header: Option<String>,
|
||||
/// Secret name in secrets store for shared-secret validation.
|
||||
pub secret_name: Option<String>,
|
||||
/// Secret name in secrets store containing Ed25519 public key (Discord-style).
|
||||
pub signature_key_secret_name: Option<String>,
|
||||
/// Secret name in secrets store for HMAC-SHA256 signing validation.
|
||||
pub hmac_secret_name: Option<String>,
|
||||
/// Header containing signature (e.g. X-Hub-Signature-256 or X-Slack-Signature).
|
||||
pub hmac_signature_header: Option<String>,
|
||||
/// Optional timestamp header. When present, Slack-style v0 signature is used.
|
||||
pub hmac_timestamp_header: Option<String>,
|
||||
/// Optional signature prefix (default: "sha256=" or "v0=" for timestamped mode).
|
||||
pub hmac_prefix: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::tools::wasm::capabilities::{Capabilities, EndpointPattern, SecretsCapability};
|
||||
@@ -319,6 +340,7 @@ mod tests {
|
||||
assert!(caps.http.is_none());
|
||||
assert!(caps.tool_invoke.is_none());
|
||||
assert!(caps.secrets.is_none());
|
||||
assert!(caps.webhook.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -35,7 +35,7 @@ use serde::{Deserialize, Serialize};
|
||||
use crate::secrets::{CredentialLocation, CredentialMapping};
|
||||
use crate::tools::wasm::{
|
||||
Capabilities, EndpointPattern, HttpCapability, RateLimitConfig, SecretsCapability,
|
||||
ToolInvokeCapability, WorkspaceCapability,
|
||||
ToolInvokeCapability, WebhookCapability, WorkspaceCapability,
|
||||
};
|
||||
|
||||
/// Root schema for a capabilities JSON file.
|
||||
@@ -65,6 +65,10 @@ pub struct CapabilitiesFile {
|
||||
#[serde(default)]
|
||||
pub workspace: Option<WorkspaceCapabilitySchema>,
|
||||
|
||||
/// Tool webhook authentication/signature configuration.
|
||||
#[serde(default)]
|
||||
pub webhook: Option<WebhookCapabilitySchema>,
|
||||
|
||||
/// Authentication setup instructions.
|
||||
/// Used by `ironclaw config` to guide users through auth setup.
|
||||
#[serde(default)]
|
||||
@@ -107,6 +111,7 @@ impl CapabilitiesFile {
|
||||
self.secrets = self.secrets.or(inner.secrets);
|
||||
self.tool_invoke = self.tool_invoke.or(inner.tool_invoke);
|
||||
self.workspace = self.workspace.or(inner.workspace);
|
||||
self.webhook = self.webhook.or(inner.webhook);
|
||||
self.auth = self.auth.or(inner.auth);
|
||||
self.setup = self.setup.or(inner.setup);
|
||||
}
|
||||
@@ -198,6 +203,10 @@ impl CapabilitiesFile {
|
||||
});
|
||||
}
|
||||
|
||||
if let Some(webhook) = &self.webhook {
|
||||
caps.webhook = Some(webhook.to_webhook_capability());
|
||||
}
|
||||
|
||||
caps
|
||||
}
|
||||
}
|
||||
@@ -419,6 +428,46 @@ pub struct WorkspaceCapabilitySchema {
|
||||
pub allowed_prefixes: Vec<String>,
|
||||
}
|
||||
|
||||
/// Webhook capability schema for tools.
|
||||
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
|
||||
pub struct WebhookCapabilitySchema {
|
||||
/// HTTP header name for secret validation.
|
||||
#[serde(default)]
|
||||
pub secret_header: Option<String>,
|
||||
/// Secret name in secrets store for shared-secret validation.
|
||||
#[serde(default)]
|
||||
pub secret_name: Option<String>,
|
||||
/// Secret name in secrets store containing Ed25519 public key.
|
||||
#[serde(default)]
|
||||
pub signature_key_secret_name: Option<String>,
|
||||
/// Secret name in secrets store for HMAC-SHA256 signing.
|
||||
#[serde(default)]
|
||||
pub hmac_secret_name: Option<String>,
|
||||
/// Signature header for HMAC verification.
|
||||
#[serde(default)]
|
||||
pub hmac_signature_header: Option<String>,
|
||||
/// Optional timestamp header for Slack-style v0 verification.
|
||||
#[serde(default)]
|
||||
pub hmac_timestamp_header: Option<String>,
|
||||
/// Optional signature prefix for body-only HMAC mode (default sha256=).
|
||||
#[serde(default)]
|
||||
pub hmac_prefix: Option<String>,
|
||||
}
|
||||
|
||||
impl WebhookCapabilitySchema {
|
||||
fn to_webhook_capability(&self) -> WebhookCapability {
|
||||
WebhookCapability {
|
||||
secret_header: self.secret_header.clone(),
|
||||
secret_name: self.secret_name.clone(),
|
||||
signature_key_secret_name: self.signature_key_secret_name.clone(),
|
||||
hmac_secret_name: self.hmac_secret_name.clone(),
|
||||
hmac_signature_header: self.hmac_signature_header.clone(),
|
||||
hmac_timestamp_header: self.hmac_timestamp_header.clone(),
|
||||
hmac_prefix: self.hmac_prefix.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Authentication setup schema.
|
||||
///
|
||||
/// Tools declare their auth requirements here. The agent uses this to provide
|
||||
@@ -769,6 +818,28 @@ mod tests {
|
||||
assert_eq!(workspace.allowed_prefixes, vec!["context/", "daily/"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_webhook_capability() {
|
||||
let json = r#"{
|
||||
"webhook": {
|
||||
"hmac_secret_name": "github_webhook_secret",
|
||||
"hmac_signature_header": "x-hub-signature-256",
|
||||
"hmac_prefix": "sha256="
|
||||
}
|
||||
}"#;
|
||||
|
||||
let caps = CapabilitiesFile::from_json(json).unwrap();
|
||||
let webhook = caps.webhook.unwrap();
|
||||
assert_eq!(
|
||||
webhook.hmac_secret_name.as_deref(),
|
||||
Some("github_webhook_secret")
|
||||
);
|
||||
assert_eq!(
|
||||
webhook.hmac_signature_header.as_deref(),
|
||||
Some("x-hub-signature-256")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_to_capabilities() {
|
||||
let json = r#"{
|
||||
|
||||
@@ -108,7 +108,7 @@ pub use wrapper::{OAuthRefreshConfig, WasmToolWrapper};
|
||||
// Capabilities (V2)
|
||||
pub use capabilities::{
|
||||
Capabilities, EndpointPattern, HttpCapability, RateLimitConfig, SecretsCapability,
|
||||
ToolInvokeCapability, WorkspaceCapability, WorkspaceReader,
|
||||
ToolInvokeCapability, WebhookCapability, WorkspaceCapability, WorkspaceReader,
|
||||
};
|
||||
|
||||
// Security components (V2)
|
||||
|
||||
@@ -808,6 +808,10 @@ impl Tool for WasmToolWrapper {
|
||||
// Use the timeout as a conservative estimate
|
||||
Some(self.prepared.limits.timeout)
|
||||
}
|
||||
|
||||
fn webhook_capability(&self) -> Option<crate::tools::wasm::WebhookCapability> {
|
||||
self.capabilities.webhook.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for WasmToolWrapper {
|
||||
|
||||
Reference in New Issue
Block a user