mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-08-31 16:49:34 +00:00
feat: extend lifecycle hooks with declarative bundles (#176)
* feat: add bundled and declarative hook bundle loading * fix: load plugin hooks only for active extensions * fix: avoid duplicate plugin hook registration * security: harden outbound webhook hooks * fix: pin webhook DNS resolutions for outbound hooks * fix: block IPv4-mapped local webhook targets * style: format webhook hardening changes for CI * fix: pass HookRegistry to ExtensionManager in AppBuilder After merging main (which extracted AppBuilder from main.rs in #198), the ExtensionManager::new() call in app.rs was missing the `hooks` parameter that PR #176 added. This moves HookRegistry creation before init_extensions() and threads it through, matching the existing pattern in main.rs. Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Illia Polosukhin <[email protected]> Co-authored-by: Claude Opus 4.6 <[email protected]>
This commit is contained in:
co-authored by
Illia Polosukhin
Claude Opus 4.6
parent
e42b1e5ec1
commit
e87d7bd066
@@ -16,6 +16,7 @@ use crate::extensions::{
|
||||
ActivateResult, AuthResult, ExtensionError, ExtensionKind, ExtensionSource, InstallResult,
|
||||
InstalledExtension, RegistryEntry, ResultSource, SearchResult,
|
||||
};
|
||||
use crate::hooks::HookRegistry;
|
||||
use crate::secrets::{CreateSecretParams, SecretsStore};
|
||||
use crate::tools::ToolRegistry;
|
||||
use crate::tools::mcp::McpClient;
|
||||
@@ -52,6 +53,7 @@ pub struct ExtensionManager {
|
||||
// Shared
|
||||
secrets: Arc<dyn SecretsStore + Send + Sync>,
|
||||
tool_registry: Arc<ToolRegistry>,
|
||||
hooks: Option<Arc<HookRegistry>>,
|
||||
pending_auth: RwLock<HashMap<String, PendingAuth>>,
|
||||
/// Tunnel URL for remote OAuth callbacks (used in future iterations).
|
||||
_tunnel_url: Option<String>,
|
||||
@@ -66,6 +68,7 @@ impl ExtensionManager {
|
||||
mcp_session_manager: Arc<McpSessionManager>,
|
||||
secrets: Arc<dyn SecretsStore + Send + Sync>,
|
||||
tool_registry: Arc<ToolRegistry>,
|
||||
hooks: Option<Arc<HookRegistry>>,
|
||||
wasm_tool_runtime: Option<Arc<WasmToolRuntime>>,
|
||||
wasm_tools_dir: PathBuf,
|
||||
wasm_channels_dir: PathBuf,
|
||||
@@ -83,6 +86,7 @@ impl ExtensionManager {
|
||||
wasm_channels_dir,
|
||||
secrets,
|
||||
tool_registry,
|
||||
hooks,
|
||||
pending_auth: RwLock::new(HashMap::new()),
|
||||
_tunnel_url: tunnel_url,
|
||||
user_id,
|
||||
@@ -320,6 +324,21 @@ impl ExtensionManager {
|
||||
// Unregister from tool registry
|
||||
self.tool_registry.unregister(name).await;
|
||||
|
||||
// Unregister hooks registered from this plugin source.
|
||||
let removed_hooks = self
|
||||
.unregister_hook_prefix(&format!("plugin.tool:{}::", name))
|
||||
.await
|
||||
+ self
|
||||
.unregister_hook_prefix(&format!("plugin.dev_tool:{}::", name))
|
||||
.await;
|
||||
if removed_hooks > 0 {
|
||||
tracing::info!(
|
||||
extension = name,
|
||||
removed_hooks = removed_hooks,
|
||||
"Removed plugin hooks for WASM tool"
|
||||
);
|
||||
}
|
||||
|
||||
// Delete files
|
||||
let wasm_path = self.wasm_tools_dir.join(format!("{}.wasm", name));
|
||||
let cap_path = self
|
||||
@@ -969,6 +988,34 @@ impl ExtensionManager {
|
||||
.await
|
||||
.map_err(|e| ExtensionError::ActivationFailed(e.to_string()))?;
|
||||
|
||||
if let Some(ref hooks) = self.hooks
|
||||
&& let Some(cap_path) = cap_path_option
|
||||
{
|
||||
let source = format!("plugin.tool:{}", name);
|
||||
let registration =
|
||||
crate::hooks::bootstrap::register_plugin_bundle_from_capabilities_file(
|
||||
hooks, &source, cap_path,
|
||||
)
|
||||
.await;
|
||||
|
||||
if registration.total_registered() > 0 {
|
||||
tracing::info!(
|
||||
extension = name,
|
||||
hooks = registration.hooks,
|
||||
outbound_webhooks = registration.outbound_webhooks,
|
||||
"Registered plugin hooks for activated WASM tool"
|
||||
);
|
||||
}
|
||||
|
||||
if registration.errors > 0 {
|
||||
tracing::warn!(
|
||||
extension = name,
|
||||
errors = registration.errors,
|
||||
"Some plugin hooks failed to register"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
tracing::info!("Activated WASM tool '{}'", name);
|
||||
|
||||
Ok(ActivateResult {
|
||||
@@ -1008,6 +1055,21 @@ impl ExtensionManager {
|
||||
let mut pending = self.pending_auth.write().await;
|
||||
pending.retain(|_, auth| auth.created_at.elapsed() < std::time::Duration::from_secs(300));
|
||||
}
|
||||
|
||||
async fn unregister_hook_prefix(&self, prefix: &str) -> usize {
|
||||
let Some(ref hooks) = self.hooks else {
|
||||
return 0;
|
||||
};
|
||||
|
||||
let names = hooks.list().await;
|
||||
let mut removed = 0;
|
||||
for hook_name in names {
|
||||
if hook_name.starts_with(prefix) && hooks.unregister(&hook_name).await {
|
||||
removed += 1;
|
||||
}
|
||||
}
|
||||
removed
|
||||
}
|
||||
}
|
||||
|
||||
/// Infer the extension kind from a URL.
|
||||
|
||||
Reference in New Issue
Block a user