============================================================================== FEATHERFLY PLUGIN REFERENCE — API v8 (native .so plugins) ============================================================================== Auto-generated by `make docs`. HTML version: docs/plugins/index.html Plugins hook into FeatherFly via lifecycle events, config/request/JSON mixins, and routes. ------------------------------------------------------------------------------ OVERVIEW ------------------------------------------------------------------------------ FeatherFly plugins are native shared libraries (`.so` files) loaded at daemon startup. They extend the daemon without recompiling FeatherFly itself. A plugin exports one symbol: `featherfly_plugin_entry`. The daemon loads each `.so`, checks the plugin API version, calls `init`, and keeps the library in memory until shutdown. Plugins register hooks during `init`. Hook systems: 1. **Lifecycle events** — callbacks fired at fixed points (config loaded, daemon started, etc.). 2. **Config mutation** — rewrite raw YAML before settings are applied. 3. **Request hooks** — run before HTTP handlers (`request.intercept`, `middleware.inject`). 4. **Plugin routes** — register new HTTP endpoints on the daemon router. 5. **JSON mutation hooks** — callbacks that receive JSON, may rewrite it, and return modified output for HTTP responses. 6. **CloudPanel command hooks** — rewrite or cancel CloudPanel CLI operations before `clpctl` runs. All hooks for a given target run in **plugin load order** (alphabetical by filename in the plugins directory). ------------------------------------------------------------------------------ TERMINOLOGY ------------------------------------------------------------------------------ FeatherFly plugin vocabulary — read this before writing hooks. | Term | Meaning | |------|---------| | **Plugin** | A native `.so` shared library loaded by the daemon at startup. | | **Host** | The running FeatherFly daemon. Passes `HostApi` to your `init`. | | **Hook** | A callback your plugin registers during `init`. | | **Event** | A lifecycle hook fired at a fixed point (startup, shutdown, config load). | | **Config mutation hook** | Rewrites raw config YAML bytes before settings are applied (`config.mutate`). | | **Request hook** | Runs before HTTP handlers — `request.intercept` (outer) or `middleware.inject` (inner). | | **Plugin route** | A handler registered with `route!` that serves a new HTTP endpoint. | | **JSON mutation hook** | A hook that receives JSON bytes, may rewrite them, and returns output for HTTP responses. | | **CloudPanel command hook** | A hook that receives CloudPanel operation metadata and CLI args JSON, then continues, rewrites args, or cancels. | | **Target** | What a hook listens to — an event name, config pipeline, route prefix, or JSON target. | | **Pipeline** | Ordered chain of hooks for one target. Each hook sees the previous hook's output. | | **Mixin** | FeatherFly's model for JSON hooks: multiple plugins stack transformations on the same response, like Minecraft mixins layering behavior. | | **Load order** | Alphabetical by `.so` filename in the plugins directory. Determines pipeline order. | | **Cancel** | Lifecycle hooks may call `HookResult::cancel()` to skip remaining handlers for that event. | | **API version** | Integer in `PluginEntry`. Must match `FEATHERFLY_PLUGIN_API_VERSION` or the plugin is skipped. | | **Action** | A panel step object `{ id, label, step }` returned in API responses for follow-up work. | ------------------------------------------------------------------------------ ARCHITECTURE ------------------------------------------------------------------------------ ## Mixin-style hook pipeline FeatherFly plugins extend the daemon **without recompiling it**, similar to how Minecraft mixins inject behavior into existing code paths. ``` Daemon startup │ ├─ load .so files (alphabetical) │ └─ init(host) ── register all hooks │ ├─ config.mutate pipeline ──► [plugin A] ──► [plugin B] ──► final YAML │ ├─ config.loaded (post-mutation YAML bytes) │ └─ HTTP listening │ ├─ request.intercept ──► middleware.inject ──► handler │ ├─ plugin routes (route.register) │ ├─ CloudPanel API ──► cloudpanel.command ──► clpctl │ └─ JSON response for /api/* ├─ json.response pipeline └─ json.actions pipeline ``` ### How a JSON mixin chain works 1. FeatherFly builds the base JSON response for a route. 2. Each plugin registered for `json.response` on that route prefix runs **in load order**. 3. Plugin 1 receives the original JSON, may mutate it, writes output. 4. Plugin 2 receives plugin 1's output, may mutate again. 5. After all body hooks, `json.actions` hooks run on the `actions` array only. 6. The final JSON is sent to the client. This is intentionally **composable**: small plugins each do one job (add a field, inject a step, strip sensitive data) instead of one monolithic plugin patching everything. ### Lifecycle vs mutation | Hook type | When it runs | Can cancel others? | Typical use | |-----------|--------------|--------------------|-------------| | Lifecycle event | Fixed daemon milestones | Yes (same event only) | Startup banners, config validation | | JSON mutation | Every matching HTTP JSON response | No — always runs in pipeline | Response fields, panel actions | | CloudPanel command | Before clpctl process spawn | Yes — cancels command execution | Policy checks, defaults | ### Versioning Plugin API **v8** (current) exposes lifecycle events, config mutation, request hooks, plugin routes, JSON mutation, and CloudPanel command hooks. Future hooks will bump the API version so old plugins keep loading safely when new hook types appear. ------------------------------------------------------------------------------ LIFECYCLE PIPELINE ------------------------------------------------------------------------------ 1. Daemon reads config.yml (raw bytes) 2. Each `.so` in the plugins directory is loaded 3. For each plugin: `init(host)` runs — register hooks here (including config.mutate) 4. After each plugin init: `plugin.loaded` event fires (payload = plugin name) 5. Registered config.mutate hooks rewrite the YAML pipeline 6. Final config is parsed and applied (directories, logging, pid file) 7. `config.loaded` fires with the post-mutation YAML bytes 8. `daemon.starting` fires before HTTP routes are wired 9. HTTP server starts (core routes + plugin routes; request middleware stack active) 10. `daemon.started` fires (payload = listen address, e.g. `127.0.0.1:9090`) 11. On shutdown: `daemon.stopping` fires, then each plugin's `shutdown` runs ------------------------------------------------------------------------------ HTTP REQUEST PIPELINE ------------------------------------------------------------------------------ ## HTTP request stack ``` Client request │ ▼ response_middleware (pass-through on request; mutates JSON on response) │ ▼ request.intercept hooks (outer — auth, rate limits, early reject) │ ▼ middleware.inject hooks (inner — tracing, header checks) │ ▼ handle_request (debug logging) │ ▼ Axum router (core routes + plugin routes) ``` Request hooks receive method, path, headers as JSON, and body bytes. Headers use lowercase keys. `authorization` is redacted. Route patterns use prefix matching: `/api/*` matches `/api/system`, `/plugins/hello` matches exactly that path prefix chain. Plugin routes register during `init` and merge into the same router as core API routes. ------------------------------------------------------------------------------ HOST API ------------------------------------------------------------------------------ During init, FeatherFly passes a HostApi struct: - api_version — must match FEATHERFLY_PLUGIN_API_VERSION - register_hook — register a lifecycle event callback - register_config_hook — register a config YAML mutation callback - register_request_hook — register request.intercept or middleware.inject - register_route — register a plugin HTTP route (GET/POST/PUT/PATCH/DELETE) - register_json_hook — register a JSON mutation callback - register_cloudpanel_hook — inspect, mutate, or cancel CloudPanel CLI commands - log_info — write an info line to the daemon log Register hooks only inside init. Callback pointers are kept until shutdown. init return codes: 0 = success. Any other value fails plugin load. Return codes: - CONFIG_MUTATE_MODIFIED / JSON_MUTATE_MODIFIED (0) — output written via write_* helpers - CONFIG_MUTATE_UNCHANGED / JSON_MUTATE_UNCHANGED (1) — keep input unchanged - REQUEST_CONTINUE (0) — pass request to next hook/handler - REQUEST_RESPOND (1) — short-circuit with write_request_response - ROUTE_OK (0) — route handled via write_route_response - CLOUDPANEL_CONTINUE / CLOUDPANEL_MODIFIED / CLOUDPANEL_CANCEL — continue, replace args, or block a CloudPanel command - Negative — error; input is kept ------------------------------------------------------------------------------ BUILD AND INSTALL ------------------------------------------------------------------------------ **Requirements:** same OS and CPU architecture as the FeatherFly binary (e.g. linux x86_64 musl build needs a musl-linked plugin). ```toml [lib] crate-type = ["cdylib"] [dependencies] featherfly-plugin-sdk = { path = "../featherfly-plugin-sdk" } serde_json = "1" ``` ```bash featherfly plugin build ./my-plugin featherfly plugin install ./my-plugin # copies .so to plugins dir featherfly plugin ship ./my-plugin # build + install make plugin-ship PLUGIN=plugins/hello ``` **Paths** - Production config: `/etc/featherfly/config.yml` - Debug (`--debug` or `debug: true`): `./config.yml` in the working directory - Plugins: `system.plugins_directory` (default `/var/lib/featherfly/plugins/*.so`) - Config: `system.plugins_directory`, `plugins.enabled` **Version mismatch:** if `descriptor.api_version` ≠ daemon API version, the plugin is skipped with a warning. ------------------------------------------------------------------------------ RETURN CODES ------------------------------------------------------------------------------ ## init | Code | Meaning | |------|---------| | `0` | Plugin loaded successfully | | non-zero | Plugin load fails; `.so` is skipped | ## Lifecycle hooks | Return | Meaning | |--------|---------| | `HookResult::continue()` | Run remaining handlers for this event | | `HookResult::cancel()` | Stop remaining handlers for this event | ## config.mutate | Code | Meaning | |------|---------| | `CONFIG_MUTATE_MODIFIED` (0) | Output written via `write_yaml_output` | | `CONFIG_MUTATE_UNCHANGED` (1) | Keep prior YAML bytes | | negative | Error; prior YAML kept | ## request.intercept / middleware.inject | Code | Meaning | |------|---------| | `REQUEST_CONTINUE` (0) | Pass to next hook or HTTP handler | | `REQUEST_RESPOND` (1) | Short-circuit; body from `write_request_response` | | negative | Error; request continues | ## route.register | Code | Meaning | |------|---------| | `ROUTE_OK` (0) | Response written via `write_route_response` | | negative | Handler error; 500 returned | ## JSON hooks | Code | Meaning | |------|---------| | `JSON_MUTATE_MODIFIED` (0) | Output written via `write_json_output` | | `JSON_MUTATE_UNCHANGED` (1) | Keep prior JSON | | negative | Error; prior JSON kept | ## CloudPanel command hooks | Code | Meaning | |------|---------| | `CLOUDPANEL_CONTINUE` (0) | Keep current CLI args and continue | | `CLOUDPANEL_MODIFIED` (1) | Output written via `write_cloudpanel_args` | | `CLOUDPANEL_CANCEL` (2) | Cancel the command; optional reason from `write_cloudpanel_cancel` | | negative | Error; current args kept | ------------------------------------------------------------------------------ SOURCE TREE ------------------------------------------------------------------------------ Key paths in the FeatherFly repository for plugin developers: | Path | Purpose | |------|---------| | `featherfly-plugin-sdk/src/lib.rs` | Plugin API types, macros, return codes | | `featherfly-plugin-sdk/src/metadata.rs` | Documentation strings consumed by docgen | | `application/src/daemon.rs` | Startup, config pipeline, HTTP server wiring | | `application/src/plugins/mod.rs` | Plugin loader, HostApi trampolines | | `application/src/plugins/events.rs` | Hook registries, config/request/route dispatch | | `application/src/plugins/request_middleware.rs` | request.intercept + middleware.inject Axum layers | | `application/src/plugins/routes.rs` | Plugin route registration and dispatch | | `application/src/plugins/middleware.rs` | JSON response mutation middleware | | `application/src/config.rs` | Config parse, preview, apply after mutation | | `plugins/hello/src/lib.rs` | Reference plugin with all v4 hook types | ------------------------------------------------------------------------------ MACROS ------------------------------------------------------------------------------ declare_plugin! Exports featherfly_plugin_entry with name, version, init, and optional shutdown. hook! Registers a lifecycle event handler during init. hook_config! Registers a config.mutate handler that rewrites YAML before apply. hook_request! Registers request.intercept or middleware.inject for a route prefix. route! Registers a plugin HTTP route (GET/POST/PUT/PATCH/DELETE). hook_json! Registers a JSON mutation handler for a route prefix during init. hook_cloudpanel! Registers a CloudPanel command hook during init. ------------------------------------------------------------------------------ HTTP METHODS (route.register) ------------------------------------------------------------------------------ GET = 1 POST = 2 PUT = 3 PATCH = 4 DELETE = 5 ------------------------------------------------------------------------------ LIFECYCLE EVENTS — 47 hook points (hook!(host, PluginEvent::..., handler)) ------------------------------------------------------------------------------ [daemon.starting] id=1 Summary: Daemon is about to start HTTP. When: After config mutation and final config apply, before the router is served. Payload: Empty. Cancelable: no Details: Use for startup initialization that needs the final daemon config. Use cases: - Warm plugin caches - Open external connections Register: hook!(host, PluginEvent::DaemonStarting, on_daemon_starting); [daemon.started] id=2 Summary: HTTP server is listening. When: After the TCP listener is bound and before serving requests. Payload: UTF-8 bytes of the listen address, e.g. 127.0.0.1:9090. Cancelable: no Details: This is the safest event for reporting daemon availability. Use cases: - Emit startup metrics - Notify external monitoring Register: hook!(host, PluginEvent::DaemonStarted, on_daemon_started); [daemon.stopping] id=3 Summary: Daemon is shutting down. When: After the HTTP server stops, before plugin shutdown callbacks run. Payload: Empty. Cancelable: no Details: Release resources here if you need ordering before plugin shutdown. Use cases: - Flush queues - Close external connections Register: hook!(host, PluginEvent::DaemonStopping, on_daemon_stopping); [config.loaded] id=4 Summary: Config file has been parsed. When: After plugin config.mutate hooks finish and final YAML is parsed. Payload: Post-mutation raw config.yml bytes. Cancelable: no Details: Config hooks run before this event; listeners should treat this payload as read-only. Use cases: - Validate effective config - Export config metadata Register: hook!(host, PluginEvent::ConfigLoaded, on_config_loaded); [plugin.loaded] id=5 Summary: A plugin finished initializing. When: Immediately after that plugin's init returns 0. Payload: UTF-8 bytes of the plugin name from its descriptor. Cancelable: no Details: Only plugins loaded earlier can observe later plugin.loaded events. Use cases: - Cross-plugin diagnostics - Log plugin inventory Register: hook!(host, PluginEvent::PluginLoaded, on_plugin_loaded); [request.received] id=6 Summary: HTTP request entered the daemon middleware stack. When: Before the request is passed to route handlers. Payload: JSON: { request_id, client_ip, method, path, query }. Cancelable: no Details: Use request.intercept hooks to short-circuit; this lifecycle event is observational. Use cases: - Access logs - Traffic analytics Register: hook!(host, PluginEvent::RequestReceived, on_request_received); [request.completed] id=7 Summary: HTTP request finished. When: After a response status is available. Payload: JSON: { request_id, client_ip, method, path, query, status, duration_ms }. Cancelable: no Details: Fires for core routes and plugin routes. Use cases: - Latency metrics - Audit logs Register: hook!(host, PluginEvent::RequestCompleted, on_request_completed); [request.not_found] id=8 Summary: Request returned 404. When: After the fallback handler responds with not found. Payload: JSON: { request_id, client_ip, method, path, query }. Cancelable: no Details: Useful with probe protection to understand noisy paths. Use cases: - Security analytics - Missing route debugging Register: hook!(host, PluginEvent::RequestNotFound, on_request_not_found); [request.blocked] id=9 Summary: Probe protection blocked a request. When: Before a blocked client request is rejected. Payload: JSON: { request_id, client_ip, method, path, query }. Cancelable: no Details: Fires when an IP is already blocked by probe protection. Use cases: - Security alerts - Blocklist observability Register: hook!(host, PluginEvent::RequestBlocked, on_request_blocked); [probe.client_blocked] id=10 Summary: Probe protection blocked a client IP. When: After the unknown route threshold is reached. Payload: JSON: { request_id, client_ip, unknown_hits, block_secs }. Cancelable: no Details: The request that crosses the threshold still receives its normal response. Use cases: - Security alerts - Abuse tracking Register: hook!(host, PluginEvent::ProbeClientBlocked, on_probe_client_blocked); [auth.failed] id=11 Summary: Bearer authentication failed. When: After an authenticated route rejects a request. Payload: JSON: { request_id, client_ip, method, path, query }. Cancelable: no Details: Authorization header values are never included in the payload. Use cases: - Security alerts - Panel integration debugging Register: hook!(host, PluginEvent::AuthFailed, on_auth_failed); [restart.scheduled] id=12 Summary: Daemon restart was queued. When: After plugin reload requests schedule a restart. Payload: JSON: { delay_ms, reason? }. Cancelable: no Details: Plugins are loaded at startup, so reload uses daemon restart semantics. Use cases: - Notify operators - Flush plugin state Register: hook!(host, PluginEvent::RestartScheduled, on_restart_scheduled); [plugins.reload_requested] id=13 Summary: Plugin reload API was called. When: POST /api/system/plugins/reload accepted. Payload: JSON: { plugin_count, delay_ms }. Cancelable: no Details: This event fires before restart.scheduled. Use cases: - Audit plugin reloads - Notify external automation Register: hook!(host, PluginEvent::PluginReloadRequested, on_reload); [node.identity_generated] id=14 Summary: Node identity was generated. When: First boot with empty uuid/token_id/token fields. Payload: JSON: { uuid, token_id }. Cancelable: no Details: The token secret is never emitted. Use cases: - First boot inventory - Operator notifications Register: hook!(host, PluginEvent::NodeIdentityGenerated, on_identity); [plugins.config_mutated] id=15 Summary: Plugin config mutation changed raw YAML. When: After config.mutate hooks run and output differs from input. Payload: JSON: { hook_count, input_len, output_len }. Cancelable: no Details: Fires once per startup if any config hook changes the YAML. Use cases: - Config audit - Plugin diagnostics Register: hook!(host, PluginEvent::PluginConfigMutated, on_config_mutated); [plugins.request_short_circuited] id=16 Summary: A request hook returned an HTTP response. When: request.intercept or middleware.inject returns REQUEST_RESPOND. Payload: JSON: { phase, method, path, status }. Cancelable: no Details: No downstream handler runs after this event. Use cases: - Custom auth - Maintenance responses Register: hook!(host, PluginEvent::PluginRequestShortCircuited, on_short_circuit); [plugins.json_response_mutated] id=17 Summary: A JSON response body hook changed a response. When: After json.response returns modified JSON. Payload: JSON: { method, path, input_len, output_len }. Cancelable: no Details: Fires only when output bytes differ from input bytes. Use cases: - Response enrichment - Compatibility shims Register: hook!(host, PluginEvent::PluginJsonResponseMutated, on_json_body); [plugins.json_actions_mutated] id=18 Summary: A JSON actions hook changed an actions array. When: After json.actions returns modified JSON. Payload: JSON: { method, path, input_len, output_len }. Cancelable: no Details: The result is inserted into the top-level actions field. Use cases: - Panel action extensions - Custom links Register: hook!(host, PluginEvent::PluginJsonActionsMutated, on_json_actions); [plugins.route_invoked] id=19 Summary: A plugin route handled a request. When: After a route.register handler returns ROUTE_OK. Payload: JSON: { plugin, method, path, status }. Cancelable: no Details: Plugin routes use the same bearer auth as /api/system. Use cases: - Plugin route metrics - Auditing custom APIs Register: hook!(host, PluginEvent::PluginRouteInvoked, on_route_invoked); [plugins.route_failed] id=20 Summary: A plugin route handler failed. When: After the handler returns a negative route status or request body read fails. Payload: JSON: { plugin, method, path, error }. Cancelable: no Details: The daemon returns a 4xx/5xx response for the request. Use cases: - Plugin error alerts - Debug custom endpoints Register: hook!(host, PluginEvent::PluginRouteFailed, on_route_failed); [cloudpanel.site.create_requested] id=21 Summary: A site (webspace) create was requested. When: Before CloudPanel command hooks run for site:add:* operations. Payload: JSON: { operation, command, args, siteType?, status?, duration_ms, error?, hook_handlers }. Cancelable: no Details: Covers static, Node.js, Python, reverse-proxy, and PHP site creation. Arguments are redacted. Use cases: - Pre-provision checks - Quota enforcement before site creation Register: hook!(host, PluginEvent::CloudPanelSiteCreateRequested, on_site_create_requested); [cloudpanel.site.created] id=22 Summary: A site (webspace) was created successfully. When: After clpctl exits successfully for site:add:* operations. Payload: JSON: { operation, command, args, siteType?, status?, duration_ms, error?, hook_handlers }. Cancelable: no Details: Emitted once the CloudPanel CLI finishes creating the webspace. Use cases: - Welcome emails - DNS provisioning - Post-create automation Register: hook!(host, PluginEvent::CloudPanelSiteCreated, on_site_created); [cloudpanel.site.create_failed] id=23 Summary: A site (webspace) create failed or was cancelled. When: After hooks cancel site creation or clpctl exits with an error. Payload: JSON: { operation, command, args, siteType?, status?, duration_ms, error?, hook_handlers }. Cancelable: no Details: The error field contains the cancellation reason or sanitized CLI error. Use cases: - Create failure alerts - Rollback hooks Register: hook!(host, PluginEvent::CloudPanelSiteCreateFailed, on_site_create_failed); [cloudpanel.site.delete_requested] id=24 Summary: A site (webspace) delete was requested. When: Before CloudPanel command hooks run for site:delete. Payload: JSON: { operation, command, args, siteType?, status?, duration_ms, error?, hook_handlers }. Cancelable: no Details: Arguments are redacted before lifecycle payloads are emitted. Use cases: - Block destructive deletes - Maintenance windows Register: hook!(host, PluginEvent::CloudPanelSiteDeleteRequested, on_site_delete_requested); [cloudpanel.site.deleted] id=25 Summary: A site (webspace) was deleted successfully. When: After clpctl exits successfully for site:delete. Payload: JSON: { operation, command, args, siteType?, status?, duration_ms, error?, hook_handlers }. Cancelable: no Details: Emitted once the CloudPanel CLI finishes removing the webspace. Use cases: - Cleanup external resources - Audit trails Register: hook!(host, PluginEvent::CloudPanelSiteDeleted, on_site_deleted); [cloudpanel.site.delete_failed] id=26 Summary: A site (webspace) delete failed or was cancelled. When: After hooks cancel site deletion or clpctl exits with an error. Payload: JSON: { operation, command, args, siteType?, status?, duration_ms, error?, hook_handlers }. Cancelable: no Details: The error field contains the cancellation reason or sanitized CLI error. Use cases: - Delete failure alerts - Safety policy enforcement Register: hook!(host, PluginEvent::CloudPanelSiteDeleteFailed, on_site_delete_failed); [cloudpanel.database.create_requested] id=27 Summary: A database create was requested. When: Before CloudPanel command hooks run for db:add. Payload: JSON: { operation, command, args, siteType?, status?, duration_ms, error?, hook_handlers }. Cancelable: no Details: Arguments are redacted before lifecycle payloads are emitted. Use cases: - Database quota checks - Naming policy validation Register: hook!(host, PluginEvent::CloudPanelDatabaseCreateRequested, on_database_create_requested); [cloudpanel.database.created] id=28 Summary: A database was created successfully. When: After clpctl exits successfully for db:add. Payload: JSON: { operation, command, args, siteType?, status?, duration_ms, error?, hook_handlers }. Cancelable: no Details: Emitted once the CloudPanel CLI finishes creating the database. Use cases: - Grant external access - Backup scheduling Register: hook!(host, PluginEvent::CloudPanelDatabaseCreated, on_database_created); [cloudpanel.database.create_failed] id=29 Summary: A database create failed or was cancelled. When: After hooks cancel database creation or clpctl exits with an error. Payload: JSON: { operation, command, args, siteType?, status?, duration_ms, error?, hook_handlers }. Cancelable: no Details: The error field contains the cancellation reason or sanitized CLI error. Use cases: - Create failure alerts Register: hook!(host, PluginEvent::CloudPanelDatabaseCreateFailed, on_database_create_failed); [cloudpanel.database.delete_requested] id=30 Summary: A database delete was requested. When: Before CloudPanel command hooks run for db:delete. Payload: JSON: { operation, command, args, siteType?, status?, duration_ms, error?, hook_handlers }. Cancelable: no Details: Arguments are redacted before lifecycle payloads are emitted. Use cases: - Block destructive deletes - Backup-before-delete checks Register: hook!(host, PluginEvent::CloudPanelDatabaseDeleteRequested, on_database_delete_requested); [cloudpanel.database.deleted] id=31 Summary: A database was deleted successfully. When: After clpctl exits successfully for db:delete. Payload: JSON: { operation, command, args, siteType?, status?, duration_ms, error?, hook_handlers }. Cancelable: no Details: Emitted once the CloudPanel CLI finishes removing the database. Use cases: - Cleanup external replicas - Audit trails Register: hook!(host, PluginEvent::CloudPanelDatabaseDeleted, on_database_deleted); [cloudpanel.database.delete_failed] id=32 Summary: A database delete failed or was cancelled. When: After hooks cancel database deletion or clpctl exits with an error. Payload: JSON: { operation, command, args, siteType?, status?, duration_ms, error?, hook_handlers }. Cancelable: no Details: The error field contains the cancellation reason or sanitized CLI error. Use cases: - Delete failure alerts Register: hook!(host, PluginEvent::CloudPanelDatabaseDeleteFailed, on_database_delete_failed); [cloudpanel.database.export_requested] id=33 Summary: A database export was requested. When: Before CloudPanel command hooks run for db:export. Payload: JSON: { operation, command, args, siteType?, status?, duration_ms, error?, hook_handlers }. Cancelable: no Details: Arguments are redacted before lifecycle payloads are emitted. Use cases: - Export path validation - Rate limiting Register: hook!(host, PluginEvent::CloudPanelDatabaseExportRequested, on_database_export_requested); [cloudpanel.database.exported] id=34 Summary: A database was exported successfully. When: After clpctl exits successfully for db:export. Payload: JSON: { operation, command, args, siteType?, status?, duration_ms, error?, hook_handlers }. Cancelable: no Details: Emitted once the CloudPanel CLI finishes writing the dump file. Use cases: - Upload to object storage - Notify operators Register: hook!(host, PluginEvent::CloudPanelDatabaseExported, on_database_exported); [cloudpanel.database.export_failed] id=35 Summary: A database export failed or was cancelled. When: After hooks cancel the export or clpctl exits with an error. Payload: JSON: { operation, command, args, siteType?, status?, duration_ms, error?, hook_handlers }. Cancelable: no Details: The error field contains the cancellation reason or sanitized CLI error. Use cases: - Export failure alerts Register: hook!(host, PluginEvent::CloudPanelDatabaseExportFailed, on_database_export_failed); [cloudpanel.user.password_reset_requested] id=36 Summary: A user password reset was requested. When: Before CloudPanel command hooks run for user:reset:password. Payload: JSON: { operation, command, args, siteType?, status?, duration_ms, error?, hook_handlers }. Cancelable: no Details: Password arguments are redacted before lifecycle payloads are emitted. Use cases: - Password policy checks - Account lockout rules Register: hook!(host, PluginEvent::CloudPanelUserPasswordResetRequested, on_password_reset_requested); [cloudpanel.user.password_reset] id=37 Summary: A user password was reset successfully. When: After clpctl exits successfully for user:reset:password. Payload: JSON: { operation, command, args, siteType?, status?, duration_ms, error?, hook_handlers }. Cancelable: no Details: Emitted once the CloudPanel CLI finishes updating the password. Use cases: - Notify user - Session invalidation hooks Register: hook!(host, PluginEvent::CloudPanelUserPasswordReset, on_password_reset); [cloudpanel.user.password_reset_failed] id=38 Summary: A user password reset failed or was cancelled. When: After hooks cancel the reset or clpctl exits with an error. Payload: JSON: { operation, command, args, siteType?, status?, duration_ms, error?, hook_handlers }. Cancelable: no Details: The error field contains the cancellation reason or sanitized CLI error. Use cases: - Reset failure alerts Register: hook!(host, PluginEvent::CloudPanelUserPasswordResetFailed, on_password_reset_failed); [cloudpanel.user.mfa_disable_requested] id=39 Summary: A user MFA disable was requested. When: Before CloudPanel command hooks run for user:disable:mfa. Payload: JSON: { operation, command, args, siteType?, status?, duration_ms, error?, hook_handlers }. Cancelable: no Details: Arguments are redacted before lifecycle payloads are emitted. Use cases: - Security policy checks - Require approval workflows Register: hook!(host, PluginEvent::CloudPanelUserMfaDisableRequested, on_mfa_disable_requested); [cloudpanel.user.mfa_disabled] id=40 Summary: User MFA was disabled successfully. When: After clpctl exits successfully for user:disable:mfa. Payload: JSON: { operation, command, args, siteType?, status?, duration_ms, error?, hook_handlers }. Cancelable: no Details: Emitted once the CloudPanel CLI finishes disabling MFA. Use cases: - Security audit logs - Notify security team Register: hook!(host, PluginEvent::CloudPanelUserMfaDisabled, on_mfa_disabled); [cloudpanel.user.mfa_disable_failed] id=41 Summary: A user MFA disable failed or was cancelled. When: After hooks cancel MFA disable or clpctl exits with an error. Payload: JSON: { operation, command, args, siteType?, status?, duration_ms, error?, hook_handlers }. Cancelable: no Details: The error field contains the cancellation reason or sanitized CLI error. Use cases: - Security alerts Register: hook!(host, PluginEvent::CloudPanelUserMfaDisableFailed, on_mfa_disable_failed); [cloudpanel.certificate.install_requested] id=42 Summary: A Let's Encrypt certificate install was requested. When: Before CloudPanel command hooks run for lets-encrypt:install:certificate. Payload: JSON: { operation, command, args, siteType?, status?, duration_ms, error?, hook_handlers }. Cancelable: no Details: Arguments are redacted before lifecycle payloads are emitted. Use cases: - Domain validation - Rate limiting ACME requests Register: hook!(host, PluginEvent::CloudPanelCertificateInstallRequested, on_certificate_install_requested); [cloudpanel.certificate.installed] id=43 Summary: A Let's Encrypt certificate was installed successfully. When: After clpctl exits successfully for lets-encrypt:install:certificate. Payload: JSON: { operation, command, args, siteType?, status?, duration_ms, error?, hook_handlers }. Cancelable: no Details: Emitted once the CloudPanel CLI finishes installing the certificate. Use cases: - CDN upload - Certificate expiry monitoring Register: hook!(host, PluginEvent::CloudPanelCertificateInstalled, on_certificate_installed); [cloudpanel.certificate.install_failed] id=44 Summary: A certificate install failed or was cancelled. When: After hooks cancel the install or clpctl exits with an error. Payload: JSON: { operation, command, args, siteType?, status?, duration_ms, error?, hook_handlers }. Cancelable: no Details: The error field contains the cancellation reason or sanitized CLI error. Use cases: - TLS failure alerts Register: hook!(host, PluginEvent::CloudPanelCertificateInstallFailed, on_certificate_install_failed); [cloudpanel.vhost_templates.import_requested] id=45 Summary: A vhost template import was requested. When: Before CloudPanel command hooks run for vhost-templates:import. Payload: JSON: { operation, command, args, siteType?, status?, duration_ms, error?, hook_handlers }. Cancelable: no Details: No CLI arguments are required for this operation. Use cases: - Validate template sources - Maintenance windows Register: hook!(host, PluginEvent::CloudPanelVhostTemplatesImportRequested, on_vhost_import_requested); [cloudpanel.vhost_templates.imported] id=46 Summary: Vhost templates were imported successfully. When: After clpctl exits successfully for vhost-templates:import. Payload: JSON: { operation, command, args, siteType?, status?, duration_ms, error?, hook_handlers }. Cancelable: no Details: Emitted once the CloudPanel CLI finishes importing templates. Use cases: - Template cache refresh - Notify operators Register: hook!(host, PluginEvent::CloudPanelVhostTemplatesImported, on_vhost_imported); [cloudpanel.vhost_templates.import_failed] id=47 Summary: A vhost template import failed or was cancelled. When: After hooks cancel the import or clpctl exits with an error. Payload: JSON: { operation, command, args, siteType?, status?, duration_ms, error?, hook_handlers }. Cancelable: no Details: The error field contains the cancellation reason or sanitized CLI error. Use cases: - Import failure alerts Register: hook!(host, PluginEvent::CloudPanelVhostTemplatesImportFailed, on_vhost_import_failed); ------------------------------------------------------------------------------ CONFIG HOOKS ------------------------------------------------------------------------------ [config.mutate] Summary: Rewrite config YAML before FeatherFly applies settings. When: After all plugins init, before directories/logging/pid are created. Input: Raw config.yml bytes from disk. Output: Replacement YAML bytes via write_yaml_output. Pipeline: Runs in plugin load order. Each hook receives the previous hook's output. Routes: N/A — global config pipeline, not route-scoped. Source: application/src/plugins/events.rs Register: hook_config!(host, on_config_mutate); ------------------------------------------------------------------------------ REQUEST HOOKS ------------------------------------------------------------------------------ [request.intercept] Summary: Inspect inbound HTTP requests before handlers run. When: On every matching request, outermost request hook layer. Input: Method code, path, headers JSON (lowercase keys), body bytes. Output: REQUEST_CONTINUE or short-circuit via write_request_response. Pipeline: Runs before middleware.inject and before Axum handlers. Routes: Prefix match: `/api/*`, `/plugins/hello`, `*` for all routes. Source: application/src/plugins/request_middleware.rs Register: hook_request!(host, RequestHookPhase::Intercept, "/api/*", on_intercept); [middleware.inject] Summary: Inner request middleware layer before handlers. When: After request.intercept hooks, before handle_request logging. Input: Same as request.intercept. Output: Same as request.intercept. Pipeline: Runs after intercept hooks on the same route prefix. Routes: Same prefix rules as request.intercept. Source: application/src/plugins/request_middleware.rs Register: hook_request!(host, RequestHookPhase::Middleware, "/api/*", on_middleware); ------------------------------------------------------------------------------ ROUTE HOOKS ------------------------------------------------------------------------------ [route.register] Summary: Expose new HTTP routes on the daemon router. When: Registered during init; routes active once HTTP server starts. Input: Request body bytes for matching method + path. Output: Response body + status via write_route_response. Pipeline: Plugin routes merge into the main Axum router alongside core API routes. Routes: Exact path per registration. Method must match (GET/POST/PUT/PATCH/DELETE). Source: application/src/plugins/routes.rs Register: route!(host, HTTP_METHOD_GET, "/plugins/hello", on_hello); ------------------------------------------------------------------------------ JSON HOOKS ------------------------------------------------------------------------------ [json.response] Summary: Mutate the entire JSON response object. Input: Serialized JSON response body bytes. Pipeline: Runs first. Output replaces the response object before action hooks run. Routes: Prefix match on request path. Examples: `/api/system`, `/api`, `*`. Details: Return JSON_MUTATE_MODIFIED after calling write_json_output, or JSON_MUTATE_UNCHANGED to keep the prior value. Register: hook_json!(host, JsonMutateTarget::ResponseBody, "/api/system", on_body); [json.actions] Summary: Mutate only the top-level actions array. Input: Serialized JSON array from the response actions field, or [] when absent. Pipeline: Runs after json.response. Output is inserted into the top-level actions field. Routes: Same prefix rules as json.response. Details: Use this for panel steps, links, and plugin-specific operator actions. Register: hook_json!(host, JsonMutateTarget::ResponseActions, "/api/system", on_actions); ------------------------------------------------------------------------------ CAPABILITIES ------------------------------------------------------------------------------ - Listen to daemon lifecycle events (startup, shutdown, config load) - Run code when other plugins finish loading - Log messages to the daemon log via log_info - Rewrite config YAML before the daemon applies settings (config.mutate) - Intercept inbound HTTP requests before handlers run (request.intercept) - Inject request middleware layers (middleware.inject) - Register new HTTP routes on the daemon router (route.register) - Modify JSON API response bodies before they are sent to clients - Inject, remove, or rewrite action steps in API responses - Inspect, mutate, or cancel CloudPanel CLI commands before execution - Cancel remaining handlers for the same event (lifecycle hooks only) ------------------------------------------------------------------------------ FULL EXAMPLE PLUGIN (all hook types) ------------------------------------------------------------------------------ use featherfly_plugin_sdk::{ declare_plugin, hook, hook_cloudpanel, hook_config, hook_json, hook_request, log_info, route, write_cloudpanel_cancel, write_json_output, write_route_response, write_yaml_output, CloudPanelCommandContext, ConfigMutateContext, EventContext, HookResult, HostApi, JsonMutateContext, JsonMutateTarget, PluginEvent, RequestHookContext, RequestHookPhase, RouteHandlerContext, CONFIG_MUTATE_UNCHANGED, CLOUDPANEL_CONTINUE, HTTP_METHOD_GET, JSON_MUTATE_UNCHANGED, REQUEST_CONTINUE, }; extern "C" fn init(host: *const HostApi) -> i32 { hook!(host, PluginEvent::DaemonStarted, on_started); hook_config!(host, on_config); hook_request!(host, RequestHookPhase::Intercept, "/api/*", on_intercept); hook_json!(host, JsonMutateTarget::ResponseBody, "/api/system", on_body); hook_cloudpanel!(host, on_cloudpanel); route!(host, HTTP_METHOD_GET, "/plugins/hello", on_route); unsafe { log_info(host, "ready") }; 0 } extern "C" fn on_started(_ctx: *const EventContext) -> HookResult { HookResult::r#continue() } extern "C" fn on_config(ctx: *const ConfigMutateContext) -> i32 { let ctx = unsafe { &*ctx }; let input = unsafe { std::slice::from_raw_parts(ctx.yaml_in_ptr, ctx.yaml_in_len) }; if input.starts_with(b"# my-plugin\n") { return CONFIG_MUTATE_UNCHANGED; } let mut out = b"# my-plugin\n".to_vec(); out.extend_from_slice(input); write_yaml_output(ctx, &out) } extern "C" fn on_intercept(_ctx: *const RequestHookContext) -> i32 { REQUEST_CONTINUE } extern "C" fn on_body(ctx: *const JsonMutateContext) -> i32 { let ctx = unsafe { &*ctx }; let input = unsafe { std::slice::from_raw_parts(ctx.json_in_ptr, ctx.json_in_len) }; let Ok(mut value) = serde_json::from_slice::(input) else { return JSON_MUTATE_UNCHANGED; }; if let Some(map) = value.as_object_mut() { map.insert("my_plugin".into(), true.into()); } let Ok(out) = serde_json::to_vec(&value) else { return JSON_MUTATE_UNCHANGED }; write_json_output(ctx, &out) } extern "C" fn on_cloudpanel(ctx: *const CloudPanelCommandContext) -> i32 { let ctx = unsafe { &*ctx }; let command = unsafe { std::slice::from_raw_parts(ctx.command_ptr, ctx.command_len) }; if command == b"site:delete" { return write_cloudpanel_cancel(ctx, b"site deletion disabled by my-plugin"); } CLOUDPANEL_CONTINUE } extern "C" fn on_route(ctx: *const RouteHandlerContext) -> i32 { let ctx = unsafe { &*ctx }; write_route_response(ctx, 200, br#"{"ok":true}"#) } declare_plugin! { name: "my-plugin", version: "0.1.0", init: init } ------------------------------------------------------------------------------ QUICK EVENT INDEX ------------------------------------------------------------------------------ ID EVENT NAME 1 daemon.starting 2 daemon.started 3 daemon.stopping 4 config.loaded 5 plugin.loaded 6 request.received 7 request.completed 8 request.not_found 9 request.blocked 10 probe.client_blocked 11 auth.failed 12 restart.scheduled 13 plugins.reload_requested 14 node.identity_generated 15 plugins.config_mutated 16 plugins.request_short_circuited 17 plugins.json_response_mutated 18 plugins.json_actions_mutated 19 plugins.route_invoked 20 plugins.route_failed 21 cloudpanel.site.create_requested 22 cloudpanel.site.created 23 cloudpanel.site.create_failed 24 cloudpanel.site.delete_requested 25 cloudpanel.site.deleted 26 cloudpanel.site.delete_failed 27 cloudpanel.database.create_requested 28 cloudpanel.database.created 29 cloudpanel.database.create_failed 30 cloudpanel.database.delete_requested 31 cloudpanel.database.deleted 32 cloudpanel.database.delete_failed 33 cloudpanel.database.export_requested 34 cloudpanel.database.exported 35 cloudpanel.database.export_failed 36 cloudpanel.user.password_reset_requested 37 cloudpanel.user.password_reset 38 cloudpanel.user.password_reset_failed 39 cloudpanel.user.mfa_disable_requested 40 cloudpanel.user.mfa_disabled 41 cloudpanel.user.mfa_disable_failed 42 cloudpanel.certificate.install_requested 43 cloudpanel.certificate.installed 44 cloudpanel.certificate.install_failed 45 cloudpanel.vhost_templates.import_requested 46 cloudpanel.vhost_templates.imported 47 cloudpanel.vhost_templates.import_failed