Codex Handbook
hooks/src/events/common.rs 290 lines
use codex_protocol::protocol::HookCompletedEvent;use codex_protocol::protocol::HookEventName;use codex_protocol::protocol::HookOutputEntry;use codex_protocol::protocol::HookOutputEntryKind;use codex_protocol::protocol::HookRunStatus;use codex_protocol::protocol::HookRunSummary;use crate::engine::ConfiguredHandler;use crate::engine::dispatcher;/// Identifies a thread-spawned subagent when a normal hook runs inside it.#[derive(Debug, Clone, PartialEq, Eq)]pub struct SubagentHookContext {    pub agent_id: String,    pub agent_type: String,}pub(crate) fn join_text_chunks(chunks: Vec<String>) -> Option<String> {    if chunks.is_empty() {        None    } else {        Some(chunks.join("\n\n"))    }}pub(crate) fn trimmed_non_empty(text: &str) -> Option<String> {    let trimmed = text.trim();    if trimmed.is_empty() {        None    } else {        Some(trimmed.to_string())    }}pub(crate) fn append_additional_context(    entries: &mut Vec<HookOutputEntry>,    additional_contexts_for_model: &mut Vec<String>,    additional_context: String,) {    entries.push(HookOutputEntry {        kind: HookOutputEntryKind::Context,        text: additional_context.clone(),    });    additional_contexts_for_model.push(additional_context);}pub(crate) fn flatten_additional_contexts<'a>(    additional_contexts: impl IntoIterator<Item = &'a [String]>,) -> Vec<String> {    additional_contexts        .into_iter()        .flat_map(|chunk| chunk.iter().cloned())        .collect()}pub(crate) fn serialization_failure_hook_events(    handlers: Vec<ConfiguredHandler>,    turn_id: Option<String>,    error_message: String,) -> Vec<HookCompletedEvent> {    handlers        .into_iter()        .map(|handler| {            let mut run = dispatcher::running_summary(&handler);            run.status = HookRunStatus::Failed;            run.completed_at = Some(run.started_at);            run.duration_ms = Some(0);            run.entries = vec![HookOutputEntry {                kind: HookOutputEntryKind::Error,                text: error_message.clone(),            }];            HookCompletedEvent {                turn_id: turn_id.clone(),                run,            }        })        .collect()}pub(crate) fn serialization_failure_hook_events_for_tool_use(    handlers: Vec<ConfiguredHandler>,    turn_id: Option<String>,    error_message: String,    tool_use_id: &str,) -> Vec<HookCompletedEvent> {    serialization_failure_hook_events(handlers, turn_id, error_message)        .into_iter()        .map(|event| hook_completed_for_tool_use(event, tool_use_id))        .collect()}pub(crate) fn hook_completed_for_tool_use(    mut event: HookCompletedEvent,    tool_use_id: &str,) -> HookCompletedEvent {    event.run = hook_run_for_tool_use(event.run, tool_use_id);    event}pub(crate) fn hook_run_for_tool_use(mut run: HookRunSummary, tool_use_id: &str) -> HookRunSummary {    run.id = format!("{}:{tool_use_id}", run.id);    run}pub(crate) fn matcher_pattern_for_event(    event_name: HookEventName,    matcher: Option<&str>,) -> Option<&str> {    match event_name {        HookEventName::PreToolUse        | HookEventName::PermissionRequest        | HookEventName::PostToolUse        | HookEventName::SessionStart        | HookEventName::SubagentStart        | HookEventName::SubagentStop        | HookEventName::PreCompact        | HookEventName::PostCompact => matcher,        HookEventName::UserPromptSubmit | HookEventName::Stop => None,    }}pub(crate) fn validate_matcher_pattern(matcher: &str) -> Result<(), regex::Error> {    if is_match_all_matcher(matcher) || is_exact_matcher(matcher) {        return Ok(());    }    regex::Regex::new(matcher).map(|_| ())}pub(crate) fn matches_matcher(matcher: Option<&str>, input: Option<&str>) -> bool {    match matcher {        None => true,        Some(matcher) if is_match_all_matcher(matcher) => true,        Some(matcher) if is_exact_matcher(matcher) => input            .map(|input| matcher.split('|').any(|candidate| candidate == input))            .unwrap_or(false),        Some(matcher) => input            .and_then(|input| {                regex::Regex::new(matcher)                    .ok()                    .map(|regex| regex.is_match(input))            })            .unwrap_or(false),    }}pub(crate) fn matcher_inputs<'a>(    tool_name: &'a str,    matcher_aliases: &'a [String],) -> Vec<&'a str> {    // Keep the canonical name first so matcher previews and execution preserve    // the same primary identity that hook stdin will serialize.    std::iter::once(tool_name)        .chain(matcher_aliases.iter().map(String::as_str))        .collect()}fn is_match_all_matcher(matcher: &str) -> bool {    matcher.is_empty() || matcher == "*"}fn is_exact_matcher(matcher: &str) -> bool {    matcher        .chars()        .all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '|')}#[cfg(test)]mod tests {    use codex_protocol::protocol::HookEventName;    use pretty_assertions::assert_eq;    use super::matcher_pattern_for_event;    use super::matches_matcher;    use super::validate_matcher_pattern;    #[test]    fn matcher_omitted_matches_all_occurrences() {        assert!(matches_matcher(/*matcher*/ None, Some("Bash")));        assert!(matches_matcher(/*matcher*/ None, Some("Write")));    }    #[test]    fn matcher_star_matches_all_occurrences() {        assert!(matches_matcher(Some("*"), Some("Bash")));        assert!(matches_matcher(Some("*"), Some("Edit")));        assert_eq!(validate_matcher_pattern("*"), Ok(()));    }    #[test]    fn matcher_empty_string_matches_all_occurrences() {        assert!(matches_matcher(Some(""), Some("Bash")));        assert!(matches_matcher(Some(""), Some("SessionStart")));        assert_eq!(validate_matcher_pattern(""), Ok(()));    }    #[test]    fn exact_matcher_supports_pipe_alternatives() {        assert!(matches_matcher(Some("Edit|Write"), Some("Edit")));        assert!(matches_matcher(Some("Edit|Write"), Some("Write")));        assert!(!matches_matcher(Some("Edit|Write"), Some("Bash")));        assert_eq!(validate_matcher_pattern("Edit|Write"), Ok(()));    }    #[test]    fn literal_matcher_uses_exact_matching() {        assert!(matches_matcher(Some("Bash"), Some("Bash")));        assert!(!matches_matcher(Some("Bash"), Some("BashOutput")));        assert!(matches_matcher(            Some("mcp__memory__create_entities"),            Some("mcp__memory__create_entities")        ));        assert!(!matches_matcher(            Some("mcp__memory"),            Some("mcp__memory__create_entities")        ));        assert_eq!(validate_matcher_pattern("mcp__memory"), Ok(()));    }    #[test]    fn matcher_uses_regex_when_it_contains_regex_characters() {        assert!(matches_matcher(Some("^Bash"), Some("BashOutput")));        assert_eq!(validate_matcher_pattern("^Bash"), Ok(()));    }    #[test]    fn mcp_matchers_support_regex_wildcards() {        assert!(matches_matcher(            Some("mcp__memory__.*"),            Some("mcp__memory__create_entities")        ));        assert!(matches_matcher(            Some("mcp__.*__write.*"),            Some("mcp__filesystem__write_file")        ));        assert!(!matches_matcher(            Some("mcp__.*__write.*"),            Some("mcp__filesystem__read_file")        ));        assert_eq!(validate_matcher_pattern("mcp__memory__.*"), Ok(()));    }    #[test]    fn matcher_supports_anchored_regexes() {        assert!(matches_matcher(Some("^Bash$"), Some("Bash")));        assert!(!matches_matcher(Some("^Bash$"), Some("BashOutput")));        assert_eq!(validate_matcher_pattern("^Bash$"), Ok(()));    }    #[test]    fn invalid_regex_is_rejected() {        assert!(validate_matcher_pattern("[").is_err());        assert!(!matches_matcher(Some("["), Some("Bash")));    }    #[test]    fn unsupported_events_ignore_matchers() {        assert_eq!(            matcher_pattern_for_event(HookEventName::UserPromptSubmit, Some("^hello")),            None        );        assert_eq!(            matcher_pattern_for_event(HookEventName::Stop, Some("^done$")),            None        );    }    #[test]    fn supported_events_keep_matchers() {        assert_eq!(            matcher_pattern_for_event(HookEventName::PreToolUse, Some("Bash")),            Some("Bash")        );        assert_eq!(            matcher_pattern_for_event(HookEventName::PostToolUse, Some("Edit|Write")),            Some("Edit|Write")        );        assert_eq!(            matcher_pattern_for_event(HookEventName::SessionStart, Some("startup|resume")),            Some("startup|resume")        );        assert_eq!(            matcher_pattern_for_event(HookEventName::PreCompact, Some("^auto$")),            Some("^auto$")        );        assert_eq!(            matcher_pattern_for_event(HookEventName::PostCompact, Some("manual|auto")),            Some("manual|auto")        );    }}