feat(TASK-034): hook dispatch engine in src/hooks.rs
- HookEvent (22 dotted variants) + owned ResolvedHook snapshots - RequestContext::resolved_hooks: global (gated by agent global_hooks) -> role (additive) -> agent precedence; per-scope cwds; unknown whitelist entries inert + debug-logged with agent name - fire/fire_resolved: fire-and-forget detached dispatch via platform shell (sh -c / cmd /C raw_arg), null stdio, fresh unix process group, reap-only await, no-runtime guard (skip + debug, never panic) - tool.* payload files: crypto-random nonce, create_new + 0600, verbatim JSON, best-effort delete after reap, hook still runs on write failure - base env (COYOTE_EVENT/HOOK_NAME/CONFIG_DIR/EVENT_TIMESTAMP RFC3339 UTC, SESSION_ID/AGENT_NAME when present) + per-event extras; every value truncated engine-side to 2048 bytes (UTF-8-safe; payload path documented exempt) - drain_pending(timeout): opt-in exit-path seam with spawn-ack semantics (Drop-guard counter; never waits for child completion) - #[cfg(test)] synchronous capture sink (records instead of spawning) + hooks-scoped debug capture in src/testing.rs (poison-proof, boundary-aware target prefix) - Agent::hooks()/global_hooks() accessors; Role::hooks() dead_code marker removed Engine compiles unused by design; call sites land in TASK-035/036/037.
This commit is contained in:
4 files changed
+1580
-14
No files matched your search
@@ -640,6 +640,14 @@ impl Agent {
|
||||
self.config.compression_model.as_deref()
|
||||
}
|
||||
|
||||
pub fn hooks(&self) -> &HooksMap {
|
||||
&self.config.hooks
|
||||
}
|
||||
|
||||
pub fn global_hooks(&self) -> &[String] {
|
||||
&self.config.global_hooks
|
||||
}
|
||||
|
||||
pub fn is_dynamic_instructions(&self) -> bool {
|
||||
self.config.dynamic_instructions
|
||||
}
|
||||
|
||||
@@ -388,7 +388,6 @@ impl Role {
|
||||
self.memory
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn hooks(&self) -> Option<&HooksMap> {
|
||||
self.hooks.as_ref()
|
||||
}
|
||||
|
||||
+1520
File diff suppressed because it is too large.
Load diff
+52
-13
@@ -3,27 +3,64 @@
|
||||
use log::{Level, LevelFilter, Log, Metadata, Record};
|
||||
use std::sync::{Mutex, Once, OnceLock};
|
||||
|
||||
struct WarnCollector;
|
||||
struct TestLogCollector;
|
||||
|
||||
static WARN_MESSAGES: OnceLock<Mutex<Vec<String>>> = OnceLock::new();
|
||||
static DEBUG_MESSAGES: OnceLock<Mutex<Vec<String>>> = OnceLock::new();
|
||||
|
||||
/// Every warn-level message captured since the collector was installed. The
|
||||
/// buffer is process-global and shared by all tests, so assert by filtering
|
||||
/// for a marker unique to the test rather than on the whole buffer.
|
||||
/// Debug capture is restricted to the hooks module so the buffer is not
|
||||
/// flooded by debug output from the rest of the crate. Matching is
|
||||
/// module-boundary-aware: only this exact module or its `::` submodules
|
||||
/// qualify, so a sibling like `hooks_registry` would not.
|
||||
const DEBUG_TARGET_PREFIX: &str = concat!(env!("CARGO_CRATE_NAME"), "::hooks");
|
||||
|
||||
fn captures_warn(metadata: &Metadata) -> bool {
|
||||
metadata.level() <= Level::Warn
|
||||
}
|
||||
|
||||
fn captures_hooks_debug(metadata: &Metadata) -> bool {
|
||||
if metadata.level() != Level::Debug {
|
||||
return false;
|
||||
}
|
||||
let target = metadata.target();
|
||||
target == DEBUG_TARGET_PREFIX
|
||||
|| target
|
||||
.strip_prefix(DEBUG_TARGET_PREFIX)
|
||||
.is_some_and(|rest| rest.starts_with("::"))
|
||||
}
|
||||
|
||||
/// Every warn- or error-level message captured since the collector was
|
||||
/// installed. The buffer is process-global and shared by all tests, so assert
|
||||
/// by filtering for a marker unique to the test rather than on the whole
|
||||
/// buffer.
|
||||
pub(crate) fn warn_messages() -> &'static Mutex<Vec<String>> {
|
||||
WARN_MESSAGES.get_or_init(Mutex::default)
|
||||
}
|
||||
|
||||
impl Log for WarnCollector {
|
||||
/// Every debug-level message from the hooks module captured since the
|
||||
/// collector was installed. Same marker-filtering discipline as
|
||||
/// [`warn_messages`].
|
||||
pub(crate) fn debug_messages() -> &'static Mutex<Vec<String>> {
|
||||
DEBUG_MESSAGES.get_or_init(Mutex::default)
|
||||
}
|
||||
|
||||
impl Log for TestLogCollector {
|
||||
fn enabled(&self, metadata: &Metadata) -> bool {
|
||||
metadata.level() <= Level::Warn
|
||||
captures_warn(metadata) || captures_hooks_debug(metadata)
|
||||
}
|
||||
|
||||
// A test that panics while holding a buffer poisons its mutex; the logger
|
||||
// recovers via `into_inner` so every later test can still log.
|
||||
fn log(&self, record: &Record) {
|
||||
if self.enabled(record.metadata()) {
|
||||
if captures_warn(record.metadata()) {
|
||||
warn_messages()
|
||||
.lock()
|
||||
.unwrap()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||
.push(record.args().to_string());
|
||||
} else if captures_hooks_debug(record.metadata()) {
|
||||
debug_messages()
|
||||
.lock()
|
||||
.unwrap_or_else(|poisoned| poisoned.into_inner())
|
||||
.push(record.args().to_string());
|
||||
}
|
||||
}
|
||||
@@ -31,13 +68,15 @@ impl Log for WarnCollector {
|
||||
fn flush(&self) {}
|
||||
}
|
||||
|
||||
/// Installs the process-wide warn collector. `log::set_logger` accepts one
|
||||
/// logger per process, so every test that captures warns must install through
|
||||
/// this shared entry point.
|
||||
/// Installs the process-wide log collector. `log::set_logger` accepts one
|
||||
/// logger per process, so every test that captures warns or hooks debug
|
||||
/// output must install through this shared entry point. The max level is
|
||||
/// Debug so the hooks debug capture sees its records; warn capture is
|
||||
/// unaffected.
|
||||
pub(crate) fn install_warn_collector() {
|
||||
static INSTALL: Once = Once::new();
|
||||
INSTALL.call_once(|| {
|
||||
log::set_logger(&WarnCollector).expect("no other logger should be installed");
|
||||
log::set_max_level(LevelFilter::Warn);
|
||||
log::set_logger(&TestLogCollector).expect("no other logger should be installed");
|
||||
log::set_max_level(LevelFilter::Debug);
|
||||
});
|
||||
}
|
||||
Reference in new issue
Block a user