orcs_hook/lib.rs
1//! Hook system for ORCS CLI.
2//!
3//! This crate provides the hook abstraction layer for the ORCS
4//! (Orchestrated Runtime for Collaborative Systems) architecture.
5//!
6//! # Crate Architecture
7//!
8//! This crate sits between the **Plugin SDK** and **Runtime** layers:
9//!
10//! ```text
11//! ┌─────────────────────────────────────────────────────────────┐
12//! │ Plugin SDK Layer │
13//! │ (External, SemVer stable, safe to depend on) │
14//! ├─────────────────────────────────────────────────────────────┤
15//! │ orcs-types : ID types, Principal, ErrorCode │
16//! │ orcs-event : Signal, Request, Event │
17//! │ orcs-component : Component trait (WIT target) │
18//! └─────────────────────────────────────────────────────────────┘
19//! ↕ depends on SDK, depended on by Runtime
20//! ┌─────────────────────────────────────────────────────────────┐
21//! │ Hook Layer ◄── HERE │
22//! ├─────────────────────────────────────────────────────────────┤
23//! │ orcs-hook : Hook trait, Registry, FQL, Config │
24//! └─────────────────────────────────────────────────────────────┘
25//! ↕
26//! ┌─────────────────────────────────────────────────────────────┐
27//! │ Runtime Layer │
28//! ├─────────────────────────────────────────────────────────────┤
29//! │ orcs-runtime : Session, EventBus, ChannelRunner │
30//! └─────────────────────────────────────────────────────────────┘
31//! ```
32//!
33//! # Overview
34//!
35//! Hooks allow cross-cutting concerns (logging, auditing, capability
36//! injection, payload transformation, metrics, etc.) to be injected
37//! at lifecycle points throughout the ORCS runtime via a single,
38//! unified configuration interface.
39//!
40//! # Core Concepts
41//!
42//! ## Hook Points
43//!
44//! [`HookPoint`] enumerates 26 lifecycle points across 8 categories:
45//! Component, Request, Signal, Child, Channel, Tool, Auth, and EventBus.
46//!
47//! ## FQL (Fully Qualified Locator)
48//!
49//! [`FqlPattern`] provides pattern matching for component addressing:
50//!
51//! ```text
52//! <scope>::<target>[/<child_path>][#<instance>]
53//! ```
54//!
55//! Examples: `"builtin::llm"`, `"*::*"`, `"builtin::llm/agent-1"`.
56//!
57//! ## Hook Trait
58//!
59//! The [`Hook`] trait defines a single hook handler:
60//!
61//! ```ignore
62//! pub trait Hook: Send + Sync {
63//! fn id(&self) -> &str;
64//! fn fql_pattern(&self) -> &FqlPattern;
65//! fn hook_point(&self) -> HookPoint;
66//! fn priority(&self) -> i32 { 100 }
67//! fn execute(&self, ctx: HookContext) -> HookAction;
68//! }
69//! ```
70//!
71//! ## Hook Actions
72//!
73//! [`HookAction`] determines what happens after a hook executes:
74//!
75//! - `Continue(ctx)` — pass (modified) context downstream
76//! - `Skip(value)` — skip the operation (pre-hooks only)
77//! - `Abort { reason }` — abort with error (pre-hooks only)
78//! - `Replace(value)` — replace result payload (post-hooks only)
79//!
80//! ## Registry
81//!
82//! [`HookRegistry`] is the central dispatch engine. It manages
83//! hook registration, priority ordering, FQL filtering, and
84//! chain execution semantics.
85//!
86//! ## Configuration
87//!
88//! [`HooksConfig`] and [`HookDef`] provide TOML-serializable
89//! declarative hook definitions for `OrcsConfig` integration.
90//!
91//! # Concurrency
92//!
93//! The registry is designed to be wrapped in
94//! `Arc<std::sync::RwLock<HookRegistry>>` following the same pattern
95//! as `SharedChannelHandles` in the runtime.
96//!
97//! # Example
98//!
99//! ```
100//! use orcs_hook::{
101//! HookRegistry, HookPoint, HookContext, HookAction, FqlPattern, Hook,
102//! };
103//! use orcs_types::{ComponentId, ChannelId, Principal};
104//! use serde_json::json;
105//!
106//! // Create a registry
107//! let mut registry = HookRegistry::new();
108//!
109//! // Build a context
110//! let ctx = HookContext::new(
111//! HookPoint::RequestPreDispatch,
112//! ComponentId::builtin("llm"),
113//! ChannelId::new(),
114//! Principal::System,
115//! 0,
116//! json!({"operation": "chat"}),
117//! );
118//!
119//! // Dispatch (no hooks registered → Continue with unchanged context)
120//! let action = registry.dispatch(
121//! HookPoint::RequestPreDispatch,
122//! &ComponentId::builtin("llm"),
123//! None,
124//! ctx,
125//! );
126//! assert!(action.is_continue());
127//! ```
128
129mod action;
130mod config;
131mod context;
132mod error;
133mod fql;
134pub mod hook;
135mod point;
136mod registry;
137
138// Re-export core types
139pub use action::HookAction;
140pub use config::{HookDef, HookDefValidationError, HooksConfig};
141pub use context::{HookContext, DEFAULT_MAX_DEPTH};
142pub use error::HookError;
143pub use fql::{FqlPattern, PatternSegment};
144pub use hook::Hook;
145pub use point::HookPoint;
146pub use registry::HookRegistry;
147
148use std::sync::{Arc, RwLock};
149
150/// Thread-safe shared reference to a [`HookRegistry`].
151///
152/// Follows the same pattern as `SharedChannelHandles` in the runtime:
153/// - `dispatch()` takes `&self` → read lock
154/// - `register()` / `unregister()` take `&mut self` → write lock
155///
156/// `std::sync::RwLock` (not tokio) because the lock is never held across
157/// `.await` points.
158pub type SharedHookRegistry = Arc<RwLock<HookRegistry>>;
159
160/// Creates a new empty [`SharedHookRegistry`].
161#[must_use]
162pub fn shared_hook_registry() -> SharedHookRegistry {
163 Arc::new(RwLock::new(HookRegistry::new()))
164}
165
166// Re-export testing utilities
167#[cfg(any(test, feature = "test-utils"))]
168pub mod testing {
169 //! Test utilities for the hook system.
170 //!
171 //! Provides [`MockHook`] for use in tests.
172 pub use crate::hook::testing::MockHook;
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use orcs_types::{ChannelId, ComponentId, Principal};
179 use serde_json::json;
180
181 #[test]
182 fn shared_registry_creation() {
183 let reg = shared_hook_registry();
184 let guard = reg
185 .read()
186 .expect("shared registry read lock should not be poisoned");
187 assert!(guard.is_empty());
188 }
189
190 #[test]
191 fn shared_registry_register_and_dispatch() {
192 let reg = shared_hook_registry();
193
194 // Write lock: register a hook
195 {
196 let mut guard = reg
197 .write()
198 .expect("shared registry write lock should not be poisoned");
199 let hook =
200 testing::MockHook::pass_through("test", "*::*", HookPoint::RequestPreDispatch);
201 guard.register(Box::new(hook));
202 assert_eq!(guard.len(), 1);
203 }
204
205 // Read lock: dispatch
206 {
207 let guard = reg
208 .read()
209 .expect("shared registry read lock should not be poisoned for dispatch");
210 let ctx = HookContext::new(
211 HookPoint::RequestPreDispatch,
212 ComponentId::builtin("llm"),
213 ChannelId::new(),
214 Principal::System,
215 0,
216 json!({"op": "test"}),
217 );
218 let action = guard.dispatch(
219 HookPoint::RequestPreDispatch,
220 &ComponentId::builtin("llm"),
221 None,
222 ctx,
223 );
224 assert!(action.is_continue());
225 }
226 }
227
228 #[test]
229 fn shared_registry_clone_shares_state() {
230 let reg = shared_hook_registry();
231 let reg2 = Arc::clone(®);
232
233 {
234 let mut guard = reg
235 .write()
236 .expect("shared registry write lock should not be poisoned for clone test");
237 guard.register(Box::new(testing::MockHook::pass_through(
238 "shared",
239 "*::*",
240 HookPoint::RequestPreDispatch,
241 )));
242 }
243
244 let guard = reg2
245 .read()
246 .expect("cloned registry read lock should reflect shared state");
247 assert_eq!(guard.len(), 1);
248 }
249}