mirror of
https://github.com/openai/codex.git
synced 2026-10-26 18:38:47 +00:00
fix: run project pre-tool hooks after trusted hooks
This commit is contained in:
@@ -94,6 +94,7 @@ pub(crate) fn discover_handlers(config_layer_stack: Option<&ConfigLayerStack>) -
|
||||
&mut warnings,
|
||||
&mut display_order,
|
||||
source_path.as_path(),
|
||||
layer.is_project_layer(),
|
||||
event_name,
|
||||
groups,
|
||||
);
|
||||
@@ -107,8 +108,7 @@ fn append_group_handlers(
|
||||
handlers: &mut Vec<ConfiguredHandler>,
|
||||
warnings: &mut Vec<String>,
|
||||
display_order: &mut i64,
|
||||
source_path: &Path,
|
||||
event_name: codex_protocol::protocol::HookEventName,
|
||||
context: AppendGroupContext<'_>,
|
||||
matcher: Option<&str>,
|
||||
group_handlers: Vec<HookHandlerConfig>,
|
||||
) {
|
||||
@@ -117,7 +117,7 @@ fn append_group_handlers(
|
||||
{
|
||||
warnings.push(format!(
|
||||
"invalid matcher {matcher:?} in {}: {err}",
|
||||
source_path.display()
|
||||
context.source_path.display()
|
||||
));
|
||||
return;
|
||||
}
|
||||
@@ -133,46 +133,55 @@ fn append_group_handlers(
|
||||
if r#async {
|
||||
warnings.push(format!(
|
||||
"skipping async hook in {}: async hooks are not supported yet",
|
||||
source_path.display()
|
||||
context.source_path.display()
|
||||
));
|
||||
continue;
|
||||
}
|
||||
if command.trim().is_empty() {
|
||||
warnings.push(format!(
|
||||
"skipping empty hook command in {}",
|
||||
source_path.display()
|
||||
context.source_path.display()
|
||||
));
|
||||
continue;
|
||||
}
|
||||
let timeout_sec = timeout_sec.unwrap_or(600).max(1);
|
||||
handlers.push(ConfiguredHandler {
|
||||
event_name,
|
||||
event_name: context.event_name,
|
||||
matcher: matcher.map(ToOwned::to_owned),
|
||||
command,
|
||||
timeout_sec,
|
||||
status_message,
|
||||
source_path: source_path.to_path_buf(),
|
||||
source_path: context.source_path.to_path_buf(),
|
||||
is_project: context.is_project,
|
||||
display_order: *display_order,
|
||||
});
|
||||
*display_order += 1;
|
||||
}
|
||||
HookHandlerConfig::Prompt {} => warnings.push(format!(
|
||||
"skipping prompt hook in {}: prompt hooks are not supported yet",
|
||||
source_path.display()
|
||||
context.source_path.display()
|
||||
)),
|
||||
HookHandlerConfig::Agent {} => warnings.push(format!(
|
||||
"skipping agent hook in {}: agent hooks are not supported yet",
|
||||
source_path.display()
|
||||
context.source_path.display()
|
||||
)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy)]
|
||||
struct AppendGroupContext<'a> {
|
||||
source_path: &'a Path,
|
||||
is_project: bool,
|
||||
event_name: codex_protocol::protocol::HookEventName,
|
||||
}
|
||||
|
||||
fn append_matcher_groups(
|
||||
handlers: &mut Vec<ConfiguredHandler>,
|
||||
warnings: &mut Vec<String>,
|
||||
display_order: &mut i64,
|
||||
source_path: &Path,
|
||||
is_project: bool,
|
||||
event_name: codex_protocol::protocol::HookEventName,
|
||||
groups: Vec<MatcherGroup>,
|
||||
) {
|
||||
@@ -181,8 +190,11 @@ fn append_matcher_groups(
|
||||
handlers,
|
||||
warnings,
|
||||
display_order,
|
||||
source_path,
|
||||
event_name,
|
||||
AppendGroupContext {
|
||||
source_path,
|
||||
is_project,
|
||||
event_name,
|
||||
},
|
||||
matcher_pattern_for_event(event_name, group.matcher.as_deref()),
|
||||
group.hooks,
|
||||
);
|
||||
@@ -197,6 +209,7 @@ mod tests {
|
||||
use codex_protocol::protocol::HookEventName;
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::AppendGroupContext;
|
||||
use super::ConfiguredHandler;
|
||||
use super::HookHandlerConfig;
|
||||
use super::append_group_handlers;
|
||||
@@ -212,8 +225,11 @@ mod tests {
|
||||
&mut handlers,
|
||||
&mut warnings,
|
||||
&mut display_order,
|
||||
Path::new("/tmp/hooks.json"),
|
||||
HookEventName::UserPromptSubmit,
|
||||
AppendGroupContext {
|
||||
source_path: Path::new("/tmp/hooks.json"),
|
||||
is_project: false,
|
||||
event_name: HookEventName::UserPromptSubmit,
|
||||
},
|
||||
matcher_pattern_for_event(HookEventName::UserPromptSubmit, Some("[")),
|
||||
vec![HookHandlerConfig::Command {
|
||||
command: "echo hello".to_string(),
|
||||
@@ -233,6 +249,7 @@ mod tests {
|
||||
timeout_sec: 600,
|
||||
status_message: None,
|
||||
source_path: PathBuf::from("/tmp/hooks.json"),
|
||||
is_project: false,
|
||||
display_order: 0,
|
||||
}]
|
||||
);
|
||||
@@ -248,8 +265,11 @@ mod tests {
|
||||
&mut handlers,
|
||||
&mut warnings,
|
||||
&mut display_order,
|
||||
Path::new("/tmp/hooks.json"),
|
||||
HookEventName::PreToolUse,
|
||||
AppendGroupContext {
|
||||
source_path: Path::new("/tmp/hooks.json"),
|
||||
is_project: false,
|
||||
event_name: HookEventName::PreToolUse,
|
||||
},
|
||||
matcher_pattern_for_event(HookEventName::PreToolUse, Some("^Bash$")),
|
||||
vec![HookHandlerConfig::Command {
|
||||
command: "echo hello".to_string(),
|
||||
@@ -269,6 +289,7 @@ mod tests {
|
||||
timeout_sec: 600,
|
||||
status_message: None,
|
||||
source_path: PathBuf::from("/tmp/hooks.json"),
|
||||
is_project: false,
|
||||
display_order: 0,
|
||||
}]
|
||||
);
|
||||
@@ -284,8 +305,11 @@ mod tests {
|
||||
&mut handlers,
|
||||
&mut warnings,
|
||||
&mut display_order,
|
||||
Path::new("/tmp/hooks.json"),
|
||||
HookEventName::PreToolUse,
|
||||
AppendGroupContext {
|
||||
source_path: Path::new("/tmp/hooks.json"),
|
||||
is_project: false,
|
||||
event_name: HookEventName::PreToolUse,
|
||||
},
|
||||
matcher_pattern_for_event(HookEventName::PreToolUse, Some("*")),
|
||||
vec![HookHandlerConfig::Command {
|
||||
command: "echo hello".to_string(),
|
||||
@@ -310,8 +334,11 @@ mod tests {
|
||||
&mut handlers,
|
||||
&mut warnings,
|
||||
&mut display_order,
|
||||
Path::new("/tmp/hooks.json"),
|
||||
HookEventName::PostToolUse,
|
||||
AppendGroupContext {
|
||||
source_path: Path::new("/tmp/hooks.json"),
|
||||
is_project: false,
|
||||
event_name: HookEventName::PostToolUse,
|
||||
},
|
||||
matcher_pattern_for_event(HookEventName::PostToolUse, Some("Edit|Write")),
|
||||
vec![HookHandlerConfig::Command {
|
||||
command: "echo hello".to_string(),
|
||||
|
||||
@@ -82,6 +82,47 @@ pub(crate) async fn execute_handlers<T>(
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn select_handlers_by_trust_precedence(
|
||||
handlers: &[ConfiguredHandler],
|
||||
event_name: HookEventName,
|
||||
matcher_input: Option<&str>,
|
||||
) -> Vec<Vec<ConfiguredHandler>> {
|
||||
let mut non_project = Vec::new();
|
||||
let mut project = Vec::new();
|
||||
for handler in select_handlers(handlers, event_name, matcher_input) {
|
||||
if handler.is_project {
|
||||
project.push(handler);
|
||||
} else {
|
||||
non_project.push(handler);
|
||||
}
|
||||
}
|
||||
|
||||
let mut tiers = Vec::new();
|
||||
if !non_project.is_empty() {
|
||||
tiers.push(non_project);
|
||||
}
|
||||
if !project.is_empty() {
|
||||
tiers.push(project);
|
||||
}
|
||||
tiers
|
||||
}
|
||||
|
||||
pub(crate) fn skipped_completed_event(
|
||||
handler: &ConfiguredHandler,
|
||||
turn_id: Option<String>,
|
||||
message: String,
|
||||
) -> HookCompletedEvent {
|
||||
let mut run = running_summary(handler);
|
||||
run.status = HookRunStatus::Failed;
|
||||
run.completed_at = Some(run.started_at);
|
||||
run.duration_ms = Some(0);
|
||||
run.entries = vec![codex_protocol::protocol::HookOutputEntry {
|
||||
kind: codex_protocol::protocol::HookOutputEntryKind::Error,
|
||||
text: message,
|
||||
}];
|
||||
HookCompletedEvent { turn_id, run }
|
||||
}
|
||||
|
||||
pub(crate) fn completed_summary(
|
||||
handler: &ConfiguredHandler,
|
||||
run_result: &CommandRunResult,
|
||||
@@ -123,6 +164,7 @@ mod tests {
|
||||
|
||||
use super::ConfiguredHandler;
|
||||
use super::select_handlers;
|
||||
use super::select_handlers_by_trust_precedence;
|
||||
|
||||
fn make_handler(
|
||||
event_name: HookEventName,
|
||||
@@ -137,6 +179,26 @@ mod tests {
|
||||
timeout_sec: 5,
|
||||
status_message: None,
|
||||
source_path: PathBuf::from("/tmp/hooks.json"),
|
||||
is_project: false,
|
||||
display_order,
|
||||
}
|
||||
}
|
||||
|
||||
fn make_handler_with_source(
|
||||
event_name: HookEventName,
|
||||
matcher: Option<&str>,
|
||||
command: &str,
|
||||
source_path: &str,
|
||||
display_order: i64,
|
||||
) -> ConfiguredHandler {
|
||||
ConfiguredHandler {
|
||||
event_name,
|
||||
matcher: matcher.map(str::to_owned),
|
||||
command: command.to_string(),
|
||||
timeout_sec: 5,
|
||||
status_message: None,
|
||||
source_path: PathBuf::from(source_path),
|
||||
is_project: source_path.contains("/project/"),
|
||||
display_order,
|
||||
}
|
||||
}
|
||||
@@ -334,4 +396,53 @@ mod tests {
|
||||
assert_eq!(selected[1].command, "second");
|
||||
assert_eq!(selected[2].command, "third");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn select_handlers_by_trust_precedence_runs_non_project_before_project() {
|
||||
let handlers = vec![
|
||||
make_handler_with_source(
|
||||
HookEventName::PreToolUse,
|
||||
Some("^Bash$"),
|
||||
"echo system",
|
||||
"/etc/codex/hooks.json",
|
||||
0,
|
||||
),
|
||||
make_handler_with_source(
|
||||
HookEventName::PreToolUse,
|
||||
Some("^Bash$"),
|
||||
"echo user one",
|
||||
"/tmp/home/.codex/hooks.json",
|
||||
1,
|
||||
),
|
||||
make_handler_with_source(
|
||||
HookEventName::PreToolUse,
|
||||
Some("^Bash$"),
|
||||
"echo user two",
|
||||
"/tmp/home/.codex/hooks.json",
|
||||
2,
|
||||
),
|
||||
make_handler_with_source(
|
||||
HookEventName::PreToolUse,
|
||||
Some("^Bash$"),
|
||||
"echo project",
|
||||
"/tmp/project/.codex/hooks.json",
|
||||
3,
|
||||
),
|
||||
];
|
||||
|
||||
let tiers =
|
||||
select_handlers_by_trust_precedence(&handlers, HookEventName::PreToolUse, Some("Bash"));
|
||||
|
||||
assert_eq!(
|
||||
tiers
|
||||
.iter()
|
||||
.map(|tier| {
|
||||
tier.iter()
|
||||
.map(|handler| handler.display_order)
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>(),
|
||||
vec![vec![0, 1, 2], vec![3]],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ pub(crate) struct ConfiguredHandler {
|
||||
pub timeout_sec: u64,
|
||||
pub status_message: Option<String>,
|
||||
pub source_path: PathBuf,
|
||||
pub is_project: bool,
|
||||
pub display_order: i64,
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user