hook trust metadata and enforcement (#20321)

# Why

We want shared hook trust that both the app and the TUI can build on,
but the metadata is only useful if runtime behavior agrees with it. This
PR adds a single backend trust model for hooks so unmanaged hooks cannot
run until the current definition has been reviewed, while managed hooks
remain runnable and non-configurable.

# What

- persist `trusted_hash` alongside hook state in `config.toml`
- expose `currentHash` and derived `trustStatus` through `hooks/list`
- derive trust from normalized hook definitions so equivalent hooks from
`config.toml` and `hooks.json` share the same trust identity
- gate unmanaged hooks on trust before they enter the runnable handler
set

# Reviewer Notes

- key file to review is `codex-rs/hooks/src/engine/discovery.rs`
- the only **core** change is schema related
This commit is contained in:
Abhinav
2026-05-05 19:13:55 +00:00
committed by GitHub
parent 78421face0
commit 0452dca986
44 changed files with 1659 additions and 383 deletions
@@ -9833,6 +9833,9 @@
"null"
]
},
"currentHash": {
"type": "string"
},
"displayOrder": {
"format": "int64",
"type": "integer"
@@ -9880,9 +9883,13 @@
"format": "uint64",
"minimum": 0.0,
"type": "integer"
},
"trustStatus": {
"$ref": "#/definitions/v2/HookTrustStatus"
}
},
"required": [
"currentHash",
"displayOrder",
"enabled",
"eventName",
@@ -9891,7 +9898,8 @@
"key",
"source",
"sourcePath",
"timeoutSec"
"timeoutSec",
"trustStatus"
],
"type": "object"
},
@@ -10081,6 +10089,15 @@
"title": "HookStartedNotification",
"type": "object"
},
"HookTrustStatus": {
"enum": [
"managed",
"untrusted",
"trusted",
"modified"
],
"type": "string"
},
"HooksListEntry": {
"properties": {
"cwd": {
@@ -6400,6 +6400,9 @@
"null"
]
},
"currentHash": {
"type": "string"
},
"displayOrder": {
"format": "int64",
"type": "integer"
@@ -6447,9 +6450,13 @@
"format": "uint64",
"minimum": 0.0,
"type": "integer"
},
"trustStatus": {
"$ref": "#/definitions/HookTrustStatus"
}
},
"required": [
"currentHash",
"displayOrder",
"enabled",
"eventName",
@@ -6458,7 +6465,8 @@
"key",
"source",
"sourcePath",
"timeoutSec"
"timeoutSec",
"trustStatus"
],
"type": "object"
},
@@ -6648,6 +6656,15 @@
"title": "HookStartedNotification",
"type": "object"
},
"HookTrustStatus": {
"enum": [
"managed",
"untrusted",
"trusted",
"modified"
],
"type": "string"
},
"HooksListEntry": {
"properties": {
"cwd": {
@@ -47,6 +47,9 @@
"null"
]
},
"currentHash": {
"type": "string"
},
"displayOrder": {
"format": "int64",
"type": "integer"
@@ -94,9 +97,13 @@
"format": "uint64",
"minimum": 0.0,
"type": "integer"
},
"trustStatus": {
"$ref": "#/definitions/HookTrustStatus"
}
},
"required": [
"currentHash",
"displayOrder",
"enabled",
"eventName",
@@ -105,7 +112,8 @@
"key",
"source",
"sourcePath",
"timeoutSec"
"timeoutSec",
"trustStatus"
],
"type": "object"
},
@@ -124,6 +132,15 @@
],
"type": "string"
},
"HookTrustStatus": {
"enum": [
"managed",
"untrusted",
"trusted",
"modified"
],
"type": "string"
},
"HooksListEntry": {
"properties": {
"cwd": {
@@ -5,5 +5,6 @@ import type { AbsolutePathBuf } from "../AbsolutePathBuf";
import type { HookEventName } from "./HookEventName";
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, sourcePath: AbsolutePathBuf, source: HookSource, pluginId: string | null, displayOrder: bigint, enabled: boolean, isManaged: boolean, };
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, currentHash: string, trustStatus: HookTrustStatus, };
@@ -0,0 +1,5 @@
// GENERATED CODE! DO NOT MODIFY BY HAND!
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
export type HookTrustStatus = "managed" | "untrusted" | "trusted" | "modified";
@@ -168,6 +168,7 @@ export type { HookRunSummary } from "./HookRunSummary";
export type { HookScope } from "./HookScope";
export type { HookSource } from "./HookSource";
export type { HookStartedNotification } from "./HookStartedNotification";
export type { HookTrustStatus } from "./HookTrustStatus";
export type { HooksListEntry } from "./HooksListEntry";
export type { HooksListParams } from "./HooksListParams";
export type { HooksListResponse } from "./HooksListResponse";
@@ -80,6 +80,7 @@ use codex_protocol::protocol::HookRunStatus as CoreHookRunStatus;
use codex_protocol::protocol::HookRunSummary as CoreHookRunSummary;
use codex_protocol::protocol::HookScope as CoreHookScope;
use codex_protocol::protocol::HookSource as CoreHookSource;
use codex_protocol::protocol::HookTrustStatus as CoreHookTrustStatus;
use codex_protocol::protocol::ModelRerouteReason as CoreModelRerouteReason;
use codex_protocol::protocol::ModelVerification as CoreModelVerification;
use codex_protocol::protocol::NetworkAccess as CoreNetworkAccess;
@@ -482,6 +483,12 @@ v2_enum_from_core!(
}
);
v2_enum_from_core!(
pub enum HookTrustStatus from CoreHookTrustStatus {
Managed, Untrusted, Trusted, Modified
}
);
fn default_hook_source() -> HookSource {
HookSource::Unknown
}
@@ -5032,6 +5039,8 @@ pub struct HookMetadata {
pub display_order: i64,
pub enabled: bool,
pub is_managed: bool,
pub current_hash: String,
pub trust_status: HookTrustStatus,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)]