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
+30
View File
@@ -1571,12 +1571,28 @@ pub enum HookSource {
Mdm,
SessionFlags,
Plugin,
CloudRequirements,
LegacyManagedConfigFile,
LegacyManagedConfigMdm,
#[default]
Unknown,
}
impl HookSource {
/// Returns whether hooks from this source are managed and therefore not
/// user-configurable.
pub fn is_managed(self) -> bool {
matches!(
self,
Self::System
| Self::Mdm
| Self::CloudRequirements
| Self::LegacyManagedConfigFile
| Self::LegacyManagedConfigMdm
)
}
}
#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, JsonSchema, TS)]
#[serde(rename_all = "snake_case")]
pub enum HookRunStatus {
@@ -3991,6 +4007,20 @@ mod tests {
use tempfile::NamedTempFile;
use tempfile::TempDir;
#[test]
fn hook_source_managedness_is_source_derived() {
assert_eq!(HookSource::System.is_managed(), true);
assert_eq!(HookSource::Mdm.is_managed(), true);
assert_eq!(HookSource::CloudRequirements.is_managed(), true);
assert_eq!(HookSource::LegacyManagedConfigFile.is_managed(), true);
assert_eq!(HookSource::LegacyManagedConfigMdm.is_managed(), true);
assert_eq!(HookSource::User.is_managed(), false);
assert_eq!(HookSource::Project.is_managed(), false);
assert_eq!(HookSource::SessionFlags.is_managed(), false);
assert_eq!(HookSource::Plugin.is_managed(), false);
assert_eq!(HookSource::Unknown.is_managed(), false);
}
fn sorted_writable_roots(roots: Vec<WritableRoot>) -> Vec<(PathBuf, Vec<PathBuf>)> {
let mut sorted_roots: Vec<(PathBuf, Vec<PathBuf>)> = roots
.into_iter()