mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
2b2de3f38b
## Summary Support the existing hooks schema in inline TOML so hooks can be configured from both `config.toml` and enterprise-managed `requirements.toml` without requiring a separate `hooks.json` payload. This gives enterprise admins a way to ship managed hook policy through the existing requirements channel while still leaving script delivery to MDM or other device-management tooling, and it keeps `hooks.json` working unchanged for existing users. This also lays the groundwork for follow-on managed filtering work such as #15937, while continuing to respect project trust gating from #14718. It does **not** implement `allow_managed_hooks_only` itself. NOTE: yes, it's a bit unfortunate that the toml isn't formatted as closely as normal to our default styling. This is because we're trying to stay compatible with the spec for plugins/hooks that we'll need to support & the main usecase here is embedding into requirements.toml ## What changed - moved the shared hook serde model out of `codex-rs/hooks` into `codex-rs/config` so the same schema can power `hooks.json`, inline `config.toml` hooks, and managed `requirements.toml` hooks - added `hooks` support to both `ConfigToml` and `ConfigRequirementsToml`, including requirements-side `managed_dir` / `windows_managed_dir` - treated requirements-managed hooks as one constrained value via `Constrained`, so managed hook policy is merged atomically and cannot drift across requirement sources - updated hook discovery to load requirements-managed hooks first, then per-layer `hooks.json`, then per-layer inline TOML hooks, with a warning when a single layer defines both representations - threaded managed hook metadata through discovered handlers and exposed requirements hooks in app-server responses, generated schemas, and `/debug-config` - added hook/config coverage in `codex-rs/config`, `codex-rs/hooks`, `codex-rs/core/src/config_loader/tests.rs`, and `codex-rs/core/tests/suite/hooks.rs` ## Testing - `cargo test -p codex-config` - `cargo test -p codex-hooks` - `cargo test -p codex-app-server config_api` ## Documentation Companion updates are needed in the developers website repo for: - the hooks guide - the config reference, sample, basic, and advanced pages - the enterprise managed configuration guide --------- Co-authored-by: Michael Bolin <mbolin@openai.com>
149 lines
4.1 KiB
Rust
149 lines
4.1 KiB
Rust
use std::path::Path;
|
|
use std::path::PathBuf;
|
|
|
|
use codex_protocol::protocol::HookEventName;
|
|
use schemars::JsonSchema;
|
|
use serde::Deserialize;
|
|
use serde::Serialize;
|
|
|
|
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
|
pub struct HooksFile {
|
|
#[serde(default)]
|
|
pub hooks: HookEventsToml,
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
|
pub struct HookEventsToml {
|
|
#[serde(rename = "PreToolUse", default)]
|
|
pub pre_tool_use: Vec<MatcherGroup>,
|
|
#[serde(rename = "PermissionRequest", default)]
|
|
pub permission_request: Vec<MatcherGroup>,
|
|
#[serde(rename = "PostToolUse", default)]
|
|
pub post_tool_use: Vec<MatcherGroup>,
|
|
#[serde(rename = "SessionStart", default)]
|
|
pub session_start: Vec<MatcherGroup>,
|
|
#[serde(rename = "UserPromptSubmit", default)]
|
|
pub user_prompt_submit: Vec<MatcherGroup>,
|
|
#[serde(rename = "Stop", default)]
|
|
pub stop: Vec<MatcherGroup>,
|
|
}
|
|
|
|
impl HookEventsToml {
|
|
pub fn is_empty(&self) -> bool {
|
|
let Self {
|
|
pre_tool_use,
|
|
permission_request,
|
|
post_tool_use,
|
|
session_start,
|
|
user_prompt_submit,
|
|
stop,
|
|
} = self;
|
|
pre_tool_use.is_empty()
|
|
&& permission_request.is_empty()
|
|
&& post_tool_use.is_empty()
|
|
&& session_start.is_empty()
|
|
&& user_prompt_submit.is_empty()
|
|
&& stop.is_empty()
|
|
}
|
|
|
|
pub fn handler_count(&self) -> usize {
|
|
let Self {
|
|
pre_tool_use,
|
|
permission_request,
|
|
post_tool_use,
|
|
session_start,
|
|
user_prompt_submit,
|
|
stop,
|
|
} = self;
|
|
[
|
|
pre_tool_use,
|
|
permission_request,
|
|
post_tool_use,
|
|
session_start,
|
|
user_prompt_submit,
|
|
stop,
|
|
]
|
|
.into_iter()
|
|
.flatten()
|
|
.map(|group| group.hooks.len())
|
|
.sum()
|
|
}
|
|
|
|
pub fn into_matcher_groups(self) -> [(HookEventName, Vec<MatcherGroup>); 6] {
|
|
[
|
|
(HookEventName::PreToolUse, self.pre_tool_use),
|
|
(HookEventName::PermissionRequest, self.permission_request),
|
|
(HookEventName::PostToolUse, self.post_tool_use),
|
|
(HookEventName::SessionStart, self.session_start),
|
|
(HookEventName::UserPromptSubmit, self.user_prompt_submit),
|
|
(HookEventName::Stop, self.stop),
|
|
]
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
|
pub struct MatcherGroup {
|
|
#[serde(default)]
|
|
pub matcher: Option<String>,
|
|
#[serde(default)]
|
|
pub hooks: Vec<HookHandlerConfig>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
|
|
#[serde(tag = "type")]
|
|
pub enum HookHandlerConfig {
|
|
#[serde(rename = "command")]
|
|
Command {
|
|
command: String,
|
|
#[serde(default, rename = "timeout")]
|
|
timeout_sec: Option<u64>,
|
|
#[serde(default)]
|
|
r#async: bool,
|
|
#[serde(default, rename = "statusMessage")]
|
|
status_message: Option<String>,
|
|
},
|
|
#[serde(rename = "prompt")]
|
|
Prompt {},
|
|
#[serde(rename = "agent")]
|
|
Agent {},
|
|
}
|
|
|
|
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
|
pub struct ManagedHooksRequirementsToml {
|
|
pub managed_dir: Option<PathBuf>,
|
|
pub windows_managed_dir: Option<PathBuf>,
|
|
#[serde(flatten)]
|
|
pub hooks: HookEventsToml,
|
|
}
|
|
|
|
impl ManagedHooksRequirementsToml {
|
|
pub fn is_empty(&self) -> bool {
|
|
let Self {
|
|
managed_dir,
|
|
windows_managed_dir,
|
|
hooks,
|
|
} = self;
|
|
managed_dir.is_none() && windows_managed_dir.is_none() && hooks.is_empty()
|
|
}
|
|
|
|
pub fn handler_count(&self) -> usize {
|
|
self.hooks.handler_count()
|
|
}
|
|
|
|
pub fn managed_dir_for_current_platform(&self) -> Option<&Path> {
|
|
#[cfg(windows)]
|
|
{
|
|
self.windows_managed_dir.as_deref()
|
|
}
|
|
|
|
#[cfg(not(windows))]
|
|
{
|
|
self.managed_dir.as_deref()
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "hooks_tests.rs"]
|
|
mod tests;
|