feat(TASK-033): add hook config schema across all scopes
Introduce src/hooks.rs as a types-only module (HookDef, HooksMap) and plumb hooks:/global_hooks: config fields through every scope: - Config + AppConfig hooks field, both Default impls, the manual from_config copy, and a COYOTE_HOOKS env override - Role frontmatter: hand-parsed hooks arm, private field, getter, and JSON-inline re-serialization in Role::export (malformed maps degrade to None) - AgentConfig + Graph: hooks + global_hooks on both structs, copied in AgentConfig::from_graph, with a per-agent global_hooks env override - paths::hooks_dir() = <config_dir>/hooks with COYOTE_HOOKS_DIR override - Commented hooks: block in the global config template (global scope only; no global_hooks key) No engine or dispatch logic yet; existing configs parse unchanged.
This commit is contained in:
12 files changed
+344
-1
No files matched your search
@@ -1,5 +1,6 @@
|
||||
use super::*;
|
||||
|
||||
use crate::hooks::HooksMap;
|
||||
use crate::{
|
||||
client::Model,
|
||||
config::memory,
|
||||
@@ -848,6 +849,10 @@ pub struct AgentConfig {
|
||||
pub mcp_tools: Option<IndexMap<String, Vec<String>>>,
|
||||
#[serde(default)]
|
||||
pub global_tools: Vec<String>,
|
||||
#[serde(default)]
|
||||
pub hooks: HooksMap,
|
||||
#[serde(default)]
|
||||
pub global_hooks: Vec<String>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub skills_enabled: Option<bool>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
@@ -917,6 +922,8 @@ impl AgentConfig {
|
||||
reasoning_effort: graph.reasoning_effort.clone(),
|
||||
description: graph.description.clone(),
|
||||
global_tools: graph.global_tools.clone(),
|
||||
hooks: graph.hooks.clone(),
|
||||
global_hooks: graph.global_hooks.clone(),
|
||||
mcp_servers: graph.mcp_servers.clone(),
|
||||
mcp_tools: graph.mcp_tools.clone(),
|
||||
skills_enabled: graph.skills_enabled,
|
||||
@@ -965,6 +972,11 @@ impl AgentConfig {
|
||||
{
|
||||
self.global_tools = v;
|
||||
}
|
||||
if let Ok(v) = env::var(with_prefix("global_hooks"))
|
||||
&& let Ok(v) = serde_json::from_str(&v)
|
||||
{
|
||||
self.global_hooks = v;
|
||||
}
|
||||
if let Ok(v) = env::var(with_prefix("mcp_servers"))
|
||||
&& let Ok(v) = serde_json::from_str(&v)
|
||||
{
|
||||
@@ -1336,6 +1348,58 @@ variables:
|
||||
assert_eq!(config.variables[0].name, "username");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_config_hooks_and_global_hooks_round_trip() {
|
||||
let yaml = "\
|
||||
name: hooked
|
||||
instructions: hi
|
||||
hooks:
|
||||
tool.started:
|
||||
- name: notify
|
||||
command: ./hooks/notify.sh
|
||||
global_hooks:
|
||||
- tool.started.notify
|
||||
";
|
||||
let config: AgentConfig = serde_yaml::from_str(yaml).unwrap();
|
||||
|
||||
assert_eq!(config.hooks["tool.started"][0].name, "notify");
|
||||
assert_eq!(config.hooks["tool.started"][0].command, "./hooks/notify.sh");
|
||||
assert_eq!(config.global_hooks, vec!["tool.started.notify"]);
|
||||
|
||||
let serialized = serde_yaml::to_string(&config).unwrap();
|
||||
let reparsed: AgentConfig = serde_yaml::from_str(&serialized).unwrap();
|
||||
|
||||
assert_eq!(reparsed.hooks, config.hooks);
|
||||
assert_eq!(reparsed.global_hooks, config.global_hooks);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn from_graph_carries_hooks_and_global_hooks() {
|
||||
let yaml = "\
|
||||
name: g
|
||||
start: e
|
||||
hooks:
|
||||
turn.completed:
|
||||
- name: webhook
|
||||
command: curl -s https://example.com/hook
|
||||
global_hooks:
|
||||
- turn.completed.webhook
|
||||
nodes:
|
||||
e:
|
||||
id: e
|
||||
type: end
|
||||
output: done
|
||||
";
|
||||
let graph: graph::Graph = serde_yaml::from_str(yaml).unwrap();
|
||||
|
||||
let config = AgentConfig::from_graph("g", &graph);
|
||||
|
||||
assert_eq!(config.hooks, graph.hooks);
|
||||
assert_eq!(config.global_hooks, graph.global_hooks);
|
||||
assert_eq!(config.hooks["turn.completed"][0].name, "webhook");
|
||||
assert_eq!(config.global_hooks, vec!["turn.completed.webhook"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn agent_config_defaults() {
|
||||
let yaml = "name: minimal\ninstructions: hi\n";
|
||||
@@ -1350,6 +1414,8 @@ variables:
|
||||
assert_eq!(config.escalation_timeout, 0);
|
||||
assert!(config.mcp_servers.is_empty());
|
||||
assert!(config.global_tools.is_empty());
|
||||
assert!(config.hooks.is_empty());
|
||||
assert!(config.global_hooks.is_empty());
|
||||
assert!(config.conversation_starters.is_empty());
|
||||
assert!(config.variables.is_empty());
|
||||
assert!(config.model_id.is_none());
|
||||
|
||||
Reference in new issue
Block a user