Keep invalid hook types advisory

This commit is contained in:
Ahmed Ibrahim
2026-05-07 05:28:26 +03:00
parent b487b3592d
commit f366ac3326
2 changed files with 86 additions and 1 deletions

View File

@@ -5,7 +5,10 @@ use std::path::PathBuf;
use codex_protocol::protocol::HookEventName;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Deserializer;
use serde::Serialize;
use serde::de::Error as SerdeError;
use serde_json::Value as JsonValue;
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
pub struct HooksFile {
@@ -102,10 +105,36 @@ impl HookEventsToml {
pub struct MatcherGroup {
#[serde(default)]
pub matcher: Option<String>,
#[serde(default)]
#[serde(default, deserialize_with = "deserialize_hook_handlers")]
pub hooks: Vec<HookHandlerConfig>,
}
/// Deserialize hook handlers while dropping entries with unknown tagged variants.
///
/// The schema warning pass reports invalid `type` values before typed config
/// deserialization. Dropping only those entries keeps startup warnings
/// non-blocking without making unrelated hook shape errors silent.
fn deserialize_hook_handlers<'de, D>(deserializer: D) -> Result<Vec<HookHandlerConfig>, D::Error>
where
D: Deserializer<'de>,
{
let values = Vec::<JsonValue>::deserialize(deserializer)?;
let mut handlers = Vec::new();
for value in values {
let invalid_type = value.get("type").is_some_and(|handler_type| {
!matches!(handler_type.as_str(), Some("command" | "prompt" | "agent"))
});
match serde_json::from_value(value) {
Ok(handler) => handlers.push(handler),
Err(_) if invalid_type => {}
Err(err) => return Err(SerdeError::custom(err)),
}
}
Ok(handlers)
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(tag = "type")]
pub enum HookHandlerConfig {

View File

@@ -128,6 +128,62 @@ command = "python3 /tmp/pre.py"
);
}
#[test]
fn hooks_toml_drops_unknown_handler_type() {
let parsed: HooksToml = toml::from_str(
r#"
[[UserPromptSubmit]]
matcher = "^UserPromptSubmit$"
[[UserPromptSubmit.hooks]]
type = "python"
command = "python3 /tmp/ignored.py"
[[UserPromptSubmit.hooks]]
type = 7
command = "python3 /tmp/also-ignored.py"
[[UserPromptSubmit.hooks]]
type = "command"
command = "python3 /tmp/kept.py"
"#,
)
.expect("unknown hook handler type should be dropped");
assert_eq!(
parsed,
HooksToml {
events: HookEventsToml {
user_prompt_submit: vec![MatcherGroup {
matcher: Some("^UserPromptSubmit$".to_string()),
hooks: vec![HookHandlerConfig::Command {
command: "python3 /tmp/kept.py".to_string(),
timeout_sec: None,
r#async: false,
status_message: None,
}],
}],
..Default::default()
},
state: BTreeMap::new(),
}
);
}
#[test]
fn hooks_toml_keeps_non_enum_handler_errors_strict() {
let result = toml::from_str::<HooksToml>(
r#"
[[UserPromptSubmit]]
[[UserPromptSubmit.hooks]]
type = "command"
"#,
);
assert!(result.is_err());
}
#[test]
fn managed_hooks_requirements_flatten_hook_events() {
let parsed: ManagedHooksRequirementsToml = toml::from_str(