mirror of
https://github.com/openai/codex.git
synced 2026-10-19 16:17:49 +00:00
Add allow_managed_hooks_only hook requirement (#20319)
## Why Enterprise-managed hook policy needs a narrow way to require Codex to ignore user-controlled lifecycle hooks without adopting the broader trust-precedence model from earlier hook work. This keeps the policy anchored in `requirements.toml`, so admins can opt into managed hooks only while normal `config.toml` files cannot enable the restriction themselves. ## What changed - Added `allow_managed_hooks_only` to the requirements data flow and preserved explicit `false` values. - Also adds it to /debug-config - Marked MDM, system, and legacy managed config layers as managed for hook discovery. - Updated hook discovery so `allow_managed_hooks_only = true`: - keeps managed requirements hooks and managed config-layer hooks, - skips user/project/session `hooks.json` and `[hooks]` entries with concise startup warnings, - skips current unmanaged plugin hooks, - ignores any `allow_managed_hooks_only` key placed in ordinary `config.toml` layers.
This commit is contained in:
+6
@@ -7540,6 +7540,12 @@
|
|||||||
},
|
},
|
||||||
"ConfigRequirements": {
|
"ConfigRequirements": {
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"allowManagedHooksOnly": {
|
||||||
|
"type": [
|
||||||
|
"boolean",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"allowedApprovalPolicies": {
|
"allowedApprovalPolicies": {
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/v2/AskForApproval"
|
"$ref": "#/definitions/v2/AskForApproval"
|
||||||
|
|||||||
+6
@@ -3929,6 +3929,12 @@
|
|||||||
},
|
},
|
||||||
"ConfigRequirements": {
|
"ConfigRequirements": {
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"allowManagedHooksOnly": {
|
||||||
|
"type": [
|
||||||
|
"boolean",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"allowedApprovalPolicies": {
|
"allowedApprovalPolicies": {
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/AskForApproval"
|
"$ref": "#/definitions/AskForApproval"
|
||||||
|
|||||||
+6
@@ -62,6 +62,12 @@
|
|||||||
},
|
},
|
||||||
"ConfigRequirements": {
|
"ConfigRequirements": {
|
||||||
"properties": {
|
"properties": {
|
||||||
|
"allowManagedHooksOnly": {
|
||||||
|
"type": [
|
||||||
|
"boolean",
|
||||||
|
"null"
|
||||||
|
]
|
||||||
|
},
|
||||||
"allowedApprovalPolicies": {
|
"allowedApprovalPolicies": {
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/definitions/AskForApproval"
|
"$ref": "#/definitions/AskForApproval"
|
||||||
|
|||||||
@@ -6,4 +6,4 @@ import type { AskForApproval } from "./AskForApproval";
|
|||||||
import type { ResidencyRequirement } from "./ResidencyRequirement";
|
import type { ResidencyRequirement } from "./ResidencyRequirement";
|
||||||
import type { SandboxMode } from "./SandboxMode";
|
import type { SandboxMode } from "./SandboxMode";
|
||||||
|
|
||||||
export type ConfigRequirements = {allowedApprovalPolicies: Array<AskForApproval> | null, allowedSandboxModes: Array<SandboxMode> | null, allowedWebSearchModes: Array<WebSearchMode> | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null};
|
export type ConfigRequirements = {allowedApprovalPolicies: Array<AskForApproval> | null, allowedSandboxModes: Array<SandboxMode> | null, allowedWebSearchModes: Array<WebSearchMode> | null, allowManagedHooksOnly: boolean | null, featureRequirements: { [key in string]?: boolean } | null, enforceResidency: ResidencyRequirement | null};
|
||||||
|
|||||||
@@ -357,6 +357,7 @@ pub struct ConfigRequirements {
|
|||||||
pub allowed_approvals_reviewers: Option<Vec<ApprovalsReviewer>>,
|
pub allowed_approvals_reviewers: Option<Vec<ApprovalsReviewer>>,
|
||||||
pub allowed_sandbox_modes: Option<Vec<SandboxMode>>,
|
pub allowed_sandbox_modes: Option<Vec<SandboxMode>>,
|
||||||
pub allowed_web_search_modes: Option<Vec<WebSearchMode>>,
|
pub allowed_web_search_modes: Option<Vec<WebSearchMode>>,
|
||||||
|
pub allow_managed_hooks_only: Option<bool>,
|
||||||
pub feature_requirements: Option<BTreeMap<String, bool>>,
|
pub feature_requirements: Option<BTreeMap<String, bool>>,
|
||||||
#[experimental("configRequirements/read.hooks")]
|
#[experimental("configRequirements/read.hooks")]
|
||||||
pub hooks: Option<ManagedHooksRequirements>,
|
pub hooks: Option<ManagedHooksRequirements>,
|
||||||
|
|||||||
@@ -1688,6 +1688,7 @@ fn config_requirements_granular_allowed_approval_policy_is_marked_experimental()
|
|||||||
allowed_approvals_reviewers: None,
|
allowed_approvals_reviewers: None,
|
||||||
allowed_sandbox_modes: None,
|
allowed_sandbox_modes: None,
|
||||||
allowed_web_search_modes: None,
|
allowed_web_search_modes: None,
|
||||||
|
allow_managed_hooks_only: None,
|
||||||
feature_requirements: None,
|
feature_requirements: None,
|
||||||
hooks: None,
|
hooks: None,
|
||||||
enforce_residency: None,
|
enforce_residency: None,
|
||||||
|
|||||||
@@ -219,7 +219,7 @@ Example with notification opt-out:
|
|||||||
- `externalAgentConfig/import` — apply selected external-agent migration items by passing explicit `migrationItems` with `cwd` (`null` for home) and any plugin/session `details` returned by detect. When a request includes migration items, the server emits `externalAgentConfig/import/completed` once after the full import finishes (immediately after the response when everything completed synchronously, or after background imports finish).
|
- `externalAgentConfig/import` — apply selected external-agent migration items by passing explicit `migrationItems` with `cwd` (`null` for home) and any plugin/session `details` returned by detect. When a request includes migration items, the server emits `externalAgentConfig/import/completed` once after the full import finishes (immediately after the response when everything completed synchronously, or after background imports finish).
|
||||||
- `config/value/write` — write a single config key/value to the user's config.toml on disk.
|
- `config/value/write` — write a single config key/value to the user's config.toml on disk.
|
||||||
- `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk, with optional `reloadUserConfig: true` to hot-reload loaded threads.
|
- `config/batchWrite` — apply multiple config edits atomically to the user's config.toml on disk, with optional `reloadUserConfig: true` to hot-reload loaded threads.
|
||||||
- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), pinned feature values (`featureRequirements`), managed lifecycle hooks (`hooks`), `enforceResidency`, and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`.
|
- `configRequirements/read` — fetch loaded requirements constraints from `requirements.toml` and/or MDM (or `null` if none are configured), including allow-lists (`allowedApprovalPolicies`, `allowedSandboxModes`, `allowedWebSearchModes`), lifecycle hook lockdown (`allowManagedHooksOnly`), pinned feature values (`featureRequirements`), managed lifecycle hooks (`hooks`), `enforceResidency`, and `network` constraints such as canonical domain/socket permissions plus `managedAllowedDomainsOnly` and `dangerFullAccessDenylistOnly`.
|
||||||
|
|
||||||
### Example: Start or resume a thread
|
### Example: Start or resume a thread
|
||||||
|
|
||||||
|
|||||||
@@ -429,6 +429,7 @@ fn map_requirements_toml_to_api(requirements: ConfigRequirementsToml) -> ConfigR
|
|||||||
}
|
}
|
||||||
normalized
|
normalized
|
||||||
}),
|
}),
|
||||||
|
allow_managed_hooks_only: requirements.allow_managed_hooks_only,
|
||||||
feature_requirements: requirements
|
feature_requirements: requirements
|
||||||
.feature_requirements
|
.feature_requirements
|
||||||
.map(|requirements| requirements.entries),
|
.map(|requirements| requirements.entries),
|
||||||
@@ -611,3 +612,21 @@ fn config_write_error(code: ConfigWriteErrorCode, message: impl Into<String>) ->
|
|||||||
}));
|
}));
|
||||||
error
|
error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::map_requirements_toml_to_api;
|
||||||
|
use codex_config::ConfigRequirementsToml;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn requirements_api_includes_allow_managed_hooks_only() {
|
||||||
|
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
|
||||||
|
allow_managed_hooks_only: Some(true),
|
||||||
|
..ConfigRequirementsToml::default()
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(mapped.allow_managed_hooks_only, Some(true));
|
||||||
|
assert_eq!(mapped.hooks, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1204,6 +1204,7 @@ mod tests {
|
|||||||
allowed_sandbox_modes: None,
|
allowed_sandbox_modes: None,
|
||||||
remote_sandbox_config: None,
|
remote_sandbox_config: None,
|
||||||
allowed_web_search_modes: None,
|
allowed_web_search_modes: None,
|
||||||
|
allow_managed_hooks_only: None,
|
||||||
guardian_policy_config: None,
|
guardian_policy_config: None,
|
||||||
feature_requirements: None,
|
feature_requirements: None,
|
||||||
hooks: None,
|
hooks: None,
|
||||||
@@ -1286,6 +1287,7 @@ mod tests {
|
|||||||
allowed_sandbox_modes: None,
|
allowed_sandbox_modes: None,
|
||||||
remote_sandbox_config: None,
|
remote_sandbox_config: None,
|
||||||
allowed_web_search_modes: None,
|
allowed_web_search_modes: None,
|
||||||
|
allow_managed_hooks_only: None,
|
||||||
guardian_policy_config: None,
|
guardian_policy_config: None,
|
||||||
feature_requirements: None,
|
feature_requirements: None,
|
||||||
hooks: None,
|
hooks: None,
|
||||||
@@ -1319,6 +1321,7 @@ mod tests {
|
|||||||
allowed_sandbox_modes: None,
|
allowed_sandbox_modes: None,
|
||||||
remote_sandbox_config: None,
|
remote_sandbox_config: None,
|
||||||
allowed_web_search_modes: None,
|
allowed_web_search_modes: None,
|
||||||
|
allow_managed_hooks_only: None,
|
||||||
guardian_policy_config: None,
|
guardian_policy_config: None,
|
||||||
feature_requirements: None,
|
feature_requirements: None,
|
||||||
hooks: None,
|
hooks: None,
|
||||||
@@ -1369,6 +1372,7 @@ mod tests {
|
|||||||
allowed_sandbox_modes: None,
|
allowed_sandbox_modes: None,
|
||||||
remote_sandbox_config: None,
|
remote_sandbox_config: None,
|
||||||
allowed_web_search_modes: None,
|
allowed_web_search_modes: None,
|
||||||
|
allow_managed_hooks_only: None,
|
||||||
guardian_policy_config: None,
|
guardian_policy_config: None,
|
||||||
feature_requirements: None,
|
feature_requirements: None,
|
||||||
hooks: None,
|
hooks: None,
|
||||||
@@ -1519,6 +1523,7 @@ command = "sample-mcp"
|
|||||||
allowed_sandbox_modes: None,
|
allowed_sandbox_modes: None,
|
||||||
remote_sandbox_config: None,
|
remote_sandbox_config: None,
|
||||||
allowed_web_search_modes: None,
|
allowed_web_search_modes: None,
|
||||||
|
allow_managed_hooks_only: None,
|
||||||
guardian_policy_config: None,
|
guardian_policy_config: None,
|
||||||
feature_requirements: None,
|
feature_requirements: None,
|
||||||
hooks: None,
|
hooks: None,
|
||||||
@@ -1599,6 +1604,7 @@ command = "sample-mcp"
|
|||||||
allowed_sandbox_modes: None,
|
allowed_sandbox_modes: None,
|
||||||
remote_sandbox_config: None,
|
remote_sandbox_config: None,
|
||||||
allowed_web_search_modes: None,
|
allowed_web_search_modes: None,
|
||||||
|
allow_managed_hooks_only: None,
|
||||||
guardian_policy_config: None,
|
guardian_policy_config: None,
|
||||||
feature_requirements: None,
|
feature_requirements: None,
|
||||||
hooks: None,
|
hooks: None,
|
||||||
@@ -1677,6 +1683,7 @@ command = "sample-mcp"
|
|||||||
allowed_sandbox_modes: None,
|
allowed_sandbox_modes: None,
|
||||||
remote_sandbox_config: None,
|
remote_sandbox_config: None,
|
||||||
allowed_web_search_modes: None,
|
allowed_web_search_modes: None,
|
||||||
|
allow_managed_hooks_only: None,
|
||||||
guardian_policy_config: None,
|
guardian_policy_config: None,
|
||||||
feature_requirements: None,
|
feature_requirements: None,
|
||||||
hooks: None,
|
hooks: None,
|
||||||
@@ -1883,6 +1890,7 @@ command = "sample-mcp"
|
|||||||
allowed_sandbox_modes: None,
|
allowed_sandbox_modes: None,
|
||||||
remote_sandbox_config: None,
|
remote_sandbox_config: None,
|
||||||
allowed_web_search_modes: None,
|
allowed_web_search_modes: None,
|
||||||
|
allow_managed_hooks_only: None,
|
||||||
guardian_policy_config: None,
|
guardian_policy_config: None,
|
||||||
feature_requirements: None,
|
feature_requirements: None,
|
||||||
hooks: None,
|
hooks: None,
|
||||||
@@ -1923,6 +1931,7 @@ command = "sample-mcp"
|
|||||||
allowed_sandbox_modes: None,
|
allowed_sandbox_modes: None,
|
||||||
remote_sandbox_config: None,
|
remote_sandbox_config: None,
|
||||||
allowed_web_search_modes: None,
|
allowed_web_search_modes: None,
|
||||||
|
allow_managed_hooks_only: None,
|
||||||
guardian_policy_config: None,
|
guardian_policy_config: None,
|
||||||
feature_requirements: None,
|
feature_requirements: None,
|
||||||
hooks: None,
|
hooks: None,
|
||||||
@@ -1983,6 +1992,7 @@ command = "sample-mcp"
|
|||||||
allowed_sandbox_modes: None,
|
allowed_sandbox_modes: None,
|
||||||
remote_sandbox_config: None,
|
remote_sandbox_config: None,
|
||||||
allowed_web_search_modes: None,
|
allowed_web_search_modes: None,
|
||||||
|
allow_managed_hooks_only: None,
|
||||||
guardian_policy_config: None,
|
guardian_policy_config: None,
|
||||||
feature_requirements: None,
|
feature_requirements: None,
|
||||||
hooks: None,
|
hooks: None,
|
||||||
@@ -2039,6 +2049,7 @@ command = "sample-mcp"
|
|||||||
allowed_sandbox_modes: None,
|
allowed_sandbox_modes: None,
|
||||||
remote_sandbox_config: None,
|
remote_sandbox_config: None,
|
||||||
allowed_web_search_modes: None,
|
allowed_web_search_modes: None,
|
||||||
|
allow_managed_hooks_only: None,
|
||||||
guardian_policy_config: None,
|
guardian_policy_config: None,
|
||||||
feature_requirements: None,
|
feature_requirements: None,
|
||||||
hooks: None,
|
hooks: None,
|
||||||
@@ -2097,6 +2108,7 @@ command = "sample-mcp"
|
|||||||
allowed_sandbox_modes: None,
|
allowed_sandbox_modes: None,
|
||||||
remote_sandbox_config: None,
|
remote_sandbox_config: None,
|
||||||
allowed_web_search_modes: None,
|
allowed_web_search_modes: None,
|
||||||
|
allow_managed_hooks_only: None,
|
||||||
guardian_policy_config: None,
|
guardian_policy_config: None,
|
||||||
feature_requirements: None,
|
feature_requirements: None,
|
||||||
hooks: None,
|
hooks: None,
|
||||||
@@ -2156,6 +2168,7 @@ command = "sample-mcp"
|
|||||||
allowed_sandbox_modes: None,
|
allowed_sandbox_modes: None,
|
||||||
remote_sandbox_config: None,
|
remote_sandbox_config: None,
|
||||||
allowed_web_search_modes: None,
|
allowed_web_search_modes: None,
|
||||||
|
allow_managed_hooks_only: None,
|
||||||
guardian_policy_config: None,
|
guardian_policy_config: None,
|
||||||
feature_requirements: None,
|
feature_requirements: None,
|
||||||
hooks: None,
|
hooks: None,
|
||||||
@@ -2215,6 +2228,7 @@ command = "sample-mcp"
|
|||||||
allowed_sandbox_modes: None,
|
allowed_sandbox_modes: None,
|
||||||
remote_sandbox_config: None,
|
remote_sandbox_config: None,
|
||||||
allowed_web_search_modes: None,
|
allowed_web_search_modes: None,
|
||||||
|
allow_managed_hooks_only: None,
|
||||||
guardian_policy_config: None,
|
guardian_policy_config: None,
|
||||||
feature_requirements: None,
|
feature_requirements: None,
|
||||||
hooks: None,
|
hooks: None,
|
||||||
@@ -2304,6 +2318,7 @@ command = "sample-mcp"
|
|||||||
allowed_sandbox_modes: None,
|
allowed_sandbox_modes: None,
|
||||||
remote_sandbox_config: None,
|
remote_sandbox_config: None,
|
||||||
allowed_web_search_modes: None,
|
allowed_web_search_modes: None,
|
||||||
|
allow_managed_hooks_only: None,
|
||||||
guardian_policy_config: None,
|
guardian_policy_config: None,
|
||||||
feature_requirements: None,
|
feature_requirements: None,
|
||||||
hooks: None,
|
hooks: None,
|
||||||
@@ -2335,6 +2350,7 @@ command = "sample-mcp"
|
|||||||
allowed_sandbox_modes: None,
|
allowed_sandbox_modes: None,
|
||||||
remote_sandbox_config: None,
|
remote_sandbox_config: None,
|
||||||
allowed_web_search_modes: None,
|
allowed_web_search_modes: None,
|
||||||
|
allow_managed_hooks_only: None,
|
||||||
guardian_policy_config: None,
|
guardian_policy_config: None,
|
||||||
feature_requirements: None,
|
feature_requirements: None,
|
||||||
hooks: None,
|
hooks: None,
|
||||||
|
|||||||
@@ -87,6 +87,7 @@ pub struct ConfigRequirements {
|
|||||||
pub approvals_reviewer: ConstrainedWithSource<ApprovalsReviewer>,
|
pub approvals_reviewer: ConstrainedWithSource<ApprovalsReviewer>,
|
||||||
pub permission_profile: ConstrainedWithSource<PermissionProfile>,
|
pub permission_profile: ConstrainedWithSource<PermissionProfile>,
|
||||||
pub web_search_mode: ConstrainedWithSource<WebSearchMode>,
|
pub web_search_mode: ConstrainedWithSource<WebSearchMode>,
|
||||||
|
pub allow_managed_hooks_only: Option<Sourced<bool>>,
|
||||||
pub feature_requirements: Option<Sourced<FeatureRequirementsToml>>,
|
pub feature_requirements: Option<Sourced<FeatureRequirementsToml>>,
|
||||||
pub managed_hooks: Option<ConstrainedWithSource<ManagedHooksRequirementsToml>>,
|
pub managed_hooks: Option<ConstrainedWithSource<ManagedHooksRequirementsToml>>,
|
||||||
pub mcp_servers: Option<Sourced<BTreeMap<String, McpServerRequirement>>>,
|
pub mcp_servers: Option<Sourced<BTreeMap<String, McpServerRequirement>>>,
|
||||||
@@ -120,6 +121,7 @@ impl Default for ConfigRequirements {
|
|||||||
Constrained::allow_any(WebSearchMode::Cached),
|
Constrained::allow_any(WebSearchMode::Cached),
|
||||||
/*source*/ None,
|
/*source*/ None,
|
||||||
),
|
),
|
||||||
|
allow_managed_hooks_only: None,
|
||||||
feature_requirements: None,
|
feature_requirements: None,
|
||||||
managed_hooks: None,
|
managed_hooks: None,
|
||||||
mcp_servers: None,
|
mcp_servers: None,
|
||||||
@@ -688,6 +690,7 @@ pub struct ConfigRequirementsToml {
|
|||||||
pub allowed_sandbox_modes: Option<Vec<SandboxModeRequirement>>,
|
pub allowed_sandbox_modes: Option<Vec<SandboxModeRequirement>>,
|
||||||
pub remote_sandbox_config: Option<Vec<RemoteSandboxConfigToml>>,
|
pub remote_sandbox_config: Option<Vec<RemoteSandboxConfigToml>>,
|
||||||
pub allowed_web_search_modes: Option<Vec<WebSearchModeRequirement>>,
|
pub allowed_web_search_modes: Option<Vec<WebSearchModeRequirement>>,
|
||||||
|
pub allow_managed_hooks_only: Option<bool>,
|
||||||
#[serde(rename = "features", alias = "feature_requirements")]
|
#[serde(rename = "features", alias = "feature_requirements")]
|
||||||
pub feature_requirements: Option<FeatureRequirementsToml>,
|
pub feature_requirements: Option<FeatureRequirementsToml>,
|
||||||
pub hooks: Option<ManagedHooksRequirementsToml>,
|
pub hooks: Option<ManagedHooksRequirementsToml>,
|
||||||
@@ -736,6 +739,7 @@ pub struct ConfigRequirementsWithSources {
|
|||||||
pub allowed_approvals_reviewers: Option<Sourced<Vec<ApprovalsReviewer>>>,
|
pub allowed_approvals_reviewers: Option<Sourced<Vec<ApprovalsReviewer>>>,
|
||||||
pub allowed_sandbox_modes: Option<Sourced<Vec<SandboxModeRequirement>>>,
|
pub allowed_sandbox_modes: Option<Sourced<Vec<SandboxModeRequirement>>>,
|
||||||
pub allowed_web_search_modes: Option<Sourced<Vec<WebSearchModeRequirement>>>,
|
pub allowed_web_search_modes: Option<Sourced<Vec<WebSearchModeRequirement>>>,
|
||||||
|
pub allow_managed_hooks_only: Option<Sourced<bool>>,
|
||||||
pub feature_requirements: Option<Sourced<FeatureRequirementsToml>>,
|
pub feature_requirements: Option<Sourced<FeatureRequirementsToml>>,
|
||||||
pub hooks: Option<Sourced<ManagedHooksRequirementsToml>>,
|
pub hooks: Option<Sourced<ManagedHooksRequirementsToml>>,
|
||||||
pub mcp_servers: Option<Sourced<BTreeMap<String, McpServerRequirement>>>,
|
pub mcp_servers: Option<Sourced<BTreeMap<String, McpServerRequirement>>>,
|
||||||
@@ -772,6 +776,7 @@ impl ConfigRequirementsWithSources {
|
|||||||
allowed_sandbox_modes: _,
|
allowed_sandbox_modes: _,
|
||||||
remote_sandbox_config: _,
|
remote_sandbox_config: _,
|
||||||
allowed_web_search_modes: _,
|
allowed_web_search_modes: _,
|
||||||
|
allow_managed_hooks_only: _,
|
||||||
feature_requirements: _,
|
feature_requirements: _,
|
||||||
hooks: _,
|
hooks: _,
|
||||||
mcp_servers: _,
|
mcp_servers: _,
|
||||||
@@ -801,6 +806,7 @@ impl ConfigRequirementsWithSources {
|
|||||||
allowed_approvals_reviewers,
|
allowed_approvals_reviewers,
|
||||||
allowed_sandbox_modes,
|
allowed_sandbox_modes,
|
||||||
allowed_web_search_modes,
|
allowed_web_search_modes,
|
||||||
|
allow_managed_hooks_only,
|
||||||
feature_requirements,
|
feature_requirements,
|
||||||
hooks,
|
hooks,
|
||||||
mcp_servers,
|
mcp_servers,
|
||||||
@@ -828,6 +834,7 @@ impl ConfigRequirementsWithSources {
|
|||||||
allowed_approvals_reviewers,
|
allowed_approvals_reviewers,
|
||||||
allowed_sandbox_modes,
|
allowed_sandbox_modes,
|
||||||
allowed_web_search_modes,
|
allowed_web_search_modes,
|
||||||
|
allow_managed_hooks_only,
|
||||||
feature_requirements,
|
feature_requirements,
|
||||||
hooks,
|
hooks,
|
||||||
mcp_servers,
|
mcp_servers,
|
||||||
@@ -845,6 +852,7 @@ impl ConfigRequirementsWithSources {
|
|||||||
allowed_sandbox_modes: allowed_sandbox_modes.map(|sourced| sourced.value),
|
allowed_sandbox_modes: allowed_sandbox_modes.map(|sourced| sourced.value),
|
||||||
remote_sandbox_config: None,
|
remote_sandbox_config: None,
|
||||||
allowed_web_search_modes: allowed_web_search_modes.map(|sourced| sourced.value),
|
allowed_web_search_modes: allowed_web_search_modes.map(|sourced| sourced.value),
|
||||||
|
allow_managed_hooks_only: allow_managed_hooks_only.map(|sourced| sourced.value),
|
||||||
feature_requirements: feature_requirements.map(|sourced| sourced.value),
|
feature_requirements: feature_requirements.map(|sourced| sourced.value),
|
||||||
hooks: hooks.map(|sourced| sourced.value),
|
hooks: hooks.map(|sourced| sourced.value),
|
||||||
mcp_servers: mcp_servers.map(|sourced| sourced.value),
|
mcp_servers: mcp_servers.map(|sourced| sourced.value),
|
||||||
@@ -928,6 +936,7 @@ impl ConfigRequirementsToml {
|
|||||||
&& self.allowed_sandbox_modes.is_none()
|
&& self.allowed_sandbox_modes.is_none()
|
||||||
&& self.remote_sandbox_config.is_none()
|
&& self.remote_sandbox_config.is_none()
|
||||||
&& self.allowed_web_search_modes.is_none()
|
&& self.allowed_web_search_modes.is_none()
|
||||||
|
&& self.allow_managed_hooks_only.is_none()
|
||||||
&& self
|
&& self
|
||||||
.feature_requirements
|
.feature_requirements
|
||||||
.as_ref()
|
.as_ref()
|
||||||
@@ -965,6 +974,7 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
|
|||||||
allowed_approvals_reviewers,
|
allowed_approvals_reviewers,
|
||||||
allowed_sandbox_modes,
|
allowed_sandbox_modes,
|
||||||
allowed_web_search_modes,
|
allowed_web_search_modes,
|
||||||
|
allow_managed_hooks_only,
|
||||||
feature_requirements,
|
feature_requirements,
|
||||||
hooks,
|
hooks,
|
||||||
mcp_servers,
|
mcp_servers,
|
||||||
@@ -1200,6 +1210,7 @@ impl TryFrom<ConfigRequirementsWithSources> for ConfigRequirements {
|
|||||||
approvals_reviewer,
|
approvals_reviewer,
|
||||||
permission_profile,
|
permission_profile,
|
||||||
web_search_mode,
|
web_search_mode,
|
||||||
|
allow_managed_hooks_only,
|
||||||
feature_requirements,
|
feature_requirements,
|
||||||
managed_hooks,
|
managed_hooks,
|
||||||
mcp_servers,
|
mcp_servers,
|
||||||
@@ -1272,6 +1283,7 @@ mod tests {
|
|||||||
allowed_sandbox_modes,
|
allowed_sandbox_modes,
|
||||||
remote_sandbox_config: _,
|
remote_sandbox_config: _,
|
||||||
allowed_web_search_modes,
|
allowed_web_search_modes,
|
||||||
|
allow_managed_hooks_only,
|
||||||
feature_requirements,
|
feature_requirements,
|
||||||
hooks,
|
hooks,
|
||||||
mcp_servers,
|
mcp_servers,
|
||||||
@@ -1292,6 +1304,8 @@ mod tests {
|
|||||||
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||||
allowed_web_search_modes: allowed_web_search_modes
|
allowed_web_search_modes: allowed_web_search_modes
|
||||||
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||||
|
allow_managed_hooks_only: allow_managed_hooks_only
|
||||||
|
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||||
feature_requirements: feature_requirements
|
feature_requirements: feature_requirements
|
||||||
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||||
hooks: hooks.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
hooks: hooks.map(|value| Sourced::new(value, RequirementSource::Unknown)),
|
||||||
@@ -1308,6 +1322,32 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn deserialize_allow_managed_hooks_only() -> Result<()> {
|
||||||
|
let requirements: ConfigRequirementsToml = from_str(
|
||||||
|
r#"
|
||||||
|
allow_managed_hooks_only = true
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
assert_eq!(requirements.allow_managed_hooks_only, Some(true));
|
||||||
|
assert!(!requirements.is_empty());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn allow_managed_hooks_only_false_is_still_configured() -> Result<()> {
|
||||||
|
let requirements: ConfigRequirementsToml = from_str(
|
||||||
|
r#"
|
||||||
|
allow_managed_hooks_only = false
|
||||||
|
"#,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
assert_eq!(requirements.allow_managed_hooks_only, Some(false));
|
||||||
|
assert!(!requirements.is_empty());
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn merge_unset_fields_copies_every_field_and_sets_sources() {
|
fn merge_unset_fields_copies_every_field_and_sets_sources() {
|
||||||
let mut target = ConfigRequirementsWithSources::default();
|
let mut target = ConfigRequirementsWithSources::default();
|
||||||
@@ -1339,6 +1379,7 @@ mod tests {
|
|||||||
allowed_sandbox_modes: Some(allowed_sandbox_modes.clone()),
|
allowed_sandbox_modes: Some(allowed_sandbox_modes.clone()),
|
||||||
remote_sandbox_config: None,
|
remote_sandbox_config: None,
|
||||||
allowed_web_search_modes: Some(allowed_web_search_modes.clone()),
|
allowed_web_search_modes: Some(allowed_web_search_modes.clone()),
|
||||||
|
allow_managed_hooks_only: Some(true),
|
||||||
feature_requirements: Some(feature_requirements.clone()),
|
feature_requirements: Some(feature_requirements.clone()),
|
||||||
hooks: None,
|
hooks: None,
|
||||||
mcp_servers: None,
|
mcp_servers: None,
|
||||||
@@ -1369,6 +1410,10 @@ mod tests {
|
|||||||
allowed_web_search_modes,
|
allowed_web_search_modes,
|
||||||
enforce_source.clone(),
|
enforce_source.clone(),
|
||||||
)),
|
)),
|
||||||
|
allow_managed_hooks_only: Some(Sourced::new(
|
||||||
|
/*value*/ true,
|
||||||
|
enforce_source.clone(),
|
||||||
|
)),
|
||||||
feature_requirements: Some(Sourced::new(
|
feature_requirements: Some(Sourced::new(
|
||||||
feature_requirements,
|
feature_requirements,
|
||||||
enforce_source.clone(),
|
enforce_source.clone(),
|
||||||
@@ -1411,6 +1456,7 @@ mod tests {
|
|||||||
allowed_approvals_reviewers: None,
|
allowed_approvals_reviewers: None,
|
||||||
allowed_sandbox_modes: None,
|
allowed_sandbox_modes: None,
|
||||||
allowed_web_search_modes: None,
|
allowed_web_search_modes: None,
|
||||||
|
allow_managed_hooks_only: None,
|
||||||
feature_requirements: None,
|
feature_requirements: None,
|
||||||
hooks: None,
|
hooks: None,
|
||||||
mcp_servers: None,
|
mcp_servers: None,
|
||||||
@@ -1458,6 +1504,7 @@ mod tests {
|
|||||||
allowed_approvals_reviewers: None,
|
allowed_approvals_reviewers: None,
|
||||||
allowed_sandbox_modes: None,
|
allowed_sandbox_modes: None,
|
||||||
allowed_web_search_modes: None,
|
allowed_web_search_modes: None,
|
||||||
|
allow_managed_hooks_only: None,
|
||||||
feature_requirements: None,
|
feature_requirements: None,
|
||||||
hooks: None,
|
hooks: None,
|
||||||
mcp_servers: None,
|
mcp_servers: None,
|
||||||
|
|||||||
@@ -228,6 +228,76 @@ async fn returns_config_error_for_schema_error_in_user_config() {
|
|||||||
assert_eq!(config_error, &expected_config_error);
|
assert_eq!(config_error, &expected_config_error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn top_level_allow_managed_hooks_only_in_user_config_does_not_enable_requirements_policy()
|
||||||
|
-> std::io::Result<()> {
|
||||||
|
let tmp = tempdir().expect("tempdir");
|
||||||
|
std::fs::write(
|
||||||
|
tmp.path().join(CONFIG_TOML_FILE),
|
||||||
|
"allow_managed_hooks_only = true",
|
||||||
|
)
|
||||||
|
.expect("write config");
|
||||||
|
|
||||||
|
let cwd = AbsolutePathBuf::try_from(tmp.path()).expect("cwd");
|
||||||
|
let layers = load_config_layers_state(
|
||||||
|
LOCAL_FS.as_ref(),
|
||||||
|
tmp.path(),
|
||||||
|
Some(cwd),
|
||||||
|
&[] as &[(String, TomlValue)],
|
||||||
|
LoaderOverrides::default(),
|
||||||
|
CloudRequirementsLoader::default(),
|
||||||
|
&codex_config::NoopThreadConfigLoader,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
assert_eq!(layers.requirements_toml().allow_managed_hooks_only, None);
|
||||||
|
assert!(layers.requirements().allow_managed_hooks_only.is_none());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn hooks_allow_managed_hooks_only_in_user_config_does_not_enable_requirements_policy()
|
||||||
|
-> std::io::Result<()> {
|
||||||
|
let tmp = tempdir().expect("tempdir");
|
||||||
|
let contents = r#"
|
||||||
|
[hooks]
|
||||||
|
allow_managed_hooks_only = true
|
||||||
|
|
||||||
|
[[hooks.PreToolUse]]
|
||||||
|
matcher = "^Bash$"
|
||||||
|
|
||||||
|
[[hooks.PreToolUse.hooks]]
|
||||||
|
type = "command"
|
||||||
|
command = "python3 /tmp/user-hook.py"
|
||||||
|
"#;
|
||||||
|
std::fs::write(tmp.path().join(CONFIG_TOML_FILE), contents).expect("write config");
|
||||||
|
|
||||||
|
let cwd = AbsolutePathBuf::try_from(tmp.path()).expect("cwd");
|
||||||
|
let layers = load_config_layers_state(
|
||||||
|
LOCAL_FS.as_ref(),
|
||||||
|
tmp.path(),
|
||||||
|
Some(cwd),
|
||||||
|
&[] as &[(String, TomlValue)],
|
||||||
|
LoaderOverrides::default(),
|
||||||
|
CloudRequirementsLoader::default(),
|
||||||
|
&codex_config::NoopThreadConfigLoader,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
layers
|
||||||
|
.get_user_layer()
|
||||||
|
.and_then(|layer| layer.config.get("hooks"))
|
||||||
|
.is_some(),
|
||||||
|
"hooks should still deserialize from config.toml"
|
||||||
|
);
|
||||||
|
assert_eq!(layers.requirements_toml().allow_managed_hooks_only, None);
|
||||||
|
assert!(layers.requirements().allow_managed_hooks_only.is_none());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn schema_error_points_to_feature_value() {
|
fn schema_error_points_to_feature_value() {
|
||||||
let tmp = tempdir().expect("tempdir");
|
let tmp = tempdir().expect("tempdir");
|
||||||
@@ -777,6 +847,7 @@ allowed_approval_policies = ["on-request"]
|
|||||||
allowed_sandbox_modes: None,
|
allowed_sandbox_modes: None,
|
||||||
remote_sandbox_config: None,
|
remote_sandbox_config: None,
|
||||||
allowed_web_search_modes: None,
|
allowed_web_search_modes: None,
|
||||||
|
allow_managed_hooks_only: None,
|
||||||
feature_requirements: None,
|
feature_requirements: None,
|
||||||
hooks: None,
|
hooks: None,
|
||||||
mcp_servers: None,
|
mcp_servers: None,
|
||||||
@@ -834,6 +905,7 @@ allowed_approval_policies = ["on-request"]
|
|||||||
allowed_sandbox_modes: None,
|
allowed_sandbox_modes: None,
|
||||||
remote_sandbox_config: None,
|
remote_sandbox_config: None,
|
||||||
allowed_web_search_modes: None,
|
allowed_web_search_modes: None,
|
||||||
|
allow_managed_hooks_only: None,
|
||||||
feature_requirements: None,
|
feature_requirements: None,
|
||||||
hooks: None,
|
hooks: None,
|
||||||
mcp_servers: None,
|
mcp_servers: None,
|
||||||
@@ -1042,6 +1114,7 @@ async fn load_config_layers_includes_cloud_requirements() -> anyhow::Result<()>
|
|||||||
allowed_sandbox_modes: None,
|
allowed_sandbox_modes: None,
|
||||||
remote_sandbox_config: None,
|
remote_sandbox_config: None,
|
||||||
allowed_web_search_modes: None,
|
allowed_web_search_modes: None,
|
||||||
|
allow_managed_hooks_only: None,
|
||||||
feature_requirements: None,
|
feature_requirements: None,
|
||||||
hooks: None,
|
hooks: None,
|
||||||
mcp_servers: None,
|
mcp_servers: None,
|
||||||
|
|||||||
@@ -8167,6 +8167,7 @@ async fn test_requirements_web_search_mode_allowlist_does_not_warn_when_unset()
|
|||||||
allowed_sandbox_modes: None,
|
allowed_sandbox_modes: None,
|
||||||
remote_sandbox_config: None,
|
remote_sandbox_config: None,
|
||||||
allowed_web_search_modes: Some(vec![codex_config::WebSearchModeRequirement::Cached]),
|
allowed_web_search_modes: Some(vec![codex_config::WebSearchModeRequirement::Cached]),
|
||||||
|
allow_managed_hooks_only: None,
|
||||||
feature_requirements: None,
|
feature_requirements: None,
|
||||||
hooks: None,
|
hooks: None,
|
||||||
mcp_servers: None,
|
mcp_servers: None,
|
||||||
@@ -8879,6 +8880,7 @@ async fn explicit_sandbox_mode_falls_back_when_disallowed_by_requirements() -> s
|
|||||||
allowed_sandbox_modes: Some(vec![codex_config::SandboxModeRequirement::ReadOnly]),
|
allowed_sandbox_modes: Some(vec![codex_config::SandboxModeRequirement::ReadOnly]),
|
||||||
remote_sandbox_config: None,
|
remote_sandbox_config: None,
|
||||||
allowed_web_search_modes: None,
|
allowed_web_search_modes: None,
|
||||||
|
allow_managed_hooks_only: None,
|
||||||
feature_requirements: None,
|
feature_requirements: None,
|
||||||
hooks: None,
|
hooks: None,
|
||||||
mcp_servers: None,
|
mcp_servers: None,
|
||||||
|
|||||||
@@ -2125,6 +2125,7 @@ impl Config {
|
|||||||
approvals_reviewer: mut constrained_approvals_reviewer,
|
approvals_reviewer: mut constrained_approvals_reviewer,
|
||||||
permission_profile: mut constrained_permission_profile,
|
permission_profile: mut constrained_permission_profile,
|
||||||
web_search_mode: mut constrained_web_search_mode,
|
web_search_mode: mut constrained_web_search_mode,
|
||||||
|
allow_managed_hooks_only: _,
|
||||||
feature_requirements,
|
feature_requirements,
|
||||||
managed_hooks: _,
|
managed_hooks: _,
|
||||||
mcp_servers,
|
mcp_servers,
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use std::collections::HashMap;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
@@ -19,7 +20,6 @@ use codex_plugin::PluginHookSource;
|
|||||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
use std::collections::HashMap;
|
|
||||||
|
|
||||||
use super::ConfiguredHandler;
|
use super::ConfiguredHandler;
|
||||||
use super::HookListEntry;
|
use super::HookListEntry;
|
||||||
@@ -46,6 +46,17 @@ struct HookHandlerSource<'a> {
|
|||||||
plugin_id: Option<String>,
|
plugin_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
struct HookDiscoveryPolicy {
|
||||||
|
allow_managed_hooks_only: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl HookDiscoveryPolicy {
|
||||||
|
fn allows(self, source: &HookHandlerSource<'_>) -> bool {
|
||||||
|
!self.allow_managed_hooks_only || source.is_managed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) fn discover_handlers(
|
pub(crate) fn discover_handlers(
|
||||||
config_layer_stack: Option<&ConfigLayerStack>,
|
config_layer_stack: Option<&ConfigLayerStack>,
|
||||||
plugin_hook_sources: Vec<PluginHookSource>,
|
plugin_hook_sources: Vec<PluginHookSource>,
|
||||||
@@ -56,6 +67,15 @@ pub(crate) fn discover_handlers(
|
|||||||
let mut warnings = plugin_hook_load_warnings;
|
let mut warnings = plugin_hook_load_warnings;
|
||||||
let mut display_order = 0_i64;
|
let mut display_order = 0_i64;
|
||||||
let hook_states = hook_states_from_stack(config_layer_stack);
|
let hook_states = hook_states_from_stack(config_layer_stack);
|
||||||
|
let policy = HookDiscoveryPolicy {
|
||||||
|
allow_managed_hooks_only: config_layer_stack.is_some_and(|config_layer_stack| {
|
||||||
|
config_layer_stack
|
||||||
|
.requirements()
|
||||||
|
.allow_managed_hooks_only
|
||||||
|
.as_ref()
|
||||||
|
.is_some_and(|requirement| requirement.value)
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
if let Some(config_layer_stack) = config_layer_stack {
|
if let Some(config_layer_stack) = config_layer_stack {
|
||||||
append_managed_requirement_handlers(
|
append_managed_requirement_handlers(
|
||||||
@@ -65,6 +85,7 @@ pub(crate) fn discover_handlers(
|
|||||||
&mut display_order,
|
&mut display_order,
|
||||||
config_layer_stack,
|
config_layer_stack,
|
||||||
&hook_states,
|
&hook_states,
|
||||||
|
policy,
|
||||||
);
|
);
|
||||||
|
|
||||||
for layer in config_layer_stack.get_layers(
|
for layer in config_layer_stack.get_layers(
|
||||||
@@ -72,6 +93,19 @@ pub(crate) fn discover_handlers(
|
|||||||
/*include_disabled*/ false,
|
/*include_disabled*/ false,
|
||||||
) {
|
) {
|
||||||
let (hook_source, is_managed) = hook_metadata_for_config_layer_source(&layer.name);
|
let (hook_source, is_managed) = hook_metadata_for_config_layer_source(&layer.name);
|
||||||
|
let policy_path = config_toml_source_path(layer);
|
||||||
|
let policy_source = HookHandlerSource {
|
||||||
|
path: &policy_path,
|
||||||
|
key_source: policy_path.display().to_string(),
|
||||||
|
source: hook_source,
|
||||||
|
is_managed,
|
||||||
|
hook_states: &hook_states,
|
||||||
|
env: HashMap::new(),
|
||||||
|
plugin_id: None,
|
||||||
|
};
|
||||||
|
if !policy.allows(&policy_source) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
let json_hooks = load_hooks_json(layer.config_folder().as_deref(), &mut warnings);
|
let json_hooks = load_hooks_json(layer.config_folder().as_deref(), &mut warnings);
|
||||||
let toml_hooks = load_toml_hooks_from_layer(layer, &mut warnings);
|
let toml_hooks = load_toml_hooks_from_layer(layer, &mut warnings);
|
||||||
|
|
||||||
@@ -103,6 +137,7 @@ pub(crate) fn discover_handlers(
|
|||||||
plugin_id: None,
|
plugin_id: None,
|
||||||
},
|
},
|
||||||
hook_events,
|
hook_events,
|
||||||
|
policy,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -115,6 +150,7 @@ pub(crate) fn discover_handlers(
|
|||||||
&mut display_order,
|
&mut display_order,
|
||||||
plugin_hook_sources,
|
plugin_hook_sources,
|
||||||
&hook_states,
|
&hook_states,
|
||||||
|
policy,
|
||||||
);
|
);
|
||||||
|
|
||||||
DiscoveryResult {
|
DiscoveryResult {
|
||||||
@@ -131,15 +167,12 @@ fn append_managed_requirement_handlers(
|
|||||||
display_order: &mut i64,
|
display_order: &mut i64,
|
||||||
config_layer_stack: &ConfigLayerStack,
|
config_layer_stack: &ConfigLayerStack,
|
||||||
hook_states: &HashMap<String, HookStateToml>,
|
hook_states: &HashMap<String, HookStateToml>,
|
||||||
|
policy: HookDiscoveryPolicy,
|
||||||
) {
|
) {
|
||||||
let Some(managed_hooks) = config_layer_stack.requirements().managed_hooks.as_ref() else {
|
let Some(managed_hooks) = config_layer_stack.requirements().managed_hooks.as_ref() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
let Some(source_path) =
|
let source_path = managed_hooks_source_path(managed_hooks.get(), managed_hooks.source.as_ref());
|
||||||
managed_hooks_source_path(managed_hooks.get(), managed_hooks.source.as_ref(), warnings)
|
|
||||||
else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
append_hook_events(
|
append_hook_events(
|
||||||
handlers,
|
handlers,
|
||||||
hook_entries,
|
hook_entries,
|
||||||
@@ -155,6 +188,7 @@ fn append_managed_requirement_handlers(
|
|||||||
plugin_id: None,
|
plugin_id: None,
|
||||||
},
|
},
|
||||||
managed_hooks.get().hooks.clone(),
|
managed_hooks.get().hooks.clone(),
|
||||||
|
policy,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,6 +199,7 @@ fn append_plugin_hook_sources(
|
|||||||
display_order: &mut i64,
|
display_order: &mut i64,
|
||||||
plugin_hook_sources: Vec<PluginHookSource>,
|
plugin_hook_sources: Vec<PluginHookSource>,
|
||||||
hook_states: &HashMap<String, HookStateToml>,
|
hook_states: &HashMap<String, HookStateToml>,
|
||||||
|
policy: HookDiscoveryPolicy,
|
||||||
) {
|
) {
|
||||||
for source in plugin_hook_sources {
|
for source in plugin_hook_sources {
|
||||||
let PluginHookSource {
|
let PluginHookSource {
|
||||||
@@ -203,6 +238,7 @@ fn append_plugin_hook_sources(
|
|||||||
plugin_id: Some(plugin_id),
|
plugin_id: Some(plugin_id),
|
||||||
},
|
},
|
||||||
hooks,
|
hooks,
|
||||||
|
policy,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -210,45 +246,35 @@ fn append_plugin_hook_sources(
|
|||||||
fn managed_hooks_source_path(
|
fn managed_hooks_source_path(
|
||||||
managed_hooks: &ManagedHooksRequirementsToml,
|
managed_hooks: &ManagedHooksRequirementsToml,
|
||||||
requirement_source: Option<&RequirementSource>,
|
requirement_source: Option<&RequirementSource>,
|
||||||
warnings: &mut Vec<String>,
|
) -> AbsolutePathBuf {
|
||||||
) -> Option<AbsolutePathBuf> {
|
if let Some(source_path) = managed_hooks.managed_dir_for_current_platform()
|
||||||
let source = requirement_source
|
&& source_path.is_absolute()
|
||||||
.map(ToString::to_string)
|
&& let Ok(source_path) = AbsolutePathBuf::from_absolute_path(source_path)
|
||||||
.unwrap_or_else(|| "managed requirements".to_string());
|
{
|
||||||
let Some(source_path) = managed_hooks.managed_dir_for_current_platform() else {
|
return source_path;
|
||||||
warnings.push(format!(
|
}
|
||||||
"skipping managed hooks from {source}: no managed hook directory is configured for this platform"
|
|
||||||
));
|
|
||||||
return None;
|
|
||||||
};
|
|
||||||
|
|
||||||
if !source_path.is_absolute() {
|
fallback_managed_hooks_source_path(requirement_source)
|
||||||
warnings.push(format!(
|
}
|
||||||
"skipping managed hooks from {source}: managed hook directory {} is not absolute",
|
|
||||||
source_path.display()
|
fn fallback_managed_hooks_source_path(
|
||||||
));
|
requirement_source: Option<&RequirementSource>,
|
||||||
None
|
) -> AbsolutePathBuf {
|
||||||
} else if !source_path.exists() {
|
match requirement_source {
|
||||||
warnings.push(format!(
|
Some(RequirementSource::SystemRequirementsToml { file })
|
||||||
"skipping managed hooks from {source}: managed hook directory {} does not exist",
|
| Some(RequirementSource::LegacyManagedConfigTomlFromFile { file }) => file.clone(),
|
||||||
source_path.display()
|
Some(RequirementSource::MdmManagedPreferences { domain, key }) => {
|
||||||
));
|
synthetic_layer_path(&format!("<mdm:{domain}:{key}>/requirements.toml"))
|
||||||
None
|
}
|
||||||
} else if !source_path.is_dir() {
|
Some(RequirementSource::CloudRequirements) => {
|
||||||
warnings.push(format!(
|
synthetic_layer_path("<cloud-requirements>/requirements.toml")
|
||||||
"skipping managed hooks from {source}: managed hook directory {} is not a directory",
|
}
|
||||||
source_path.display()
|
Some(RequirementSource::LegacyManagedConfigTomlFromMdm) => {
|
||||||
));
|
synthetic_layer_path("<legacy-managed-config.toml-mdm>/managed_config.toml")
|
||||||
None
|
}
|
||||||
} else {
|
Some(RequirementSource::Unknown) | None => {
|
||||||
AbsolutePathBuf::from_absolute_path(source_path)
|
synthetic_layer_path("<managed-requirements>/requirements.toml")
|
||||||
.inspect_err(|err| {
|
}
|
||||||
warnings.push(format!(
|
|
||||||
"skipping managed hooks from {source}: could not normalize managed hook directory {}: {err}",
|
|
||||||
source_path.display()
|
|
||||||
));
|
|
||||||
})
|
|
||||||
.ok()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,7 +376,12 @@ fn append_hook_events(
|
|||||||
display_order: &mut i64,
|
display_order: &mut i64,
|
||||||
source: HookHandlerSource<'_>,
|
source: HookHandlerSource<'_>,
|
||||||
hook_events: HookEventsToml,
|
hook_events: HookEventsToml,
|
||||||
|
policy: HookDiscoveryPolicy,
|
||||||
) {
|
) {
|
||||||
|
if !policy.allows(&source) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
for (event_name, groups) in hook_events.into_matcher_groups() {
|
for (event_name, groups) in hook_events.into_matcher_groups() {
|
||||||
append_matcher_groups(
|
append_matcher_groups(
|
||||||
handlers,
|
handlers,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use codex_config::HookHandlerConfig;
|
|||||||
use codex_config::ManagedHooksRequirementsToml;
|
use codex_config::ManagedHooksRequirementsToml;
|
||||||
use codex_config::MatcherGroup;
|
use codex_config::MatcherGroup;
|
||||||
use codex_config::RequirementSource;
|
use codex_config::RequirementSource;
|
||||||
|
use codex_config::Sourced;
|
||||||
use codex_config::TomlValue;
|
use codex_config::TomlValue;
|
||||||
use codex_plugin::PluginHookSource;
|
use codex_plugin::PluginHookSource;
|
||||||
use codex_plugin::PluginId;
|
use codex_plugin::PluginId;
|
||||||
@@ -55,6 +56,88 @@ fn managed_hooks_for_current_platform(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn pre_tool_use_hook_events(command: impl Into<String>) -> HookEventsToml {
|
||||||
|
HookEventsToml {
|
||||||
|
pre_tool_use: vec![MatcherGroup {
|
||||||
|
matcher: Some("^Bash$".to_string()),
|
||||||
|
hooks: vec![HookHandlerConfig::Command {
|
||||||
|
command: command.into(),
|
||||||
|
command_windows: None,
|
||||||
|
timeout_sec: Some(10),
|
||||||
|
r#async: false,
|
||||||
|
status_message: Some("checking".to_string()),
|
||||||
|
}],
|
||||||
|
}],
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn config_toml_with_pre_tool_use(command: &str) -> TomlValue {
|
||||||
|
let mut config_toml = TomlValue::Table(Default::default());
|
||||||
|
let TomlValue::Table(config_table) = &mut config_toml else {
|
||||||
|
unreachable!("config TOML root should be a table");
|
||||||
|
};
|
||||||
|
let mut hooks_table = TomlValue::Table(Default::default());
|
||||||
|
let TomlValue::Table(hooks_entries) = &mut hooks_table else {
|
||||||
|
unreachable!("hooks entry should be a table");
|
||||||
|
};
|
||||||
|
let mut pre_tool_use_group = TomlValue::Table(Default::default());
|
||||||
|
let TomlValue::Table(pre_tool_use_group_entries) = &mut pre_tool_use_group else {
|
||||||
|
unreachable!("PreToolUse group should be a table");
|
||||||
|
};
|
||||||
|
pre_tool_use_group_entries.insert(
|
||||||
|
"matcher".to_string(),
|
||||||
|
TomlValue::String("^Bash$".to_string()),
|
||||||
|
);
|
||||||
|
let mut handler = TomlValue::Table(Default::default());
|
||||||
|
let TomlValue::Table(handler_entries) = &mut handler else {
|
||||||
|
unreachable!("PreToolUse handler should be a table");
|
||||||
|
};
|
||||||
|
handler_entries.insert("type".to_string(), TomlValue::String("command".to_string()));
|
||||||
|
handler_entries.insert(
|
||||||
|
"command".to_string(),
|
||||||
|
TomlValue::String(command.to_string()),
|
||||||
|
);
|
||||||
|
handler_entries.insert("timeout".to_string(), TomlValue::Integer(10));
|
||||||
|
handler_entries.insert(
|
||||||
|
"statusMessage".to_string(),
|
||||||
|
TomlValue::String("checking".to_string()),
|
||||||
|
);
|
||||||
|
pre_tool_use_group_entries.insert("hooks".to_string(), TomlValue::Array(vec![handler]));
|
||||||
|
hooks_entries.insert(
|
||||||
|
"PreToolUse".to_string(),
|
||||||
|
TomlValue::Array(vec![pre_tool_use_group]),
|
||||||
|
);
|
||||||
|
config_table.insert("hooks".to_string(), hooks_table);
|
||||||
|
config_toml
|
||||||
|
}
|
||||||
|
|
||||||
|
fn requirements_with_managed_hooks_only(
|
||||||
|
allow_managed_hooks_only: bool,
|
||||||
|
managed_hooks: Option<ManagedHooksRequirementsToml>,
|
||||||
|
) -> (ConfigRequirements, ConfigRequirementsToml) {
|
||||||
|
(
|
||||||
|
ConfigRequirements {
|
||||||
|
allow_managed_hooks_only: Some(Sourced::new(
|
||||||
|
allow_managed_hooks_only,
|
||||||
|
RequirementSource::CloudRequirements,
|
||||||
|
)),
|
||||||
|
managed_hooks: managed_hooks.clone().map(|hooks| {
|
||||||
|
ConstrainedWithSource::new(
|
||||||
|
Constrained::allow_any(hooks),
|
||||||
|
Some(RequirementSource::CloudRequirements),
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
..ConfigRequirements::default()
|
||||||
|
},
|
||||||
|
ConfigRequirementsToml {
|
||||||
|
allow_managed_hooks_only: Some(allow_managed_hooks_only),
|
||||||
|
hooks: managed_hooks,
|
||||||
|
..ConfigRequirementsToml::default()
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn requirements_managed_hooks_execute_from_managed_dir() {
|
async fn requirements_managed_hooks_execute_from_managed_dir() {
|
||||||
let temp = tempdir().expect("create temp dir");
|
let temp = tempdir().expect("create temp dir");
|
||||||
@@ -535,7 +618,7 @@ fn trusted_plugin_hook_stack(
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn requirements_managed_hooks_warn_when_managed_dir_is_missing() {
|
fn requirements_managed_hooks_load_when_managed_dir_is_missing() {
|
||||||
let temp = tempdir().expect("create temp dir");
|
let temp = tempdir().expect("create temp dir");
|
||||||
let missing_dir = temp.path().join("missing-managed-hooks");
|
let missing_dir = temp.path().join("missing-managed-hooks");
|
||||||
let managed_hooks = managed_hooks_for_current_platform(
|
let managed_hooks = managed_hooks_for_current_platform(
|
||||||
@@ -544,7 +627,7 @@ fn requirements_managed_hooks_warn_when_managed_dir_is_missing() {
|
|||||||
pre_tool_use: vec![MatcherGroup {
|
pre_tool_use: vec![MatcherGroup {
|
||||||
matcher: Some("^Bash$".to_string()),
|
matcher: Some("^Bash$".to_string()),
|
||||||
hooks: vec![HookHandlerConfig::Command {
|
hooks: vec![HookHandlerConfig::Command {
|
||||||
command: format!("python3 {}", missing_dir.join("pre.py").display()),
|
command: "echo hi".to_string(),
|
||||||
command_windows: None,
|
command_windows: None,
|
||||||
timeout_sec: Some(10),
|
timeout_sec: Some(10),
|
||||||
r#async: false,
|
r#async: false,
|
||||||
@@ -581,30 +664,294 @@ fn requirements_managed_hooks_warn_when_managed_dir_is_missing() {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
assert!(engine.warnings().iter().any(|warning| {
|
assert!(engine.warnings().is_empty());
|
||||||
warning.contains("managed hook directory")
|
|
||||||
&& warning.contains("does not exist")
|
|
||||||
&& warning.contains(&missing_dir.display().to_string())
|
|
||||||
}));
|
|
||||||
let cwd = cwd();
|
let cwd = cwd();
|
||||||
assert!(
|
let preview = engine.preview_pre_tool_use(&PreToolUseRequest {
|
||||||
engine
|
session_id: ThreadId::new(),
|
||||||
.preview_pre_tool_use(&PreToolUseRequest {
|
turn_id: "turn-1".to_string(),
|
||||||
session_id: ThreadId::new(),
|
cwd,
|
||||||
turn_id: "turn-1".to_string(),
|
transcript_path: None,
|
||||||
cwd,
|
model: "gpt-test".to_string(),
|
||||||
transcript_path: None,
|
permission_mode: "default".to_string(),
|
||||||
model: "gpt-test".to_string(),
|
tool_name: "Bash".to_string(),
|
||||||
permission_mode: "default".to_string(),
|
matcher_aliases: Vec::new(),
|
||||||
tool_name: "Bash".to_string(),
|
tool_use_id: "tool-1".to_string(),
|
||||||
matcher_aliases: Vec::new(),
|
tool_input: serde_json::json!({ "command": "echo hello" }),
|
||||||
tool_use_id: "tool-1".to_string(),
|
});
|
||||||
tool_input: serde_json::json!({ "command": "echo hello" }),
|
assert_eq!(preview.len(), 1);
|
||||||
})
|
assert_eq!(engine.handlers[0].command, "echo hi");
|
||||||
.is_empty()
|
assert_eq!(
|
||||||
|
engine.handlers[0].source_path,
|
||||||
|
AbsolutePathBuf::try_from(missing_dir).expect("absolute missing dir")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn allow_managed_hooks_only_false_keeps_unmanaged_hooks() {
|
||||||
|
let temp = tempdir().expect("create temp dir");
|
||||||
|
let config_path =
|
||||||
|
AbsolutePathBuf::try_from(temp.path().join("config.toml")).expect("absolute config path");
|
||||||
|
let (requirements, requirements_toml) = requirements_with_managed_hooks_only(
|
||||||
|
/*allow_managed_hooks_only*/ false, /*managed_hooks*/ None,
|
||||||
|
);
|
||||||
|
let config_layer_stack = ConfigLayerStack::new(
|
||||||
|
vec![ConfigLayerEntry::new(
|
||||||
|
ConfigLayerSource::User { file: config_path },
|
||||||
|
config_toml_with_pre_tool_use("python3 /tmp/user-hook.py"),
|
||||||
|
)],
|
||||||
|
requirements,
|
||||||
|
requirements_toml,
|
||||||
|
)
|
||||||
|
.expect("config layer stack");
|
||||||
|
|
||||||
|
let engine = ClaudeHooksEngine::new(
|
||||||
|
/*enabled*/ true,
|
||||||
|
Some(&config_layer_stack),
|
||||||
|
Vec::new(),
|
||||||
|
Vec::new(),
|
||||||
|
CommandShell {
|
||||||
|
program: String::new(),
|
||||||
|
args: Vec::new(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(engine.warnings().is_empty());
|
||||||
|
assert!(engine.handlers.is_empty());
|
||||||
|
let discovered =
|
||||||
|
super::discovery::discover_handlers(Some(&config_layer_stack), Vec::new(), Vec::new());
|
||||||
|
assert_eq!(discovered.hook_entries.len(), 1);
|
||||||
|
assert!(!discovered.hook_entries[0].is_managed);
|
||||||
|
assert_eq!(
|
||||||
|
discovered.hook_entries[0].command.as_deref(),
|
||||||
|
Some("python3 /tmp/user-hook.py")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn allow_managed_hooks_only_in_config_toml_does_not_enable_policy() {
|
||||||
|
let temp = tempdir().expect("create temp dir");
|
||||||
|
let config_path =
|
||||||
|
AbsolutePathBuf::try_from(temp.path().join("config.toml")).expect("absolute config path");
|
||||||
|
let mut config_toml = config_toml_with_pre_tool_use("python3 /tmp/user-hook.py");
|
||||||
|
let TomlValue::Table(config_table) = &mut config_toml else {
|
||||||
|
unreachable!("config TOML root should be a table");
|
||||||
|
};
|
||||||
|
config_table.insert(
|
||||||
|
"allow_managed_hooks_only".to_string(),
|
||||||
|
TomlValue::Boolean(true),
|
||||||
|
);
|
||||||
|
let config_layer_stack = ConfigLayerStack::new(
|
||||||
|
vec![ConfigLayerEntry::new(
|
||||||
|
ConfigLayerSource::User { file: config_path },
|
||||||
|
config_toml,
|
||||||
|
)],
|
||||||
|
ConfigRequirements::default(),
|
||||||
|
ConfigRequirementsToml::default(),
|
||||||
|
)
|
||||||
|
.expect("config layer stack");
|
||||||
|
|
||||||
|
let engine = ClaudeHooksEngine::new(
|
||||||
|
/*enabled*/ true,
|
||||||
|
Some(&config_layer_stack),
|
||||||
|
Vec::new(),
|
||||||
|
Vec::new(),
|
||||||
|
CommandShell {
|
||||||
|
program: String::new(),
|
||||||
|
args: Vec::new(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(engine.warnings().is_empty());
|
||||||
|
assert!(engine.handlers.is_empty());
|
||||||
|
let discovered =
|
||||||
|
super::discovery::discover_handlers(Some(&config_layer_stack), Vec::new(), Vec::new());
|
||||||
|
assert_eq!(discovered.hook_entries.len(), 1);
|
||||||
|
assert!(!discovered.hook_entries[0].is_managed);
|
||||||
|
assert_eq!(
|
||||||
|
discovered.hook_entries[0].command.as_deref(),
|
||||||
|
Some("python3 /tmp/user-hook.py")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn allow_managed_hooks_only_skips_unmanaged_json_and_toml_hooks() {
|
||||||
|
let temp = tempdir().expect("create temp dir");
|
||||||
|
let config_path =
|
||||||
|
AbsolutePathBuf::try_from(temp.path().join("config.toml")).expect("absolute config path");
|
||||||
|
let hooks_json_path =
|
||||||
|
AbsolutePathBuf::try_from(temp.path().join("hooks.json")).expect("absolute hooks path");
|
||||||
|
fs::write(
|
||||||
|
hooks_json_path.as_path(),
|
||||||
|
r#"{
|
||||||
|
"hooks": {
|
||||||
|
"PreToolUse": [
|
||||||
|
{
|
||||||
|
"matcher": "^Bash$",
|
||||||
|
"hooks": [
|
||||||
|
{
|
||||||
|
"type": "command",
|
||||||
|
"command": "python3 /tmp/json-hook.py"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}"#,
|
||||||
|
)
|
||||||
|
.expect("write hooks.json");
|
||||||
|
let (requirements, requirements_toml) = requirements_with_managed_hooks_only(
|
||||||
|
/*allow_managed_hooks_only*/ true, /*managed_hooks*/ None,
|
||||||
|
);
|
||||||
|
let config_layer_stack = ConfigLayerStack::new(
|
||||||
|
vec![ConfigLayerEntry::new(
|
||||||
|
ConfigLayerSource::User { file: config_path },
|
||||||
|
config_toml_with_pre_tool_use("python3 /tmp/toml-hook.py"),
|
||||||
|
)],
|
||||||
|
requirements,
|
||||||
|
requirements_toml,
|
||||||
|
)
|
||||||
|
.expect("config layer stack");
|
||||||
|
|
||||||
|
let engine = ClaudeHooksEngine::new(
|
||||||
|
/*enabled*/ true,
|
||||||
|
Some(&config_layer_stack),
|
||||||
|
Vec::new(),
|
||||||
|
Vec::new(),
|
||||||
|
CommandShell {
|
||||||
|
program: String::new(),
|
||||||
|
args: Vec::new(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(engine.handlers.is_empty());
|
||||||
|
assert!(engine.warnings().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn allow_managed_hooks_only_skips_unmanaged_plugin_hooks() {
|
||||||
|
let temp = tempdir().expect("create temp dir");
|
||||||
|
let plugin_root =
|
||||||
|
AbsolutePathBuf::try_from(temp.path().join("demo-plugin")).expect("plugin root");
|
||||||
|
let plugin_data_root =
|
||||||
|
AbsolutePathBuf::try_from(temp.path().join("plugin-data")).expect("plugin data root");
|
||||||
|
let source_path = plugin_root.join("hooks/hooks.json");
|
||||||
|
let plugin_id = PluginId::parse("demo-plugin@test-marketplace").expect("plugin id");
|
||||||
|
let plugin_hook_sources = vec![PluginHookSource {
|
||||||
|
plugin_id,
|
||||||
|
plugin_root,
|
||||||
|
plugin_data_root,
|
||||||
|
source_path,
|
||||||
|
source_relative_path: "hooks/hooks.json".to_string(),
|
||||||
|
hooks: pre_tool_use_hook_events("python3 /tmp/plugin-hook.py"),
|
||||||
|
}];
|
||||||
|
let (requirements, requirements_toml) = requirements_with_managed_hooks_only(
|
||||||
|
/*allow_managed_hooks_only*/ true, /*managed_hooks*/ None,
|
||||||
|
);
|
||||||
|
let config_layer_stack = ConfigLayerStack::new(Vec::new(), requirements, requirements_toml)
|
||||||
|
.expect("config layer stack");
|
||||||
|
|
||||||
|
let engine = ClaudeHooksEngine::new(
|
||||||
|
/*enabled*/ true,
|
||||||
|
Some(&config_layer_stack),
|
||||||
|
plugin_hook_sources,
|
||||||
|
Vec::new(),
|
||||||
|
CommandShell {
|
||||||
|
program: String::new(),
|
||||||
|
args: Vec::new(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(engine.handlers.is_empty());
|
||||||
|
assert!(engine.warnings().is_empty());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn allow_managed_hooks_only_keeps_managed_requirement_and_config_layer_hooks() {
|
||||||
|
let temp = tempdir().expect("create temp dir");
|
||||||
|
let managed_dir =
|
||||||
|
AbsolutePathBuf::try_from(temp.path().join("managed-hooks")).expect("absolute path");
|
||||||
|
fs::create_dir_all(managed_dir.as_path()).expect("create managed hooks dir");
|
||||||
|
let system_config_path =
|
||||||
|
AbsolutePathBuf::try_from(temp.path().join("system").join("config.toml"))
|
||||||
|
.expect("absolute system config path");
|
||||||
|
let system_parent = system_config_path
|
||||||
|
.as_path()
|
||||||
|
.parent()
|
||||||
|
.expect("system config parent");
|
||||||
|
fs::create_dir_all(system_parent).expect("create system config dir");
|
||||||
|
let legacy_config_path = AbsolutePathBuf::try_from(temp.path().join("managed_config.toml"))
|
||||||
|
.expect("absolute legacy config path");
|
||||||
|
|
||||||
|
let managed_hooks = managed_hooks_for_current_platform(
|
||||||
|
managed_dir,
|
||||||
|
pre_tool_use_hook_events("python3 /tmp/requirements-hook.py"),
|
||||||
|
);
|
||||||
|
let (requirements, requirements_toml) = requirements_with_managed_hooks_only(
|
||||||
|
/*allow_managed_hooks_only*/ true,
|
||||||
|
Some(managed_hooks),
|
||||||
|
);
|
||||||
|
let config_layer_stack = ConfigLayerStack::new(
|
||||||
|
vec![
|
||||||
|
ConfigLayerEntry::new(
|
||||||
|
ConfigLayerSource::Mdm {
|
||||||
|
domain: "com.openai.codex".to_string(),
|
||||||
|
key: "config".to_string(),
|
||||||
|
},
|
||||||
|
config_toml_with_pre_tool_use("python3 /tmp/mdm-hook.py"),
|
||||||
|
),
|
||||||
|
ConfigLayerEntry::new(
|
||||||
|
ConfigLayerSource::System {
|
||||||
|
file: system_config_path,
|
||||||
|
},
|
||||||
|
config_toml_with_pre_tool_use("python3 /tmp/system-hook.py"),
|
||||||
|
),
|
||||||
|
ConfigLayerEntry::new(
|
||||||
|
ConfigLayerSource::LegacyManagedConfigTomlFromFile {
|
||||||
|
file: legacy_config_path,
|
||||||
|
},
|
||||||
|
config_toml_with_pre_tool_use("python3 /tmp/legacy-file-hook.py"),
|
||||||
|
),
|
||||||
|
ConfigLayerEntry::new(
|
||||||
|
ConfigLayerSource::LegacyManagedConfigTomlFromMdm,
|
||||||
|
config_toml_with_pre_tool_use("python3 /tmp/legacy-mdm-hook.py"),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
requirements,
|
||||||
|
requirements_toml,
|
||||||
|
)
|
||||||
|
.expect("config layer stack");
|
||||||
|
|
||||||
|
let engine = ClaudeHooksEngine::new(
|
||||||
|
/*enabled*/ true,
|
||||||
|
Some(&config_layer_stack),
|
||||||
|
Vec::new(),
|
||||||
|
Vec::new(),
|
||||||
|
CommandShell {
|
||||||
|
program: String::new(),
|
||||||
|
args: Vec::new(),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
assert!(engine.warnings().is_empty());
|
||||||
|
assert_eq!(
|
||||||
|
engine
|
||||||
|
.handlers
|
||||||
|
.iter()
|
||||||
|
.map(|handler| handler.command.as_str())
|
||||||
|
.collect::<Vec<_>>(),
|
||||||
|
vec![
|
||||||
|
"python3 /tmp/requirements-hook.py",
|
||||||
|
"python3 /tmp/mdm-hook.py",
|
||||||
|
"python3 /tmp/system-hook.py",
|
||||||
|
"python3 /tmp/legacy-file-hook.py",
|
||||||
|
"python3 /tmp/legacy-mdm-hook.py",
|
||||||
|
]
|
||||||
|
);
|
||||||
|
let discovered =
|
||||||
|
super::discovery::discover_handlers(Some(&config_layer_stack), Vec::new(), Vec::new());
|
||||||
|
assert!(discovered.hook_entries.iter().all(|entry| entry.is_managed));
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn discovers_hooks_from_json_and_toml_in_the_same_layer() {
|
fn discovers_hooks_from_json_and_toml_in_the_same_layer() {
|
||||||
let temp = tempdir().expect("create temp dir");
|
let temp = tempdir().expect("create temp dir");
|
||||||
|
|||||||
@@ -145,6 +145,17 @@ fn render_debug_config_lines(stack: &ConfigLayerStack) -> Vec<Line<'static>> {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if let Some(allow_managed_hooks_only) = requirements_toml.allow_managed_hooks_only {
|
||||||
|
requirement_lines.push(requirement_line(
|
||||||
|
"allow_managed_hooks_only",
|
||||||
|
allow_managed_hooks_only.to_string(),
|
||||||
|
requirements
|
||||||
|
.allow_managed_hooks_only
|
||||||
|
.as_ref()
|
||||||
|
.map(|sourced| &sourced.source),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
if requirements_toml.guardian_policy_config.is_some() {
|
if requirements_toml.guardian_policy_config.is_some() {
|
||||||
requirement_lines.push(requirement_line(
|
requirement_lines.push(requirement_line(
|
||||||
"guardian_policy_config",
|
"guardian_policy_config",
|
||||||
@@ -647,6 +658,10 @@ mod tests {
|
|||||||
Constrained::allow_any(WebSearchMode::Cached),
|
Constrained::allow_any(WebSearchMode::Cached),
|
||||||
Some(RequirementSource::CloudRequirements),
|
Some(RequirementSource::CloudRequirements),
|
||||||
),
|
),
|
||||||
|
allow_managed_hooks_only: Some(Sourced::new(
|
||||||
|
/*value*/ true,
|
||||||
|
RequirementSource::CloudRequirements,
|
||||||
|
)),
|
||||||
feature_requirements: Some(Sourced::new(
|
feature_requirements: Some(Sourced::new(
|
||||||
FeatureRequirementsToml {
|
FeatureRequirementsToml {
|
||||||
entries: BTreeMap::from([("guardian_approval".to_string(), true)]),
|
entries: BTreeMap::from([("guardian_approval".to_string(), true)]),
|
||||||
@@ -684,6 +699,7 @@ mod tests {
|
|||||||
allowed_sandbox_modes: Some(vec![SandboxModeRequirement::ReadOnly]),
|
allowed_sandbox_modes: Some(vec![SandboxModeRequirement::ReadOnly]),
|
||||||
remote_sandbox_config: None,
|
remote_sandbox_config: None,
|
||||||
allowed_web_search_modes: Some(vec![WebSearchModeRequirement::Cached]),
|
allowed_web_search_modes: Some(vec![WebSearchModeRequirement::Cached]),
|
||||||
|
allow_managed_hooks_only: Some(true),
|
||||||
guardian_policy_config: Some("Use the managed guardian policy.".to_string()),
|
guardian_policy_config: Some("Use the managed guardian policy.".to_string()),
|
||||||
feature_requirements: Some(FeatureRequirementsToml {
|
feature_requirements: Some(FeatureRequirementsToml {
|
||||||
entries: BTreeMap::from([("guardian_approval".to_string(), true)]),
|
entries: BTreeMap::from([("guardian_approval".to_string(), true)]),
|
||||||
@@ -741,6 +757,7 @@ mod tests {
|
|||||||
"allowed_web_search_modes: cached, disabled (source: cloud requirements)"
|
"allowed_web_search_modes: cached, disabled (source: cloud requirements)"
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
assert!(rendered.contains("allow_managed_hooks_only: true (source: cloud requirements)"));
|
||||||
assert!(
|
assert!(
|
||||||
rendered.contains("guardian_policy_config: configured (source: cloud requirements)")
|
rendered.contains("guardian_policy_config: configured (source: cloud requirements)")
|
||||||
);
|
);
|
||||||
@@ -893,6 +910,7 @@ approval_policy = "never"
|
|||||||
allowed_sandbox_modes: None,
|
allowed_sandbox_modes: None,
|
||||||
remote_sandbox_config: None,
|
remote_sandbox_config: None,
|
||||||
allowed_web_search_modes: Some(Vec::new()),
|
allowed_web_search_modes: Some(Vec::new()),
|
||||||
|
allow_managed_hooks_only: None,
|
||||||
guardian_policy_config: None,
|
guardian_policy_config: None,
|
||||||
feature_requirements: None,
|
feature_requirements: None,
|
||||||
hooks: None,
|
hooks: None,
|
||||||
|
|||||||
@@ -5,3 +5,11 @@ For basic configuration instructions, see [this documentation](https://developer
|
|||||||
For advanced configuration instructions, see [this documentation](https://developers.openai.com/codex/config-advanced).
|
For advanced configuration instructions, see [this documentation](https://developers.openai.com/codex/config-advanced).
|
||||||
|
|
||||||
For a full configuration reference, see [this documentation](https://developers.openai.com/codex/config-reference).
|
For a full configuration reference, see [this documentation](https://developers.openai.com/codex/config-reference).
|
||||||
|
|
||||||
|
## Lifecycle hooks
|
||||||
|
|
||||||
|
Admins can set top-level `allow_managed_hooks_only = true` in
|
||||||
|
`requirements.toml` to ignore user, project, and session hook configs while
|
||||||
|
still allowing managed hooks from requirements and managed config layers. This
|
||||||
|
setting is only supported in `requirements.toml`; putting it in `config.toml`
|
||||||
|
does not enable managed-hooks-only mode.
|
||||||
|
|||||||
Reference in New Issue
Block a user