Add persisted hook enablement state (#19840)

## Why

After `hooks/list` exposes the hook inventory, clients need a way to
persist user hook preferences, make those changes effective in
already-open sessions, and distinguish user-controllable hooks from
managed requirements without adding another bespoke app-server write
API.

## What

- Extends `hooks/list` entries with effective `enabled` state.
- Persists user-level hook state under `hooks.state.<hook-id>` so the
model can grow beyond a single boolean over time.
- Uses the existing `config/batchWrite` path for hook state updates
instead of introducing a dedicated hook write RPC.
- Refreshes live session hook engines after config writes so
already-open threads observe updated enablement without a restart.

## Stack

1. openai/codex#19705
2. openai/codex#19778
3. This PR - openai/codex#19840
4. openai/codex#19882

## Reviewer Notes

The generated schema files account for much of the raw diff. The core
behavior is in:

- `hooks/src/config_rules.rs`, which resolves per-hook user state from
the config layer stack.
- `hooks/src/engine/discovery.rs`, which projects effective enablement
into `hooks/list` from source-derived managedness.
- `config/src/hook_config.rs`, which defines the new `hooks.state`
representation.
- `core/src/session/mod.rs`, which rebuilds live hook state after user
config reloads.

---------

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Abhinav
2026-04-30 04:46:32 +00:00
committed by GitHub
co-authored by Codex
parent ac4332c05b
commit 8f3c06cc97
39 changed files with 1212 additions and 181 deletions
@@ -1901,6 +1901,7 @@
"mdm",
"sessionFlags",
"plugin",
"cloudRequirements",
"legacyManagedConfigFile",
"legacyManagedConfigMdm",
"unknown"
@@ -9698,12 +9698,21 @@
"format": "int64",
"type": "integer"
},
"enabled": {
"type": "boolean"
},
"eventName": {
"$ref": "#/definitions/v2/HookEventName"
},
"handlerType": {
"$ref": "#/definitions/v2/HookHandlerType"
},
"isManaged": {
"type": "boolean"
},
"key": {
"type": "string"
},
"matcher": {
"type": [
"string",
@@ -9736,8 +9745,11 @@
},
"required": [
"displayOrder",
"enabled",
"eventName",
"handlerType",
"isManaged",
"key",
"source",
"sourcePath",
"timeoutSec"
@@ -9900,6 +9912,7 @@
"mdm",
"sessionFlags",
"plugin",
"cloudRequirements",
"legacyManagedConfigFile",
"legacyManagedConfigMdm",
"unknown"
@@ -6308,12 +6308,21 @@
"format": "int64",
"type": "integer"
},
"enabled": {
"type": "boolean"
},
"eventName": {
"$ref": "#/definitions/HookEventName"
},
"handlerType": {
"$ref": "#/definitions/HookHandlerType"
},
"isManaged": {
"type": "boolean"
},
"key": {
"type": "string"
},
"matcher": {
"type": [
"string",
@@ -6346,8 +6355,11 @@
},
"required": [
"displayOrder",
"enabled",
"eventName",
"handlerType",
"isManaged",
"key",
"source",
"sourcePath",
"timeoutSec"
@@ -6510,6 +6522,7 @@
"mdm",
"sessionFlags",
"plugin",
"cloudRequirements",
"legacyManagedConfigFile",
"legacyManagedConfigMdm",
"unknown"
@@ -161,6 +161,7 @@
"mdm",
"sessionFlags",
"plugin",
"cloudRequirements",
"legacyManagedConfigFile",
"legacyManagedConfigMdm",
"unknown"
@@ -161,6 +161,7 @@
"mdm",
"sessionFlags",
"plugin",
"cloudRequirements",
"legacyManagedConfigFile",
"legacyManagedConfigMdm",
"unknown"
@@ -51,12 +51,21 @@
"format": "int64",
"type": "integer"
},
"enabled": {
"type": "boolean"
},
"eventName": {
"$ref": "#/definitions/HookEventName"
},
"handlerType": {
"$ref": "#/definitions/HookHandlerType"
},
"isManaged": {
"type": "boolean"
},
"key": {
"type": "string"
},
"matcher": {
"type": [
"string",
@@ -89,8 +98,11 @@
},
"required": [
"displayOrder",
"enabled",
"eventName",
"handlerType",
"isManaged",
"key",
"source",
"sourcePath",
"timeoutSec"
@@ -105,6 +117,7 @@
"mdm",
"sessionFlags",
"plugin",
"cloudRequirements",
"legacyManagedConfigFile",
"legacyManagedConfigMdm",
"unknown"
@@ -6,4 +6,4 @@ import type { HookEventName } from "./HookEventName";
import type { HookHandlerType } from "./HookHandlerType";
import type { HookSource } from "./HookSource";
export type HookMetadata = { eventName: HookEventName, handlerType: HookHandlerType, matcher: string | null, command: string | null, timeoutSec: bigint, statusMessage: string | null, sourcePath: AbsolutePathBuf, source: HookSource, pluginId: string | null, displayOrder: bigint, };
export type HookMetadata = { key: string, eventName: HookEventName, handlerType: HookHandlerType, matcher: string | null, command: string | null, timeoutSec: bigint, statusMessage: string | null, sourcePath: AbsolutePathBuf, source: HookSource, pluginId: string | null, displayOrder: bigint, enabled: boolean, isManaged: boolean, };
@@ -2,4 +2,4 @@
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type HookSource = "system" | "user" | "project" | "mdm" | "sessionFlags" | "plugin" | "legacyManagedConfigFile" | "legacyManagedConfigMdm" | "unknown";
export type HookSource = "system" | "user" | "project" | "mdm" | "sessionFlags" | "plugin" | "cloudRequirements" | "legacyManagedConfigFile" | "legacyManagedConfigMdm" | "unknown";
@@ -472,6 +472,7 @@ v2_enum_from_core!(
Mdm,
SessionFlags,
Plugin,
CloudRequirements,
LegacyManagedConfigFile,
LegacyManagedConfigMdm,
Unknown,
@@ -4716,6 +4717,7 @@ pub struct HooksListEntry {
#[serde(rename_all = "camelCase")]
#[ts(export_to = "v2/")]
pub struct HookMetadata {
pub key: String,
pub event_name: HookEventName,
pub handler_type: HookHandlerType,
pub matcher: Option<String>,
@@ -4726,6 +4728,8 @@ pub struct HookMetadata {
pub source: HookSource,
pub plugin_id: Option<String>,
pub display_order: i64,
pub enabled: bool,
pub is_managed: bool,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]