1use crate::{builtin_adapter_registry, MANAGED_HOOK_REVISION};
2use gate4agent_types::{AdapterBinding, AdapterFamily};
3use thiserror::Error;
4
5pub const MANAGED_HOOK_TIMEOUT_SECONDS: u64 = 10;
6pub const MANAGED_HOOK_TIMEOUT_MILLISECONDS: u64 = MANAGED_HOOK_TIMEOUT_SECONDS * 1_000;
7
8#[derive(Clone, Copy, Debug, Eq, PartialEq)]
9pub enum ManagedHookConfigLocation {
10 HomeRelative(&'static str),
11 EnvironmentHome {
12 variable: &'static str,
13 fallback: &'static str,
14 suffix: &'static str,
15 },
16 AppDataOrHome {
17 app_data_suffix: &'static str,
18 home_fallback: &'static str,
19 },
20 RuntimeDataRelative(&'static str),
21}
22
23#[derive(Clone, Copy, Debug, Eq, PartialEq)]
24pub enum ManagedHookConfigKind {
25 JsonHooks {
26 container: &'static str,
27 require_version_one: bool,
28 },
29 AmpPlugin,
30 KimiToml,
31}
32
33#[derive(Clone, Copy, Debug, Eq, PartialEq)]
34pub enum ManagedHookEventShape {
35 NestedCommand {
36 matcher: Option<&'static str>,
37 timeout: u64,
38 },
39 DirectCommand {
40 timeout: u64,
41 },
42}
43
44#[derive(Clone, Copy, Debug, Eq, PartialEq)]
45pub struct ManagedHookEventSpec {
46 pub name: &'static str,
47 pub shape: ManagedHookEventShape,
48 pub passes_event_name: bool,
49}
50
51#[derive(Clone, Copy, Debug, Eq, PartialEq)]
52pub struct ManagedHookAdapterSpec {
53 pub target: &'static str,
54 pub source_adapter: &'static str,
55 pub config_location: ManagedHookConfigLocation,
56 pub config_kind: ManagedHookConfigKind,
57 pub script_stem: &'static str,
58 pub events: &'static [ManagedHookEventSpec],
59}
60
61const fn nested(name: &'static str, matcher: Option<&'static str>) -> ManagedHookEventSpec {
62 ManagedHookEventSpec {
63 name,
64 shape: ManagedHookEventShape::NestedCommand {
65 matcher,
66 timeout: MANAGED_HOOK_TIMEOUT_SECONDS,
67 },
68 passes_event_name: false,
69 }
70}
71
72const fn direct(name: &'static str, passes_event_name: bool) -> ManagedHookEventSpec {
73 ManagedHookEventSpec {
74 name,
75 shape: ManagedHookEventShape::DirectCommand {
76 timeout: MANAGED_HOOK_TIMEOUT_SECONDS,
77 },
78 passes_event_name,
79 }
80}
81
82const CLAUDE_EVENTS: &[ManagedHookEventSpec] = &[
83 nested("SessionStart", None),
95 nested("UserPromptSubmit", None),
96 nested("Stop", None),
97 nested("StopFailure", None),
98 nested("SubagentStart", None),
99 nested("SubagentStop", None),
100 nested("TeammateIdle", None),
101 nested("PreToolUse", Some("*")),
102 nested("PostToolUse", Some("*")),
103 nested("PostToolUseFailure", Some("*")),
104 nested("PermissionRequest", Some("*")),
105];
106
107const CODEX_EVENTS: &[ManagedHookEventSpec] = &[
108 nested("SessionStart", None),
109 nested("UserPromptSubmit", None),
110 nested("PreToolUse", None),
111 nested("PermissionRequest", None),
112 nested("PostToolUse", None),
113 nested("Stop", None),
114];
115
116const GROK_EVENTS: &[ManagedHookEventSpec] = &[
117 nested("SessionStart", None),
118 nested("UserPromptSubmit", None),
119 nested("Stop", None),
120 nested("StopFailure", None),
121 nested("SessionEnd", None),
122 nested("PreToolUse", Some(".*")),
123 nested("PostToolUse", Some(".*")),
124 nested("PostToolUseFailure", Some(".*")),
125 nested("Notification", None),
126];
127
128const KIMI_EVENTS: &[ManagedHookEventSpec] = &[
129 direct("SessionStart", false),
137 direct("SessionEnd", false),
138 direct("Notification", false),
139 direct("UserPromptSubmit", false),
140 direct("PreToolUse", false),
141 direct("PostToolUse", false),
142 direct("PostToolUseFailure", false),
143 direct("PermissionRequest", false),
144 direct("Stop", false),
145 direct("StopFailure", false),
146];
147
148const SPECS: &[ManagedHookAdapterSpec] = &[
149 json(
150 "claude",
151 "claude-code",
152 ".claude/settings.json",
153 "claude-hook",
154 CLAUDE_EVENTS,
155 false,
156 ),
157 ManagedHookAdapterSpec {
158 target: "codex",
159 source_adapter: "codex",
160 config_location: ManagedHookConfigLocation::HomeRelative(".codex/hooks.json"),
163 config_kind: ManagedHookConfigKind::JsonHooks {
164 container: "hooks",
165 require_version_one: false,
166 },
167 script_stem: "codex-hook",
168 events: CODEX_EVENTS,
169 },
170 ManagedHookAdapterSpec {
171 target: "grok",
172 source_adapter: "grok",
173 config_location: ManagedHookConfigLocation::EnvironmentHome {
174 variable: "GROK_HOME",
175 fallback: ".grok",
176 suffix: "hooks/gate4agent-status.json",
177 },
178 config_kind: ManagedHookConfigKind::JsonHooks {
179 container: "hooks",
180 require_version_one: false,
181 },
182 script_stem: "grok-hook",
183 events: GROK_EVENTS,
184 },
185 ManagedHookAdapterSpec {
186 target: "kimi",
187 source_adapter: "kimi",
188 config_location: ManagedHookConfigLocation::EnvironmentHome {
189 variable: "KIMI_CODE_HOME",
190 fallback: ".kimi-code",
191 suffix: "config.toml",
192 },
193 config_kind: ManagedHookConfigKind::KimiToml,
194 script_stem: "kimi-hook",
195 events: KIMI_EVENTS,
196 },
197];
198
199const fn json(
200 target: &'static str,
201 source_adapter: &'static str,
202 path: &'static str,
203 script_stem: &'static str,
204 events: &'static [ManagedHookEventSpec],
205 require_version_one: bool,
206) -> ManagedHookAdapterSpec {
207 ManagedHookAdapterSpec {
208 target,
209 source_adapter,
210 config_location: ManagedHookConfigLocation::HomeRelative(path),
211 config_kind: ManagedHookConfigKind::JsonHooks {
212 container: "hooks",
213 require_version_one,
214 },
215 script_stem,
216 events,
217 }
218}
219
220pub fn managed_hook_specs() -> &'static [ManagedHookAdapterSpec] {
221 SPECS
222}
223
224pub fn managed_hook_spec(
225 binding: &AdapterBinding,
226) -> Result<&'static ManagedHookAdapterSpec, ManagedHookAdapterError> {
227 if binding.revision != MANAGED_HOOK_REVISION {
228 return Err(ManagedHookAdapterError::RevisionMismatch {
229 requested: binding.revision.clone(),
230 });
231 }
232 let registered = builtin_adapter_registry()
233 .binding(AdapterFamily::ManagedHook, binding.id.as_str())
234 .is_some_and(|registered| registered == binding);
235 if !registered {
236 return Err(ManagedHookAdapterError::UnsupportedTarget(
237 binding.id.as_str().to_owned(),
238 ));
239 }
240 SPECS
241 .iter()
242 .find(|spec| spec.target == binding.id.as_str())
243 .ok_or_else(|| ManagedHookAdapterError::UnsupportedTarget(binding.id.as_str().to_owned()))
244}
245
246#[derive(Clone, Debug, Error, Eq, PartialEq)]
247pub enum ManagedHookAdapterError {
248 #[error("managed Hook adapter target is unsupported: {0}")]
249 UnsupportedTarget(String),
250 #[error("managed Hook adapter revision mismatch: requested {requested}")]
251 RevisionMismatch { requested: String },
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257 use std::collections::BTreeSet;
258
259 #[test]
260 fn inventory_matches_the_fleet_managed_controls() {
261 let actual = managed_hook_specs()
262 .iter()
263 .map(|spec| spec.target)
264 .collect::<BTreeSet<_>>();
265 let expected = ["claude", "codex", "grok", "kimi"]
266 .into_iter()
267 .collect::<BTreeSet<_>>();
268 assert_eq!(actual, expected);
269 }
270
271 #[test]
272 fn registry_and_specs_are_revision_exact() {
273 for spec in managed_hook_specs() {
274 let binding = builtin_adapter_registry()
275 .binding(AdapterFamily::ManagedHook, spec.target)
276 .unwrap();
277 assert_eq!(managed_hook_spec(binding).unwrap(), spec);
278 }
279 }
280}