Skip to content

feat: extend lifecycle hooks with declarative bundles - #176

Merged
ilblackdragon merged 10 commits into
mainfrom
hooks-extended
Feb 19, 2026
Merged

ilblackdragon merged 10 commits into
mainfrom
hooks-extended

Conversation

@serrrfirat

Copy link
Copy Markdown
Collaborator

Summary

  • add declarative lifecycle hook bundles with built-in audit hooks, rule hooks (reject/regex replace/prepend/append), and outbound webhook hooks
  • bootstrap hooks from active WASM plugin capabilities plus workspace files (hooks/hooks.json, hooks/*.hook.json) and wire runtime activate/remove flows to register/unregister plugin hooks
  • harden outbound webhooks with HTTPS-only URLs, restricted target/header validation, redirect blocking, bounded in-flight delivery, and redacted event payloads
  • add hook-registry duplicate-name replacement to prevent accidental double execution
  • update FEATURE_PARITY.md statuses for bundled/plugin/workspace/outbound hooks (with transcribeAudio intentionally still pending)

Testing

  • cargo test hooks::

Notes

@gemini-code-assist

Copy link
Copy Markdown
Contributor

Summary of Changes

Hello @serrrfirat, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances the extensibility and control within the system by introducing a comprehensive, declarative lifecycle hook mechanism. It allows users and plugins to define custom behaviors at various points in the application's lifecycle through configuration, rather than requiring code changes. This change not only broadens the system's capabilities for customization and integration but also incorporates strong security measures for external communications, ensuring that new functionalities are both powerful and safe.

Highlights

  • Declarative Hook Bundles Introduced: Implemented a new system for declarative lifecycle hook bundles, including built-in audit hooks, flexible rule hooks (for rejection, regex replacement, prepending, and appending content), and robust outbound webhook hooks.
  • Dynamic Hook Registration: Enabled bootstrapping of hooks from active WASM plugin capabilities and workspace files (e.g., hooks/hooks.json, hooks/*.hook.json), with runtime flows to register and unregister plugin hooks upon extension activation or removal.
  • Enhanced Outbound Webhook Security and Reliability: Hardened outbound webhooks by enforcing HTTPS-only URLs, restricting target and header validation, blocking redirects, bounding in-flight delivery to prevent overload, and redacting event payloads for security.
  • Duplicate Hook Name Handling: Added logic to the hook registry to replace existing hooks when a new hook with the same name is registered, preventing accidental double execution and ensuring predictable behavior.
  • Feature Parity Documentation Updated: Updated the FEATURE_PARITY.md document to reflect the new statuses for bundled, plugin, workspace, and outbound hooks, indicating their successful implementation.
Changelog
  • FEATURE_PARITY.md
    • Updated the status of 'Hook plugins' to '✅' with a note about declarative hooks from extension capabilities.
    • Marked 'Bundled hooks', 'Plugin hooks', 'Workspace hooks', and 'Outbound webhooks' as implemented ('✅') with descriptive notes for each.
    • Revised the 'Hooks system' entry in the P1 section to reflect the inclusion of bundled, plugin, workspace hooks, and outbound webhooks.
  • src/extensions/manager.rs
    • Imported HookRegistry for use within the extension manager.
    • Added an optional Arc<HookRegistry> field to the ExtensionManager struct.
    • Modified the ExtensionManager::new constructor to accept and store the HookRegistry.
    • Implemented logic to unregister plugin hooks by prefix when a WASM tool is removed.
    • Added logic to register plugin hooks from a WASM tool's capabilities file upon activation, logging registration summaries and errors.
    • Introduced a new asynchronous helper function unregister_hook_prefix to remove hooks based on a naming prefix.
  • src/hooks/bootstrap.rs
    • Added a new module for bootstrapping hooks.
    • Defined HookBootstrapSummary to track the number of registered bundled, plugin, workspace, and outbound webhooks, along with errors.
    • Implemented bootstrap_hooks function to orchestrate the registration of all hook types at startup.
    • Created register_plugin_bundles and register_plugin_bundle_from_capabilities_file to load hooks from WASM plugin capabilities.
    • Developed collect_plugin_capability_files to discover capability files from WASM tools and channels, ensuring uniqueness and filtering by active extensions.
    • Added load_plugin_bundle_from_capabilities_file to parse hook configurations from capability JSON files.
    • Implemented register_workspace_bundles to load hooks from hooks/hooks.json and hooks/*.hook.json files within the workspace.
    • Provided utility functions extract_hooks_section, parse_workspace_bundle, and is_workspace_hook_file for parsing and filtering workspace hook files.
    • Included unit tests for JSON parsing and workspace file filtering.
  • src/hooks/bundled.rs
    • Added a new module for bundled hook implementations and declarative registration.
    • Defined HookBundleError enum for various validation and parsing errors.
    • Introduced HookBundleConfig for declarative definition of rule and outbound webhook hooks, supporting both object and array JSON formats.
    • Created HookRegistrationSummary to report the outcome of hook bundle registrations.
    • Implemented register_bundled_hooks to register the built-in AuditLogHook.
    • Developed register_bundle to process HookBundleConfig and register individual RuleHook and OutboundWebhookHook instances.
    • Defined HookRuleConfig for declarative content/tool/session rules, including regex guards, replacements, prepends, and appends.
    • Defined OutboundWebhookConfig for declarative fire-and-forget outbound webhooks, including URL, headers, timeout, and concurrency limits.
    • Implemented AuditLogHook for logging lifecycle events.
    • Created RuleHook and OutboundWebhookHook structs, which are runtime implementations compiled from their respective configurations, including validation logic for URLs, IP addresses, and headers.
    • Added helper functions summarize_webhook_event, validate_webhook_url, validate_webhook_target_runtime, validate_webhook_headers, is_forbidden_webhook_host, is_forbidden_ip, is_forbidden_header, timeout_from_ms, event_user_id, and extract_primary_content.
    • Included extensive unit tests for rule hooks, outbound webhooks, and validation logic.
  • src/hooks/hook.rs
    • Derived Serialize and Deserialize traits for HookPoint, HookEvent, and HookFailureMode enums to enable JSON serialization/deserialization.
    • Added #[serde(rename_all = "camelCase")] to HookPoint and #[serde(rename_all = "snake_case")] to HookFailureMode for consistent JSON casing.
    • Implemented an as_str method for HookPoint to provide human-readable string representations.
  • src/hooks/mod.rs
    • Declared new public modules bootstrap and bundled.
    • Exported HookBootstrapSummary, bootstrap_hooks, HookBundleConfig, HookRegistrationSummary, register_bundle, and register_bundled_hooks from the new modules.
  • src/hooks/registry.rs
    • Modified register_with_priority to check for existing hooks with the same name and replace them, logging a warning if a replacement occurs.
    • Added a unit test test_register_duplicate_name_replaces_existing to verify the new duplicate name replacement behavior.
  • src/main.rs
    • Imported bootstrap_hooks from the hooks module.
    • Initialized the HookRegistry earlier in the main function to allow runtime extension activation to register hooks.
    • Passed the Arc<HookRegistry> to the ExtensionManager::new constructor.
    • Collected dev_loaded_tool_names during WASM tool loading.
    • Collected loaded_wasm_channel_names during WASM channel loading.
    • Called bootstrap_hooks with the HookRegistry, workspace, WASM directories, and active extension names to register all initial hooks.
    • Logged a summary of the initialized lifecycle hooks.
  • src/tools/builtin/extension_tools.rs
    • Updated the ExtensionManager::new call in tests to include the new hooks parameter, passing None as it's not relevant for these specific tests.
Activity
  • The pull request introduces a comprehensive set of new features related to lifecycle hooks, including declarative bundles, plugin integration, and enhanced security for webhooks.
  • The author has provided a detailed summary of the changes, testing instructions, and notes regarding the scope of the PR.
  • No external review comments or activity are available in the provided context.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution. ↩

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a declarative lifecycle hook system, a significant feature enhancement. It adds support for bundled, plugin-based, and workspace-based hooks, including rule-based transformations and outbound webhooks. The implementation includes robust security measures for webhooks, such as HTTPS-only enforcement, SSRF protection via hostname and IP validation, and payload redaction. The changes are well-structured across new modules for bootstrapping and bundled hooks, and the core ExtensionManager and HookRegistry are updated to support the new dynamic hook lifecycle. My review includes a couple of suggestions to improve the efficiency of webhook delivery and hook unregistration. Overall, this is a high-quality contribution with strong security considerations.

Comment thread src/extensions/manager.rs
Comment on lines +1059 to +1072
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
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation of unregister_hook_prefix iterates over all hook names and calls hooks.unregister for each match. Since unregister acquires a write lock on the hooks list, this results in repeated locking and unlocking within the loop, which is inefficient, especially for plugins with many hooks.

To improve performance, consider enhancing HookRegistry with a method that can unregister multiple hooks by prefix in a single operation, for example unregister_by_prefix. This would allow acquiring the write lock only once to remove all matching hooks. ExtensionManager could then call this more efficient method.

Comment thread src/hooks/bundled.rs Outdated
Comment on lines +673 to +701
async fn validate_webhook_target_runtime(url: &str) -> Result<(), String> {
let parsed = reqwest::Url::parse(url).map_err(|e| format!("Invalid URL: {e}"))?;
let host = parsed
.host_str()
.ok_or_else(|| "Webhook URL has no host".to_string())?;

if let Ok(ip) = host.parse::<IpAddr>() {
if is_forbidden_ip(ip) {
return Err(format!("Webhook target resolves to blocked IP {ip}"));
}
return Ok(());
}

let port = parsed
.port_or_known_default()
.ok_or_else(|| "Webhook URL has no valid port".to_string())?;

let addrs = tokio::net::lookup_host((host, port))
.await
.map_err(|e| format!("DNS resolution failed: {e}"))?;

for addr in addrs {
if is_forbidden_ip(addr.ip()) {
return Err(format!("Webhook target resolves to blocked IP {}", addr.ip()));
}
}

Ok(())
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The validate_webhook_target_runtime function performs a DNS lookup for every outbound webhook delivery. For high-frequency webhooks targeting the same host, this can lead to redundant network calls and add latency.

To optimize this, consider introducing a cache for the validation results. A time-based cache (e.g., using the moka crate) could store the validation status of a host for a short period (e.g., 1-5 minutes), avoiding repeated DNS lookups for the same target within that window. This would improve the efficiency of the webhook delivery system.

@serrrfirat serrrfirat linked an issue Feb 18, 2026 that may be closed by this pull request
11 tasks
ilblackdragon
ilblackdragon previously approved these changes Feb 19, 2026
ilblackdragon and others added 2 commits February 19, 2026 14:33
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 <noreply@anthropic.com>
@ilblackdragon
ilblackdragon merged commit e87d7bd into main Feb 19, 2026
2 checks passed
@ilblackdragon
ilblackdragon deleted the hooks-extended branch February 19, 2026 23:00
This was referenced Feb 19, 2026
jaswinder6991 pushed a commit to jaswinder6991/ironclaw that referenced this pull request Feb 26, 2026
* 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 nearai#198),
the ExtensionManager::new() call in app.rs was missing the `hooks`
parameter that PR nearai#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 <noreply@anthropic.com>

---------

Co-authored-by: Illia Polosukhin <ilblackdragon@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
bkutasi pushed a commit to bkutasi/ironclaw that referenced this pull request Mar 28, 2026
* 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 nearai#198),
the ExtensionManager::new() call in app.rs was missing the `hooks`
parameter that PR nearai#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 <noreply@anthropic.com>

---------

Co-authored-by: Illia Polosukhin <ilblackdragon@gmail.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: Lifecycle hooks system (beforeInbound, beforeToolCall, onSession, etc.)

2 participants