mirror of
https://github.com/openai/codex.git
synced 2026-05-04 03:16:31 +00:00
codex: support hooks in config.toml and requirements.toml (#18893)
## 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>
This commit is contained in:
@@ -17,6 +17,7 @@ use super::requirements_exec_policy::RequirementsExecPolicy;
|
||||
use super::requirements_exec_policy::RequirementsExecPolicyToml;
|
||||
use crate::Constrained;
|
||||
use crate::ConstraintError;
|
||||
use crate::ManagedHooksRequirementsToml;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum RequirementSource {
|
||||
@@ -86,6 +87,7 @@ pub struct ConfigRequirements {
|
||||
pub sandbox_policy: ConstrainedWithSource<SandboxPolicy>,
|
||||
pub web_search_mode: ConstrainedWithSource<WebSearchMode>,
|
||||
pub feature_requirements: Option<Sourced<FeatureRequirementsToml>>,
|
||||
pub managed_hooks: Option<ConstrainedWithSource<ManagedHooksRequirementsToml>>,
|
||||
pub mcp_servers: Option<Sourced<BTreeMap<String, McpServerRequirement>>>,
|
||||
pub exec_policy: Option<Sourced<RequirementsExecPolicy>>,
|
||||
pub enforce_residency: ConstrainedWithSource<Option<ResidencyRequirement>>,
|
||||
@@ -117,6 +119,7 @@ impl Default for ConfigRequirements {
|
||||
/*source*/ None,
|
||||
),
|
||||
feature_requirements: None,
|
||||
managed_hooks: None,
|
||||
mcp_servers: None,
|
||||
exec_policy: None,
|
||||
enforce_residency: ConstrainedWithSource::new(
|
||||
@@ -628,6 +631,7 @@ pub struct ConfigRequirementsToml {
|
||||
pub allowed_web_search_modes: Option<Vec<WebSearchModeRequirement>>,
|
||||
#[serde(rename = "features", alias = "feature_requirements")]
|
||||
pub feature_requirements: Option<FeatureRequirementsToml>,
|
||||
pub hooks: Option<ManagedHooksRequirementsToml>,
|
||||
pub mcp_servers: Option<BTreeMap<String, McpServerRequirement>>,
|
||||
pub apps: Option<AppsRequirementsToml>,
|
||||
pub rules: Option<RequirementsExecPolicyToml>,
|
||||
@@ -673,6 +677,7 @@ pub struct ConfigRequirementsWithSources {
|
||||
pub allowed_sandbox_modes: Option<Sourced<Vec<SandboxModeRequirement>>>,
|
||||
pub allowed_web_search_modes: Option<Sourced<Vec<WebSearchModeRequirement>>>,
|
||||
pub feature_requirements: Option<Sourced<FeatureRequirementsToml>>,
|
||||
pub hooks: Option<Sourced<ManagedHooksRequirementsToml>>,
|
||||
pub mcp_servers: Option<Sourced<BTreeMap<String, McpServerRequirement>>>,
|
||||
pub apps: Option<Sourced<AppsRequirementsToml>>,
|
||||
pub rules: Option<Sourced<RequirementsExecPolicyToml>>,
|
||||
@@ -707,6 +712,7 @@ impl ConfigRequirementsWithSources {
|
||||
remote_sandbox_config: _,
|
||||
allowed_web_search_modes: _,
|
||||
feature_requirements: _,
|
||||
hooks: _,
|
||||
mcp_servers: _,
|
||||
apps: _,
|
||||
rules: _,
|
||||
@@ -734,6 +740,7 @@ impl ConfigRequirementsWithSources {
|
||||
allowed_sandbox_modes,
|
||||
allowed_web_search_modes,
|
||||
feature_requirements,
|
||||
hooks,
|
||||
mcp_servers,
|
||||
rules,
|
||||
enforce_residency,
|
||||
@@ -759,6 +766,7 @@ impl ConfigRequirementsWithSources {
|
||||
allowed_sandbox_modes,
|
||||
allowed_web_search_modes,
|
||||
feature_requirements,
|
||||
hooks,
|
||||
mcp_servers,
|
||||
apps,
|
||||
rules,
|
||||
@@ -774,6 +782,7 @@ impl ConfigRequirementsWithSources {
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: allowed_web_search_modes.map(|sourced| sourced.value),
|
||||
feature_requirements: feature_requirements.map(|sourced| sourced.value),
|
||||
hooks: hooks.map(|sourced| sourced.value),
|
||||
mcp_servers: mcp_servers.map(|sourced| sourced.value),
|
||||
apps: apps.map(|sourced| sourced.value),
|
||||
rules: rules.map(|sourced| sourced.value),
|
||||
@@ -858,6 +867,10 @@ impl ConfigRequirementsToml {
|
||||
.feature_requirements
|
||||
.as_ref()
|
||||
.is_none_or(FeatureRequirementsToml::is_empty)
|
||||
&& self
|
||||
.hooks
|
||||
.as_ref()
|
||||
.is_none_or(ManagedHooksRequirementsToml::is_empty)
|
||||
&& self.mcp_servers.is_none()
|
||||
&& self
|
||||
.apps
|
||||
@@ -884,6 +897,7 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
|
||||
allowed_sandbox_modes,
|
||||
allowed_web_search_modes,
|
||||
feature_requirements,
|
||||
hooks,
|
||||
mcp_servers,
|
||||
apps: _apps,
|
||||
rules,
|
||||
@@ -1064,6 +1078,34 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
|
||||
};
|
||||
let feature_requirements =
|
||||
feature_requirements.filter(|requirements| !requirements.value.is_empty());
|
||||
let managed_hooks = hooks
|
||||
.filter(|managed_hooks| managed_hooks.value.handler_count() > 0)
|
||||
.map(|sourced_hooks| {
|
||||
let Sourced {
|
||||
value,
|
||||
source: requirement_source,
|
||||
} = sourced_hooks;
|
||||
let allowed = value;
|
||||
let allowed_for_error = format!("{allowed:?}");
|
||||
let requirement_source_for_error = requirement_source.clone();
|
||||
let constrained = Constrained::new(allowed.clone(), move |candidate| {
|
||||
if candidate == &allowed {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ConstraintError::InvalidValue {
|
||||
field_name: "hooks",
|
||||
candidate: format!("{candidate:?}"),
|
||||
allowed: allowed_for_error.clone(),
|
||||
requirement_source: requirement_source_for_error.clone(),
|
||||
})
|
||||
}
|
||||
})?;
|
||||
Ok(ConstrainedWithSource::new(
|
||||
constrained,
|
||||
Some(requirement_source),
|
||||
))
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
let enforce_residency = match enforce_residency {
|
||||
Some(Sourced {
|
||||
@@ -1106,6 +1148,7 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
|
||||
sandbox_policy,
|
||||
web_search_mode,
|
||||
feature_requirements,
|
||||
managed_hooks,
|
||||
mcp_servers,
|
||||
exec_policy,
|
||||
enforce_residency,
|
||||
@@ -1119,6 +1162,7 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::HookEventsToml;
|
||||
use anyhow::Result;
|
||||
use codex_execpolicy::Decision;
|
||||
use codex_execpolicy::Evaluation;
|
||||
@@ -1147,6 +1191,7 @@ mod tests {
|
||||
remote_sandbox_config: _,
|
||||
allowed_web_search_modes,
|
||||
feature_requirements,
|
||||
hooks,
|
||||
mcp_servers,
|
||||
apps,
|
||||
rules,
|
||||
@@ -1166,6 +1211,7 @@ mod tests {
|
||||
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
feature_requirements: feature_requirements
|
||||
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
hooks: hooks.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
mcp_servers: mcp_servers.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
apps: apps.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
rules: rules.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||
@@ -1210,6 +1256,7 @@ mod tests {
|
||||
remote_sandbox_config: None,
|
||||
allowed_web_search_modes: Some(allowed_web_search_modes.clone()),
|
||||
feature_requirements: Some(feature_requirements.clone()),
|
||||
hooks: None,
|
||||
mcp_servers: None,
|
||||
apps: None,
|
||||
rules: None,
|
||||
@@ -1241,6 +1288,7 @@ mod tests {
|
||||
feature_requirements,
|
||||
enforce_source.clone(),
|
||||
)),
|
||||
hooks: None,
|
||||
mcp_servers: None,
|
||||
apps: None,
|
||||
rules: None,
|
||||
@@ -1278,6 +1326,7 @@ mod tests {
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_web_search_modes: None,
|
||||
feature_requirements: None,
|
||||
hooks: None,
|
||||
mcp_servers: None,
|
||||
apps: None,
|
||||
rules: None,
|
||||
@@ -1323,6 +1372,7 @@ mod tests {
|
||||
allowed_sandbox_modes: None,
|
||||
allowed_web_search_modes: None,
|
||||
feature_requirements: None,
|
||||
hooks: None,
|
||||
mcp_servers: None,
|
||||
apps: None,
|
||||
rules: None,
|
||||
@@ -2232,6 +2282,124 @@ allowed_approvals_reviewers = ["user"]
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_managed_hooks_requirements() -> Result<()> {
|
||||
let toml_str = r#"
|
||||
managed_dir = "/enterprise/hooks"
|
||||
windows_managed_dir = 'C:\enterprise\hooks'
|
||||
|
||||
[[PreToolUse]]
|
||||
matcher = "^Bash$"
|
||||
|
||||
[[PreToolUse.hooks]]
|
||||
type = "command"
|
||||
command = "python3 /enterprise/hooks/pre.py"
|
||||
timeout = 10
|
||||
statusMessage = "checking"
|
||||
"#;
|
||||
let hooks: ManagedHooksRequirementsToml = from_str(toml_str)?;
|
||||
|
||||
assert_eq!(
|
||||
hooks.managed_dir.as_deref(),
|
||||
Some(std::path::Path::new("/enterprise/hooks"))
|
||||
);
|
||||
assert_eq!(hooks.handler_count(), 1);
|
||||
assert_eq!(hooks.hooks.pre_tool_use.len(), 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn merge_unset_fields_does_not_overwrite_existing_hooks() -> Result<()> {
|
||||
let mut target = ConfigRequirementsWithSources::default();
|
||||
target.merge_unset_fields(
|
||||
RequirementSource::CloudRequirements,
|
||||
from_str::<ConfigRequirementsToml>(
|
||||
r#"
|
||||
[hooks]
|
||||
managed_dir = "/cloud/hooks"
|
||||
|
||||
[[hooks.PreToolUse]]
|
||||
matcher = "^Bash$"
|
||||
|
||||
[[hooks.PreToolUse.hooks]]
|
||||
type = "command"
|
||||
command = "python3 /cloud/hooks/pre.py"
|
||||
"#,
|
||||
)?,
|
||||
);
|
||||
target.merge_unset_fields(
|
||||
RequirementSource::SystemRequirementsToml {
|
||||
file: system_requirements_toml_file_for_test()?,
|
||||
},
|
||||
from_str::<ConfigRequirementsToml>(
|
||||
r#"
|
||||
[hooks]
|
||||
managed_dir = "/system/hooks"
|
||||
|
||||
[[hooks.PreToolUse]]
|
||||
matcher = "^Bash$"
|
||||
|
||||
[[hooks.PreToolUse.hooks]]
|
||||
type = "command"
|
||||
command = "python3 /system/hooks/pre.py"
|
||||
"#,
|
||||
)?,
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
target
|
||||
.hooks
|
||||
.as_ref()
|
||||
.and_then(|hooks| hooks.value.managed_dir.as_ref())
|
||||
.map(std::path::PathBuf::as_path),
|
||||
Some(std::path::Path::new("/cloud/hooks"))
|
||||
);
|
||||
assert_eq!(
|
||||
target.hooks.as_ref().map(|hooks| hooks.source.clone()),
|
||||
Some(RequirementSource::CloudRequirements)
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_hooks_constraint_rejects_drift() -> Result<()> {
|
||||
let config: ConfigRequirementsToml = from_str(
|
||||
r#"
|
||||
[hooks]
|
||||
managed_dir = "/enterprise/hooks"
|
||||
|
||||
[[hooks.PreToolUse]]
|
||||
matcher = "^Bash$"
|
||||
|
||||
[[hooks.PreToolUse.hooks]]
|
||||
type = "command"
|
||||
command = "python3 /enterprise/hooks/pre.py"
|
||||
"#,
|
||||
)?;
|
||||
let requirements: ConfigRequirements = with_unknown_source(config).try_into()?;
|
||||
let mut managed_hooks = requirements
|
||||
.managed_hooks
|
||||
.expect("expected managed hooks requirements");
|
||||
|
||||
let err = managed_hooks
|
||||
.set(ManagedHooksRequirementsToml {
|
||||
managed_dir: Some(std::path::PathBuf::from("/other/hooks")),
|
||||
windows_managed_dir: None,
|
||||
hooks: HookEventsToml::default(),
|
||||
})
|
||||
.expect_err("managed hooks should reject drift");
|
||||
|
||||
assert!(matches!(
|
||||
err,
|
||||
ConstraintError::InvalidValue {
|
||||
field_name: "hooks",
|
||||
requirement_source: RequirementSource::Unknown,
|
||||
..
|
||||
}
|
||||
));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn network_requirements_are_preserved_as_constraints_with_source() -> Result<()> {
|
||||
let toml_str = r#"
|
||||
|
||||
@@ -4,6 +4,7 @@ use std::collections::BTreeMap;
|
||||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
use crate::HookEventsToml;
|
||||
use crate::permissions_toml::PermissionsToml;
|
||||
use crate::profile_toml::ConfigProfile;
|
||||
use crate::types::AnalyticsConfigToml;
|
||||
@@ -332,6 +333,9 @@ pub struct ConfigToml {
|
||||
/// User-level skill config entries keyed by SKILL.md path.
|
||||
pub skills: Option<SkillsConfig>,
|
||||
|
||||
/// Lifecycle hooks configured inline in TOML.
|
||||
pub hooks: Option<HookEventsToml>,
|
||||
|
||||
/// User-level plugin config entries keyed by plugin name.
|
||||
#[serde(default)]
|
||||
pub plugins: HashMap<String, PluginConfig>,
|
||||
|
||||
148
codex-rs/config/src/hook_config.rs
Normal file
148
codex-rs/config/src/hook_config.rs
Normal file
@@ -0,0 +1,148 @@
|
||||
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;
|
||||
119
codex-rs/config/src/hooks_tests.rs
Normal file
119
codex-rs/config/src/hooks_tests.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use pretty_assertions::assert_eq;
|
||||
|
||||
use super::HookEventsToml;
|
||||
use super::HookHandlerConfig;
|
||||
use super::HooksFile;
|
||||
use super::ManagedHooksRequirementsToml;
|
||||
use super::MatcherGroup;
|
||||
|
||||
#[test]
|
||||
fn hooks_file_deserializes_existing_json_shape() {
|
||||
let parsed: HooksFile = serde_json::from_str(
|
||||
r#"{
|
||||
"hooks": {
|
||||
"PreToolUse": [
|
||||
{
|
||||
"matcher": "^Bash$",
|
||||
"hooks": [
|
||||
{
|
||||
"type": "command",
|
||||
"command": "python3 /tmp/pre.py",
|
||||
"timeout": 10,
|
||||
"statusMessage": "checking"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}"#,
|
||||
)
|
||||
.expect("hooks.json should deserialize");
|
||||
|
||||
assert_eq!(
|
||||
parsed,
|
||||
HooksFile {
|
||||
hooks: HookEventsToml {
|
||||
pre_tool_use: vec![MatcherGroup {
|
||||
matcher: Some("^Bash$".to_string()),
|
||||
hooks: vec![HookHandlerConfig::Command {
|
||||
command: "python3 /tmp/pre.py".to_string(),
|
||||
timeout_sec: Some(10),
|
||||
r#async: false,
|
||||
status_message: Some("checking".to_string()),
|
||||
}],
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hook_events_deserialize_from_toml_arrays_of_tables() {
|
||||
let parsed: HookEventsToml = toml::from_str(
|
||||
r#"
|
||||
[[PreToolUse]]
|
||||
matcher = "^Bash$"
|
||||
|
||||
[[PreToolUse.hooks]]
|
||||
type = "command"
|
||||
command = "python3 /tmp/pre.py"
|
||||
timeout = 10
|
||||
statusMessage = "checking"
|
||||
"#,
|
||||
)
|
||||
.expect("hook events TOML should deserialize");
|
||||
|
||||
assert_eq!(
|
||||
parsed,
|
||||
HookEventsToml {
|
||||
pre_tool_use: vec![MatcherGroup {
|
||||
matcher: Some("^Bash$".to_string()),
|
||||
hooks: vec![HookHandlerConfig::Command {
|
||||
command: "python3 /tmp/pre.py".to_string(),
|
||||
timeout_sec: Some(10),
|
||||
r#async: false,
|
||||
status_message: Some("checking".to_string()),
|
||||
}],
|
||||
}],
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn managed_hooks_requirements_flatten_hook_events() {
|
||||
let parsed: ManagedHooksRequirementsToml = toml::from_str(
|
||||
r#"
|
||||
managed_dir = "/enterprise/place"
|
||||
|
||||
[[PreToolUse]]
|
||||
matcher = "^Bash$"
|
||||
|
||||
[[PreToolUse.hooks]]
|
||||
type = "command"
|
||||
command = "python3 /enterprise/place/pre.py"
|
||||
"#,
|
||||
)
|
||||
.expect("requirements hooks TOML should deserialize");
|
||||
|
||||
assert_eq!(
|
||||
parsed,
|
||||
ManagedHooksRequirementsToml {
|
||||
managed_dir: Some(std::path::PathBuf::from("/enterprise/place")),
|
||||
windows_managed_dir: None,
|
||||
hooks: HookEventsToml {
|
||||
pre_tool_use: vec![MatcherGroup {
|
||||
matcher: Some("^Bash$".to_string()),
|
||||
hooks: vec![HookHandlerConfig::Command {
|
||||
command: "python3 /enterprise/place/pre.py".to_string(),
|
||||
timeout_sec: None,
|
||||
r#async: false,
|
||||
status_message: None,
|
||||
}],
|
||||
}],
|
||||
..Default::default()
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -4,6 +4,7 @@ pub mod config_toml;
|
||||
mod constraint;
|
||||
mod diagnostics;
|
||||
mod fingerprint;
|
||||
mod hook_config;
|
||||
mod host_name;
|
||||
mod key_aliases;
|
||||
mod marketplace_edit;
|
||||
@@ -27,6 +28,8 @@ pub const CONFIG_TOML_FILE: &str = "config.toml";
|
||||
pub use cloud_requirements::CloudRequirementsLoadError;
|
||||
pub use cloud_requirements::CloudRequirementsLoadErrorCode;
|
||||
pub use cloud_requirements::CloudRequirementsLoader;
|
||||
pub use codex_app_server_protocol::ConfigLayerSource;
|
||||
pub use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
pub use config_requirements::AppRequirementToml;
|
||||
pub use config_requirements::AppsRequirementsToml;
|
||||
pub use config_requirements::ConfigRequirements;
|
||||
@@ -65,6 +68,11 @@ pub use diagnostics::format_config_error;
|
||||
pub use diagnostics::format_config_error_with_source;
|
||||
pub use diagnostics::io_error_from_config_error;
|
||||
pub use fingerprint::version_for_toml;
|
||||
pub use hook_config::HookEventsToml;
|
||||
pub use hook_config::HookHandlerConfig;
|
||||
pub use hook_config::HooksFile;
|
||||
pub use hook_config::ManagedHooksRequirementsToml;
|
||||
pub use hook_config::MatcherGroup;
|
||||
pub use host_name::host_name;
|
||||
pub use marketplace_edit::MarketplaceConfigUpdate;
|
||||
pub use marketplace_edit::RemoveMarketplaceConfigOutcome;
|
||||
@@ -106,5 +114,4 @@ pub use thread_config::ThreadConfigLoadErrorCode;
|
||||
pub use thread_config::ThreadConfigLoader;
|
||||
pub use thread_config::ThreadConfigSource;
|
||||
pub use thread_config::UserThreadConfig;
|
||||
|
||||
pub use codex_app_server_protocol::ConfigLayerSource;
|
||||
pub use toml::Value as TomlValue;
|
||||
|
||||
Reference in New Issue
Block a user