mirror of
https://github.com/openai/codex.git
synced 2026-10-17 17:08:55 +00:00
Expose execution mode in hook listings (#37538)
## What changed - Add `executionMode` to `HookMetadata` returned by `hooks/list`, with `sync` as the default for compatibility. - Propagate each discovered hook's `sync` or `async` mode through the app-server protocol and generated schemas. - Show the execution mode in the TUI hooks browser. ## Testing - Cover async and default-sync modes in `hooks/list` tests and TUI snapshots. GitOrigin-RevId: 9c9f2890798fe417face76c15847616b2dea9db4
This commit is contained in:
+8
@@ -11794,6 +11794,14 @@
|
||||
"eventName": {
|
||||
"$ref": "#/definitions/v2/HookEventName"
|
||||
},
|
||||
"executionMode": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/v2/HookExecutionMode"
|
||||
}
|
||||
],
|
||||
"default": "sync"
|
||||
},
|
||||
"handlerType": {
|
||||
"$ref": "#/definitions/v2/HookHandlerType"
|
||||
},
|
||||
|
||||
+8
@@ -8070,6 +8070,14 @@
|
||||
"eventName": {
|
||||
"$ref": "#/definitions/HookEventName"
|
||||
},
|
||||
"executionMode": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/HookExecutionMode"
|
||||
}
|
||||
],
|
||||
"default": "sync"
|
||||
},
|
||||
"handlerType": {
|
||||
"$ref": "#/definitions/HookHandlerType"
|
||||
},
|
||||
|
||||
@@ -36,6 +36,13 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"HookExecutionMode": {
|
||||
"enum": [
|
||||
"sync",
|
||||
"async"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"HookHandlerType": {
|
||||
"enum": [
|
||||
"command",
|
||||
@@ -74,6 +81,14 @@
|
||||
"eventName": {
|
||||
"$ref": "#/definitions/HookEventName"
|
||||
},
|
||||
"executionMode": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/HookExecutionMode"
|
||||
}
|
||||
],
|
||||
"default": "sync"
|
||||
},
|
||||
"handlerType": {
|
||||
"$ref": "#/definitions/HookHandlerType"
|
||||
},
|
||||
|
||||
BIN
Binary file not shown.
BIN
Binary file not shown.
@@ -3,11 +3,12 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { AbsolutePathBuf } from "../AbsolutePathBuf";
|
||||
import type { HookEventName } from "./HookEventName";
|
||||
import type { HookExecutionMode } from "./HookExecutionMode";
|
||||
import type { HookHandlerType } from "./HookHandlerType";
|
||||
import type { HookSource } from "./HookSource";
|
||||
import type { HookTrustStatus } from "./HookTrustStatus";
|
||||
|
||||
export type HookMetadata = { key: string, eventName: HookEventName, handlerType: HookHandlerType, matcher: string | null, command: string | null, timeoutSec: bigint, statusMessage: string | null,
|
||||
export type HookMetadata = { key: string, eventName: HookEventName, handlerType: HookHandlerType, executionMode: HookExecutionMode, matcher: string | null, command: string | null, timeoutSec: bigint, statusMessage: string | null,
|
||||
/**
|
||||
* Configured `additionalContext` spill threshold.
|
||||
* `null` uses 2,500 tokens; `0` disables spilling.
|
||||
|
||||
@@ -28,8 +28,11 @@ v2_enum_from_core!(
|
||||
);
|
||||
|
||||
v2_enum_from_core!(
|
||||
#[derive(Default)]
|
||||
pub enum HookExecutionMode from CoreHookExecutionMode {
|
||||
Sync, Async
|
||||
#[default]
|
||||
Sync,
|
||||
Async
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use super::AppSummary;
|
||||
use super::HookEventName;
|
||||
use super::HookExecutionMode;
|
||||
use super::HookHandlerType;
|
||||
use super::HookSource;
|
||||
use super::HookTrustStatus;
|
||||
@@ -524,6 +525,8 @@ pub struct HookMetadata {
|
||||
pub key: String,
|
||||
pub event_name: HookEventName,
|
||||
pub handler_type: HookHandlerType,
|
||||
#[serde(default)]
|
||||
pub execution_mode: HookExecutionMode,
|
||||
pub matcher: Option<String>,
|
||||
pub command: Option<String>,
|
||||
pub timeout_sec: u64,
|
||||
|
||||
@@ -1926,6 +1926,8 @@ For linked Git worktrees, project hook declarations come from the matching `.cod
|
||||
|
||||
Hooks are returned even when disabled so clients can render and re-enable them. User-controlled state lives under `hooks.state`. Managed hooks are non-configurable, and user entries for managed hook keys are ignored during loading.
|
||||
|
||||
`executionMode` reports how a command hook runs. `sync` hooks participate in the current operation, while `async` hooks run in the background and deliver informational output through the existing steer-based injection path. Output is injected immediately into an active turn or persisted without starting a new turn when the session is idle.
|
||||
|
||||
For unmanaged hooks, `currentHash` and `trustStatus` describe whether the current definition is first-seen, approved, or changed since approval. Only trusted unmanaged hooks become runnable. Hook keys combine the source identity with a trailing event/group/handler selector that is currently positional.
|
||||
|
||||
```json
|
||||
@@ -1948,6 +1950,7 @@ For unmanaged hooks, `currentHash` and `trustStatus` describe whether the curren
|
||||
"key": "/Users/me/.codex/config.toml:pre_tool_use:0:0",
|
||||
"eventName": "pre_tool_use",
|
||||
"handlerType": "command",
|
||||
"executionMode": "sync",
|
||||
"isManaged": false,
|
||||
"matcher": "Bash",
|
||||
"command": "python3 /Users/me/hook.py",
|
||||
|
||||
@@ -70,6 +70,7 @@ fn hooks_to_info(hooks: &[codex_hooks::HookListEntry]) -> Vec<HookMetadata> {
|
||||
key: hook.key.clone(),
|
||||
event_name: hook.event_name.into(),
|
||||
handler_type: hook.handler_type.into(),
|
||||
execution_mode: hook.execution_mode.into(),
|
||||
matcher: hook.matcher.clone(),
|
||||
command: hook.command.clone(),
|
||||
timeout_sec: hook.timeout_sec,
|
||||
|
||||
@@ -8,6 +8,7 @@ use app_test_support::create_mock_responses_server_sequence_unchecked;
|
||||
use codex_app_server_protocol::ConfigBatchWriteParams;
|
||||
use codex_app_server_protocol::ConfigEdit;
|
||||
use codex_app_server_protocol::HookEventName;
|
||||
use codex_app_server_protocol::HookExecutionMode;
|
||||
use codex_app_server_protocol::HookHandlerType;
|
||||
use codex_app_server_protocol::HookMetadata;
|
||||
use codex_app_server_protocol::HookSource;
|
||||
@@ -45,6 +46,7 @@ fn command_hook_hash(
|
||||
matcher: Option<&str>,
|
||||
command: &str,
|
||||
timeout_sec: u64,
|
||||
execution_mode: HookExecutionMode,
|
||||
status_message: Option<&str>,
|
||||
additional_context_limit: Option<usize>,
|
||||
) -> String {
|
||||
@@ -56,7 +58,7 @@ fn command_hook_hash(
|
||||
command: command.to_string(),
|
||||
command_windows: None,
|
||||
timeout_sec: Some(timeout_sec),
|
||||
r#async: false,
|
||||
r#async: execution_mode == HookExecutionMode::Async,
|
||||
status_message: status_message.map(ToOwned::to_owned),
|
||||
additional_context_limit,
|
||||
}],
|
||||
@@ -80,6 +82,7 @@ matcher = "Bash"
|
||||
type = "command"
|
||||
command = "python3 /tmp/listed-hook.py"
|
||||
timeout = 5
|
||||
async = true
|
||||
statusMessage = "running listed hook"
|
||||
additionalContextLimit = 4096
|
||||
"#,
|
||||
@@ -161,6 +164,7 @@ async fn hooks_list_shows_discovered_hook() -> Result<()> {
|
||||
key: format!("{}:pre_tool_use:0:0", config_path.as_path().display()),
|
||||
event_name: HookEventName::PreToolUse,
|
||||
handler_type: HookHandlerType::Command,
|
||||
execution_mode: HookExecutionMode::Async,
|
||||
matcher: Some("Bash".to_string()),
|
||||
command: Some("python3 /tmp/listed-hook.py".to_string()),
|
||||
timeout_sec: 5,
|
||||
@@ -177,6 +181,7 @@ async fn hooks_list_shows_discovered_hook() -> Result<()> {
|
||||
Some("Bash"),
|
||||
"python3 /tmp/listed-hook.py",
|
||||
/*timeout_sec*/ 5,
|
||||
HookExecutionMode::Async,
|
||||
Some("running listed hook"),
|
||||
/*additional_context_limit*/ Some(4_096),
|
||||
),
|
||||
@@ -240,6 +245,7 @@ async fn hooks_list_shows_discovered_plugin_hook() -> Result<()> {
|
||||
key: "demo@test:hooks/hooks.json:pre_tool_use:0:0".to_string(),
|
||||
event_name: HookEventName::PreToolUse,
|
||||
handler_type: HookHandlerType::Command,
|
||||
execution_mode: HookExecutionMode::Sync,
|
||||
matcher: Some("Bash".to_string()),
|
||||
command: Some("echo plugin hook".to_string()),
|
||||
timeout_sec: 7,
|
||||
@@ -256,6 +262,7 @@ async fn hooks_list_shows_discovered_plugin_hook() -> Result<()> {
|
||||
Some("Bash"),
|
||||
"echo plugin hook",
|
||||
/*timeout_sec*/ 7,
|
||||
HookExecutionMode::Sync,
|
||||
Some("running plugin hook"),
|
||||
/*additional_context_limit*/ None,
|
||||
),
|
||||
@@ -436,6 +443,7 @@ timeout = 5
|
||||
),
|
||||
event_name: HookEventName::PreToolUse,
|
||||
handler_type: HookHandlerType::Command,
|
||||
execution_mode: HookExecutionMode::Sync,
|
||||
matcher: Some("Bash".to_string()),
|
||||
command: Some("echo project hook".to_string()),
|
||||
timeout_sec: 5,
|
||||
@@ -452,6 +460,7 @@ timeout = 5
|
||||
Some("Bash"),
|
||||
"echo project hook",
|
||||
/*timeout_sec*/ 5,
|
||||
HookExecutionMode::Sync,
|
||||
/*status_message*/ None,
|
||||
/*additional_context_limit*/ None,
|
||||
),
|
||||
|
||||
@@ -558,6 +558,7 @@ fn append_matcher_groups(
|
||||
is_managed: source.is_managed,
|
||||
current_hash,
|
||||
trust_status,
|
||||
execution_mode,
|
||||
});
|
||||
if enabled
|
||||
&& (source.bypass_hook_trust
|
||||
|
||||
@@ -108,6 +108,7 @@ pub struct HookListEntry {
|
||||
pub is_managed: bool,
|
||||
pub current_hash: String,
|
||||
pub trust_status: HookTrustStatus,
|
||||
pub execution_mode: codex_protocol::protocol::HookExecutionMode,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use codex_app_server_protocol::HookEventName;
|
||||
use codex_app_server_protocol::HookExecutionMode;
|
||||
use codex_app_server_protocol::HookMetadata;
|
||||
use codex_app_server_protocol::HookSource;
|
||||
use codex_app_server_protocol::HookTrustStatus;
|
||||
@@ -492,6 +493,11 @@ impl HooksBrowserView {
|
||||
width,
|
||||
Some(MAX_COMMAND_DETAIL_LINES),
|
||||
));
|
||||
let execution_mode = match hook.execution_mode {
|
||||
HookExecutionMode::Sync => "Sync",
|
||||
HookExecutionMode::Async => "Async",
|
||||
};
|
||||
lines.push(detail_line("Mode", execution_mode));
|
||||
lines.push(detail_line("Timeout", &format!("{}s", hook.timeout_sec)));
|
||||
if let Some(limit) = hook.additional_context_limit {
|
||||
let value = if limit == 0 {
|
||||
@@ -883,6 +889,7 @@ mod tests {
|
||||
use crate::test_support::test_path_display;
|
||||
use codex_app_server_protocol::HookErrorInfo;
|
||||
use codex_app_server_protocol::HookEventName;
|
||||
use codex_app_server_protocol::HookExecutionMode;
|
||||
use codex_app_server_protocol::HookHandlerType;
|
||||
use codex_app_server_protocol::HookMetadata;
|
||||
use codex_app_server_protocol::HookSource;
|
||||
@@ -947,6 +954,7 @@ mod tests {
|
||||
key: key.to_string(),
|
||||
event_name,
|
||||
handler_type: HookHandlerType::Command,
|
||||
execution_mode: HookExecutionMode::Sync,
|
||||
is_managed,
|
||||
matcher: Some("Bash".to_string()),
|
||||
command: Some(command.to_string()),
|
||||
@@ -1142,6 +1150,7 @@ mod tests {
|
||||
/*display_order*/ 0,
|
||||
);
|
||||
untrusted_hook.trust_status = HookTrustStatus::Untrusted;
|
||||
untrusted_hook.execution_mode = HookExecutionMode::Async;
|
||||
let mut view = HooksBrowserView::new(
|
||||
vec![untrusted_hook],
|
||||
Vec::new(),
|
||||
|
||||
+1
@@ -12,6 +12,7 @@ expression: "render_lines(&view, 112)"
|
||||
Matcher Bash
|
||||
Source User config - /tmp/hooks.json
|
||||
Command /tmp/pre-tool-use.sh
|
||||
Mode Sync
|
||||
Timeout 30s
|
||||
Context unlimited
|
||||
Trust Trusted
|
||||
|
||||
+1
@@ -14,6 +14,7 @@ expression: "render_lines(&view, 44)"
|
||||
Command one two three four five six
|
||||
seven eight nine ten eleven
|
||||
twelve thirteen fourteen…
|
||||
Mode Sync
|
||||
Timeout 30s
|
||||
Trust Trusted
|
||||
|
||||
|
||||
+1
@@ -13,6 +13,7 @@ expression: "render_lines(&view, 112)"
|
||||
Matcher Bash
|
||||
Source Plugin - superpowers@openai-curated
|
||||
Command ${CODEX_PLUGIN_ROOT}/hooks/pre-tool-use-check.sh
|
||||
Mode Sync
|
||||
Timeout 30s
|
||||
Trust Trusted
|
||||
|
||||
|
||||
+1
@@ -12,6 +12,7 @@ expression: "render_lines(&view, 112)"
|
||||
Matcher Bash
|
||||
Source Admin config
|
||||
Command /enterprise/hooks/permission-check.sh
|
||||
Mode Sync
|
||||
Timeout 30s
|
||||
Trust Managed
|
||||
|
||||
|
||||
+1
@@ -12,6 +12,7 @@ expression: "render_lines(&view, 112)"
|
||||
Matcher Bash
|
||||
Source User config - /tmp/hooks.json
|
||||
Command /tmp/pre-tool-use-check.sh
|
||||
Mode Sync
|
||||
Timeout 30s
|
||||
Trust New hook - review required
|
||||
|
||||
|
||||
+1
@@ -19,6 +19,7 @@ expression: "render_lines(&view, 112)"
|
||||
Matcher Bash
|
||||
Source User config - /tmp/hooks.json
|
||||
Command /tmp/hook-8.sh
|
||||
Mode Sync
|
||||
Timeout 30s
|
||||
Trust Trusted
|
||||
|
||||
|
||||
+1
@@ -13,6 +13,7 @@ expression: "render_lines(&view, 112)"
|
||||
Matcher Bash
|
||||
Source Admin config
|
||||
Command /enterprise/hooks/pre-tool-use-2.sh
|
||||
Mode Sync
|
||||
Timeout 30s
|
||||
Trust Managed
|
||||
|
||||
|
||||
+1
@@ -12,6 +12,7 @@ expression: "render_lines(&view, 112)"
|
||||
Matcher Bash
|
||||
Source User config - /tmp/hooks.json
|
||||
Command ~/bin/untrusted.sh
|
||||
Mode Async
|
||||
Timeout 30s
|
||||
Trust New hook - review required
|
||||
|
||||
|
||||
@@ -305,6 +305,7 @@ mod tests {
|
||||
use crate::test_support::PathBufExt;
|
||||
use crate::test_support::test_path_buf;
|
||||
use codex_app_server_protocol::HookEventName;
|
||||
use codex_app_server_protocol::HookExecutionMode;
|
||||
use codex_app_server_protocol::HookHandlerType;
|
||||
use codex_app_server_protocol::HookMetadata;
|
||||
use codex_app_server_protocol::HookSource;
|
||||
@@ -320,6 +321,7 @@ mod tests {
|
||||
key: key.to_string(),
|
||||
event_name: HookEventName::PreToolUse,
|
||||
handler_type: HookHandlerType::Command,
|
||||
execution_mode: HookExecutionMode::Sync,
|
||||
is_managed: false,
|
||||
matcher: Some("Bash".to_string()),
|
||||
command: Some("/tmp/hook.sh".to_string()),
|
||||
|
||||
Reference in New Issue
Block a user