mirror of
https://github.com/openai/codex.git
synced 2026-10-21 17:48:44 +00:00
config: add repeatable UserInstructions declarations
This commit is contained in:
+7
@@ -11728,6 +11728,12 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"UserInstructions": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/ConfiguredHookHandler"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"UserPromptSubmit": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/v2/ConfiguredHookMatcherGroup"
|
||||
@@ -11757,6 +11763,7 @@
|
||||
"Stop",
|
||||
"SubagentStart",
|
||||
"SubagentStop",
|
||||
"UserInstructions",
|
||||
"UserPromptSubmit"
|
||||
],
|
||||
"type": "object"
|
||||
|
||||
+7
@@ -8132,6 +8132,12 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"UserInstructions": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/ConfiguredHookHandler"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"UserPromptSubmit": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/ConfiguredHookMatcherGroup"
|
||||
@@ -8161,6 +8167,7 @@
|
||||
"Stop",
|
||||
"SubagentStart",
|
||||
"SubagentStop",
|
||||
"UserInstructions",
|
||||
"UserPromptSubmit"
|
||||
],
|
||||
"type": "object"
|
||||
|
||||
+7
@@ -339,6 +339,12 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"UserInstructions": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/ConfiguredHookHandler"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"UserPromptSubmit": {
|
||||
"items": {
|
||||
"$ref": "#/definitions/ConfiguredHookMatcherGroup"
|
||||
@@ -368,6 +374,7 @@
|
||||
"Stop",
|
||||
"SubagentStart",
|
||||
"SubagentStop",
|
||||
"UserInstructions",
|
||||
"UserPromptSubmit"
|
||||
],
|
||||
"type": "object"
|
||||
|
||||
+2
-1
@@ -1,6 +1,7 @@
|
||||
// GENERATED CODE! DO NOT MODIFY BY HAND!
|
||||
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { ConfiguredHookHandler } from "./ConfiguredHookHandler";
|
||||
import type { ConfiguredHookMatcherGroup } from "./ConfiguredHookMatcherGroup";
|
||||
|
||||
export type ManagedHooksRequirements = { managedDir: string | null, windowsManagedDir: string | null, PreToolUse: Array<ConfiguredHookMatcherGroup>, PermissionRequest: Array<ConfiguredHookMatcherGroup>, PostToolUse: Array<ConfiguredHookMatcherGroup>, PreCompact: Array<ConfiguredHookMatcherGroup>, PostCompact: Array<ConfiguredHookMatcherGroup>, SessionStart: Array<ConfiguredHookMatcherGroup>, UserPromptSubmit: Array<ConfiguredHookMatcherGroup>, SubagentStart: Array<ConfiguredHookMatcherGroup>, SubagentStop: Array<ConfiguredHookMatcherGroup>, Stop: Array<ConfiguredHookMatcherGroup>, };
|
||||
export type ManagedHooksRequirements = { managedDir: string | null, windowsManagedDir: string | null, PreToolUse: Array<ConfiguredHookMatcherGroup>, PermissionRequest: Array<ConfiguredHookMatcherGroup>, PostToolUse: Array<ConfiguredHookMatcherGroup>, PreCompact: Array<ConfiguredHookMatcherGroup>, PostCompact: Array<ConfiguredHookMatcherGroup>, SessionStart: Array<ConfiguredHookMatcherGroup>, UserInstructions: Array<ConfiguredHookHandler>, UserPromptSubmit: Array<ConfiguredHookMatcherGroup>, SubagentStart: Array<ConfiguredHookMatcherGroup>, SubagentStop: Array<ConfiguredHookMatcherGroup>, Stop: Array<ConfiguredHookMatcherGroup>, };
|
||||
|
||||
@@ -440,6 +440,9 @@ pub struct ManagedHooksRequirements {
|
||||
#[serde(rename = "SessionStart")]
|
||||
#[ts(rename = "SessionStart")]
|
||||
pub session_start: Vec<ConfiguredHookMatcherGroup>,
|
||||
#[serde(rename = "UserInstructions")]
|
||||
#[ts(rename = "UserInstructions")]
|
||||
pub user_instructions: Vec<ConfiguredHookHandler>,
|
||||
#[serde(rename = "UserPromptSubmit")]
|
||||
#[ts(rename = "UserPromptSubmit")]
|
||||
pub user_prompt_submit: Vec<ConfiguredHookMatcherGroup>,
|
||||
|
||||
@@ -408,6 +408,7 @@ fn map_hooks_requirements_to_api(hooks: ManagedHooksRequirementsToml) -> Managed
|
||||
pre_compact,
|
||||
post_compact,
|
||||
session_start,
|
||||
user_instructions,
|
||||
user_prompt_submit,
|
||||
subagent_start,
|
||||
subagent_stop,
|
||||
@@ -423,6 +424,10 @@ fn map_hooks_requirements_to_api(hooks: ManagedHooksRequirementsToml) -> Managed
|
||||
pre_compact: map_hook_matcher_groups_to_api(pre_compact),
|
||||
post_compact: map_hook_matcher_groups_to_api(post_compact),
|
||||
session_start: map_hook_matcher_groups_to_api(session_start),
|
||||
user_instructions: user_instructions
|
||||
.into_iter()
|
||||
.map(map_hook_handler_to_api)
|
||||
.collect(),
|
||||
user_prompt_submit: map_hook_matcher_groups_to_api(user_prompt_submit),
|
||||
subagent_start: map_hook_matcher_groups_to_api(subagent_start),
|
||||
subagent_stop: map_hook_matcher_groups_to_api(subagent_stop),
|
||||
@@ -574,9 +579,14 @@ fn config_write_error(code: ConfigWriteErrorCode, message: impl Into<String>) ->
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::map_requirements_toml_to_api;
|
||||
use codex_app_server_protocol::ConfiguredHookHandler;
|
||||
use codex_app_server_protocol::ManagedHooksRequirements;
|
||||
use codex_app_server_protocol::WindowsSandboxSetupMode;
|
||||
use codex_config::ComputerUseRequirementsToml;
|
||||
use codex_config::ConfigRequirementsToml;
|
||||
use codex_config::HookEventsToml;
|
||||
use codex_config::HookHandlerConfig;
|
||||
use codex_config::ManagedHooksRequirementsToml;
|
||||
use codex_config::ModelsRequirementsToml;
|
||||
use codex_config::NewThreadModelDefaultsToml;
|
||||
use codex_config::WindowsRequirementsToml;
|
||||
@@ -595,6 +605,69 @@ mod tests {
|
||||
assert_eq!(mapped.hooks, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requirements_api_includes_user_instructions_hooks() {
|
||||
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
|
||||
hooks: Some(ManagedHooksRequirementsToml {
|
||||
hooks: HookEventsToml {
|
||||
user_instructions: vec![
|
||||
HookHandlerConfig::Command {
|
||||
command: "python3 first.py".to_string(),
|
||||
command_windows: None,
|
||||
timeout_sec: Some(10),
|
||||
r#async: false,
|
||||
status_message: Some("loading first instructions".to_string()),
|
||||
},
|
||||
HookHandlerConfig::Command {
|
||||
command: "python3 second.py".to_string(),
|
||||
command_windows: None,
|
||||
timeout_sec: Some(20),
|
||||
r#async: false,
|
||||
status_message: Some("loading second instructions".to_string()),
|
||||
},
|
||||
],
|
||||
..HookEventsToml::default()
|
||||
},
|
||||
..ManagedHooksRequirementsToml::default()
|
||||
}),
|
||||
..ConfigRequirementsToml::default()
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
mapped.hooks,
|
||||
Some(ManagedHooksRequirements {
|
||||
managed_dir: None,
|
||||
windows_managed_dir: None,
|
||||
pre_tool_use: Vec::new(),
|
||||
permission_request: Vec::new(),
|
||||
post_tool_use: Vec::new(),
|
||||
pre_compact: Vec::new(),
|
||||
post_compact: Vec::new(),
|
||||
session_start: Vec::new(),
|
||||
user_instructions: vec![
|
||||
ConfiguredHookHandler::Command {
|
||||
command: "python3 first.py".to_string(),
|
||||
command_windows: None,
|
||||
timeout_sec: Some(10),
|
||||
r#async: false,
|
||||
status_message: Some("loading first instructions".to_string()),
|
||||
},
|
||||
ConfiguredHookHandler::Command {
|
||||
command: "python3 second.py".to_string(),
|
||||
command_windows: None,
|
||||
timeout_sec: Some(20),
|
||||
r#async: false,
|
||||
status_message: Some("loading second instructions".to_string()),
|
||||
},
|
||||
],
|
||||
user_prompt_submit: Vec::new(),
|
||||
subagent_start: Vec::new(),
|
||||
subagent_stop: Vec::new(),
|
||||
stop: Vec::new(),
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn requirements_api_includes_permission_default_and_allowlist() {
|
||||
let mapped = map_requirements_toml_to_api(ConfigRequirementsToml {
|
||||
|
||||
@@ -46,6 +46,10 @@ pub struct HookEventsToml {
|
||||
pub post_compact: Vec<MatcherGroup>,
|
||||
#[serde(rename = "SessionStart", default)]
|
||||
pub session_start: Vec<MatcherGroup>,
|
||||
// UserInstructions is unconditional, so it stores handlers directly
|
||||
// instead of wrapping them in matcher groups.
|
||||
#[serde(rename = "UserInstructions", default)]
|
||||
pub user_instructions: Vec<HookHandlerConfig>,
|
||||
#[serde(rename = "UserPromptSubmit", default)]
|
||||
pub user_prompt_submit: Vec<MatcherGroup>,
|
||||
#[serde(rename = "SubagentStart", default)]
|
||||
@@ -65,6 +69,7 @@ impl HookEventsToml {
|
||||
pre_compact,
|
||||
post_compact,
|
||||
session_start,
|
||||
user_instructions,
|
||||
user_prompt_submit,
|
||||
subagent_start,
|
||||
subagent_stop,
|
||||
@@ -76,6 +81,7 @@ impl HookEventsToml {
|
||||
&& pre_compact.is_empty()
|
||||
&& post_compact.is_empty()
|
||||
&& session_start.is_empty()
|
||||
&& user_instructions.is_empty()
|
||||
&& user_prompt_submit.is_empty()
|
||||
&& subagent_start.is_empty()
|
||||
&& subagent_stop.is_empty()
|
||||
@@ -90,12 +96,13 @@ impl HookEventsToml {
|
||||
pre_compact,
|
||||
post_compact,
|
||||
session_start,
|
||||
user_instructions,
|
||||
user_prompt_submit,
|
||||
subagent_start,
|
||||
subagent_stop,
|
||||
stop,
|
||||
} = self;
|
||||
[
|
||||
let matcher_group_handlers = [
|
||||
pre_tool_use,
|
||||
permission_request,
|
||||
post_tool_use,
|
||||
@@ -106,14 +113,16 @@ impl HookEventsToml {
|
||||
subagent_start,
|
||||
subagent_stop,
|
||||
stop,
|
||||
]
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|group| group.hooks.len())
|
||||
.sum()
|
||||
];
|
||||
matcher_group_handlers
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|group| group.hooks.len())
|
||||
.sum::<usize>()
|
||||
+ user_instructions.len()
|
||||
}
|
||||
|
||||
pub fn into_matcher_groups(self) -> [(HookEventName, Vec<MatcherGroup>); 10] {
|
||||
pub fn into_matcher_groups(self) -> [(HookEventName, Vec<MatcherGroup>); 11] {
|
||||
[
|
||||
(HookEventName::PreToolUse, self.pre_tool_use),
|
||||
(HookEventName::PermissionRequest, self.permission_request),
|
||||
@@ -121,6 +130,17 @@ impl HookEventsToml {
|
||||
(HookEventName::PreCompact, self.pre_compact),
|
||||
(HookEventName::PostCompact, self.post_compact),
|
||||
(HookEventName::SessionStart, self.session_start),
|
||||
(
|
||||
HookEventName::UserInstructions,
|
||||
if self.user_instructions.is_empty() {
|
||||
Vec::new()
|
||||
} else {
|
||||
vec![MatcherGroup {
|
||||
matcher: None,
|
||||
hooks: self.user_instructions,
|
||||
}]
|
||||
},
|
||||
),
|
||||
(HookEventName::UserPromptSubmit, self.user_prompt_submit),
|
||||
(HookEventName::SubagentStart, self.subagent_start),
|
||||
(HookEventName::SubagentStop, self.subagent_stop),
|
||||
|
||||
@@ -112,6 +112,68 @@ statusMessage = "checking"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_instructions_event_deserializes_from_toml() {
|
||||
let parsed: HookEventsToml = toml::from_str(
|
||||
r#"
|
||||
[[UserInstructions]]
|
||||
type = "command"
|
||||
command = "python3 /tmp/user_instructions.py"
|
||||
"#,
|
||||
)
|
||||
.expect("UserInstructions hook TOML should deserialize");
|
||||
|
||||
assert_eq!(
|
||||
parsed,
|
||||
HookEventsToml {
|
||||
user_instructions: vec![HookHandlerConfig::Command {
|
||||
command: "python3 /tmp/user_instructions.py".to_string(),
|
||||
command_windows: None,
|
||||
timeout_sec: None,
|
||||
r#async: false,
|
||||
status_message: None,
|
||||
}],
|
||||
..Default::default()
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_instructions_event_preserves_multiple_hooks() {
|
||||
let parsed = toml::from_str::<HookEventsToml>(
|
||||
r#"
|
||||
[[UserInstructions]]
|
||||
type = "command"
|
||||
command = "python3 /tmp/first.py"
|
||||
|
||||
[[UserInstructions]]
|
||||
type = "command"
|
||||
command = "python3 /tmp/second.py"
|
||||
"#,
|
||||
)
|
||||
.expect("multiple UserInstructions hooks should deserialize");
|
||||
|
||||
assert_eq!(
|
||||
parsed.user_instructions,
|
||||
vec![
|
||||
HookHandlerConfig::Command {
|
||||
command: "python3 /tmp/first.py".to_string(),
|
||||
command_windows: None,
|
||||
timeout_sec: None,
|
||||
r#async: false,
|
||||
status_message: None,
|
||||
},
|
||||
HookHandlerConfig::Command {
|
||||
command: "python3 /tmp/second.py".to_string(),
|
||||
command_windows: None,
|
||||
timeout_sec: None,
|
||||
r#async: false,
|
||||
status_message: None,
|
||||
},
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hooks_toml_deserializes_inline_events_and_state_map() {
|
||||
let parsed: HooksToml = toml::from_str(
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Hook events are append-only across requirements layers. The managed hook
|
||||
//! directory is different: only one directory is usable on a given platform, so
|
||||
//! conflicting values for the active platform fail closed. The inactive platform
|
||||
//! field is first-filled to allow the same layer stack to carry OS-specific
|
||||
//! directories.
|
||||
//! directory is a singleton: only one directory is usable on a given platform,
|
||||
//! so conflicting values for the active platform fail closed.
|
||||
//! The inactive platform field is first-filled to allow the same layer stack to
|
||||
//! carry OS-specific directories.
|
||||
|
||||
use crate::HookEventsToml;
|
||||
use crate::ManagedHooksRequirementsToml;
|
||||
@@ -210,12 +210,12 @@ fn append_hook_events(existing: &mut HookEventsToml, incoming: HookEventsToml) -
|
||||
pre_compact,
|
||||
post_compact,
|
||||
session_start,
|
||||
user_instructions,
|
||||
user_prompt_submit,
|
||||
subagent_start,
|
||||
subagent_stop,
|
||||
stop,
|
||||
} = incoming;
|
||||
|
||||
let mut changed = false;
|
||||
changed |= append_vec(&mut existing.pre_tool_use, pre_tool_use);
|
||||
changed |= append_vec(&mut existing.permission_request, permission_request);
|
||||
@@ -223,6 +223,7 @@ fn append_hook_events(existing: &mut HookEventsToml, incoming: HookEventsToml) -
|
||||
changed |= append_vec(&mut existing.pre_compact, pre_compact);
|
||||
changed |= append_vec(&mut existing.post_compact, post_compact);
|
||||
changed |= append_vec(&mut existing.session_start, session_start);
|
||||
changed |= append_vec(&mut existing.user_instructions, user_instructions);
|
||||
changed |= append_vec(&mut existing.user_prompt_submit, user_prompt_submit);
|
||||
changed |= append_vec(&mut existing.subagent_start, subagent_start);
|
||||
changed |= append_vec(&mut existing.subagent_stop, subagent_stop);
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
//! - `remote_sandbox_config` is evaluated within each layer before merging.
|
||||
//! - `rules.prefix_rules` append high-priority rules first.
|
||||
//! - `hooks` append high-priority event groups first while failing closed on
|
||||
//! active managed-dir conflicts.
|
||||
//! active managed-dir and `UserInstructions` conflicts.
|
||||
//! - `permissions.filesystem.deny_read` is a high-priority-first union across
|
||||
//! layers.
|
||||
|
||||
|
||||
@@ -802,6 +802,50 @@ managed_dir = "/managed/high"
|
||||
assert!(err.to_string().contains("Low (req_low)"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn user_instructions_hooks_append_across_requirements_layers() {
|
||||
let composed = compose_with_hook_directory_field(
|
||||
vec![
|
||||
layer(
|
||||
"req_low",
|
||||
"Low",
|
||||
r#"
|
||||
[[hooks.UserInstructions]]
|
||||
type = "command"
|
||||
command = "low"
|
||||
"#,
|
||||
),
|
||||
layer(
|
||||
"req_high",
|
||||
"High",
|
||||
r#"
|
||||
[[hooks.UserInstructions]]
|
||||
type = "command"
|
||||
command = "high"
|
||||
"#,
|
||||
),
|
||||
],
|
||||
HookDirectoryField::ManagedDir,
|
||||
)
|
||||
.expect("UserInstructions hooks should compose")
|
||||
.expect("requirements present");
|
||||
|
||||
assert_eq!(
|
||||
composed,
|
||||
expected_requirements(
|
||||
r#"
|
||||
[[hooks.UserInstructions]]
|
||||
type = "command"
|
||||
command = "high"
|
||||
|
||||
[[hooks.UserInstructions]]
|
||||
type = "command"
|
||||
command = "low"
|
||||
"#
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn active_windows_managed_dir_conflicts_fail_closed() {
|
||||
let err = compose_with_hook_directory_field(
|
||||
|
||||
@@ -118,7 +118,7 @@ enum RawPluginManifestMcpServers {
|
||||
enum RawPluginManifestHooks {
|
||||
Path(String),
|
||||
Paths(Vec<String>),
|
||||
Inline(HooksFile),
|
||||
Inline(Box<HooksFile>),
|
||||
InlineList(Vec<HooksFile>),
|
||||
Invalid(JsonValue),
|
||||
}
|
||||
@@ -285,7 +285,7 @@ fn resolve_manifest_hooks(
|
||||
}
|
||||
RawPluginManifestHooks::Inline(hooks) => {
|
||||
Some(codex_plugin::manifest::PluginManifestHooks::Inline(vec![
|
||||
hooks,
|
||||
*hooks,
|
||||
]))
|
||||
}
|
||||
RawPluginManifestHooks::InlineList(hooks) => (!hooks.is_empty())
|
||||
|
||||
@@ -1310,6 +1310,13 @@
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"UserInstructions": {
|
||||
"default": [],
|
||||
"items": {
|
||||
"$ref": "#/definitions/HookHandlerConfig"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"UserPromptSubmit": {
|
||||
"default": [],
|
||||
"items": {
|
||||
|
||||
Reference in New Issue
Block a user