mirror of
https://github.com/openai/codex.git
synced 2026-05-29 23:40:29 +00:00
Tighten hook output event schemas (#24962)
# Why Fixes #23993. Hook command output schemas are published as the contract for hook authors and schema-driven tooling. The event-specific output schemas previously described `hookSpecificOutput.hookEventName` as the global `HookEventNameWire` enum, so a `pre-tool-use.command.output` schema would validate mismatched values like `PostToolUse`. That made the schemas less precise than the intended event-specific contract. # What Constrain each hook-specific output schema to the matching literal `hookEventName` value, mirroring the existing input-schema shape. Also split `SubagentStartHookSpecificOutputWire` from the session-start output wire so `subagent-start.command.output.schema.json` can emit `const: "SubagentStart"` instead of sharing the session-start definition. # Verification - `cargo nextest run -p codex-hooks` - `just fix -p codex-hooks` - `just argument-comment-lint -p codex-hooks -- --all-targets`
This commit is contained in:
@@ -2,21 +2,6 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"HookEventNameWire": {
|
||||
"enum": [
|
||||
"PreToolUse",
|
||||
"PermissionRequest",
|
||||
"PostToolUse",
|
||||
"PreCompact",
|
||||
"PostCompact",
|
||||
"SessionStart",
|
||||
"UserPromptSubmit",
|
||||
"SubagentStart",
|
||||
"SubagentStop",
|
||||
"Stop"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PermissionRequestBehaviorWire": {
|
||||
"enum": [
|
||||
"allow",
|
||||
@@ -65,7 +50,8 @@
|
||||
"default": null
|
||||
},
|
||||
"hookEventName": {
|
||||
"$ref": "#/definitions/HookEventNameWire"
|
||||
"const": "PermissionRequest",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -8,21 +8,6 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"HookEventNameWire": {
|
||||
"enum": [
|
||||
"PreToolUse",
|
||||
"PermissionRequest",
|
||||
"PostToolUse",
|
||||
"PreCompact",
|
||||
"PostCompact",
|
||||
"SessionStart",
|
||||
"UserPromptSubmit",
|
||||
"SubagentStart",
|
||||
"SubagentStop",
|
||||
"Stop"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PostToolUseHookSpecificOutputWire": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -31,7 +16,8 @@
|
||||
"type": "string"
|
||||
},
|
||||
"hookEventName": {
|
||||
"$ref": "#/definitions/HookEventNameWire"
|
||||
"const": "PostToolUse",
|
||||
"type": "string"
|
||||
},
|
||||
"updatedMCPToolOutput": {
|
||||
"default": null
|
||||
|
||||
@@ -2,21 +2,6 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"HookEventNameWire": {
|
||||
"enum": [
|
||||
"PreToolUse",
|
||||
"PermissionRequest",
|
||||
"PostToolUse",
|
||||
"PreCompact",
|
||||
"PostCompact",
|
||||
"SessionStart",
|
||||
"UserPromptSubmit",
|
||||
"SubagentStart",
|
||||
"SubagentStop",
|
||||
"Stop"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"PreToolUseDecisionWire": {
|
||||
"enum": [
|
||||
"approve",
|
||||
@@ -32,7 +17,8 @@
|
||||
"type": "string"
|
||||
},
|
||||
"hookEventName": {
|
||||
"$ref": "#/definitions/HookEventNameWire"
|
||||
"const": "PreToolUse",
|
||||
"type": "string"
|
||||
},
|
||||
"permissionDecision": {
|
||||
"allOf": [
|
||||
|
||||
@@ -2,21 +2,6 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"HookEventNameWire": {
|
||||
"enum": [
|
||||
"PreToolUse",
|
||||
"PermissionRequest",
|
||||
"PostToolUse",
|
||||
"PreCompact",
|
||||
"PostCompact",
|
||||
"SessionStart",
|
||||
"UserPromptSubmit",
|
||||
"SubagentStart",
|
||||
"SubagentStop",
|
||||
"Stop"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"SessionStartHookSpecificOutputWire": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -25,7 +10,8 @@
|
||||
"type": "string"
|
||||
},
|
||||
"hookEventName": {
|
||||
"$ref": "#/definitions/HookEventNameWire"
|
||||
"const": "SessionStart",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -2,22 +2,7 @@
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"additionalProperties": false,
|
||||
"definitions": {
|
||||
"HookEventNameWire": {
|
||||
"enum": [
|
||||
"PreToolUse",
|
||||
"PermissionRequest",
|
||||
"PostToolUse",
|
||||
"PreCompact",
|
||||
"PostCompact",
|
||||
"SessionStart",
|
||||
"UserPromptSubmit",
|
||||
"SubagentStart",
|
||||
"SubagentStop",
|
||||
"Stop"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"SessionStartHookSpecificOutputWire": {
|
||||
"SubagentStartHookSpecificOutputWire": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
"additionalContext": {
|
||||
@@ -25,7 +10,8 @@
|
||||
"type": "string"
|
||||
},
|
||||
"hookEventName": {
|
||||
"$ref": "#/definitions/HookEventNameWire"
|
||||
"const": "SubagentStart",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
@@ -42,7 +28,7 @@
|
||||
"hookSpecificOutput": {
|
||||
"allOf": [
|
||||
{
|
||||
"$ref": "#/definitions/SessionStartHookSpecificOutputWire"
|
||||
"$ref": "#/definitions/SubagentStartHookSpecificOutputWire"
|
||||
}
|
||||
],
|
||||
"default": null
|
||||
|
||||
@@ -8,21 +8,6 @@
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"HookEventNameWire": {
|
||||
"enum": [
|
||||
"PreToolUse",
|
||||
"PermissionRequest",
|
||||
"PostToolUse",
|
||||
"PreCompact",
|
||||
"PostCompact",
|
||||
"SessionStart",
|
||||
"UserPromptSubmit",
|
||||
"SubagentStart",
|
||||
"SubagentStop",
|
||||
"Stop"
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"UserPromptSubmitHookSpecificOutputWire": {
|
||||
"additionalProperties": false,
|
||||
"properties": {
|
||||
@@ -31,7 +16,8 @@
|
||||
"type": "string"
|
||||
},
|
||||
"hookEventName": {
|
||||
"$ref": "#/definitions/HookEventNameWire"
|
||||
"const": "UserPromptSubmit",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
|
||||
@@ -94,7 +94,8 @@ pub(crate) fn parse_session_start(stdout: &str) -> Option<SessionStartOutput> {
|
||||
let wire: SessionStartCommandOutputWire = parse_json(stdout)?;
|
||||
Some(session_start_output(
|
||||
wire.universal,
|
||||
wire.hook_specific_output,
|
||||
wire.hook_specific_output
|
||||
.and_then(|output| output.additional_context),
|
||||
))
|
||||
}
|
||||
|
||||
@@ -102,15 +103,15 @@ pub(crate) fn parse_subagent_start(stdout: &str) -> Option<SessionStartOutput> {
|
||||
let wire: SubagentStartCommandOutputWire = parse_json(stdout)?;
|
||||
Some(session_start_output(
|
||||
wire.universal,
|
||||
wire.hook_specific_output,
|
||||
wire.hook_specific_output
|
||||
.and_then(|output| output.additional_context),
|
||||
))
|
||||
}
|
||||
|
||||
fn session_start_output(
|
||||
universal: HookUniversalOutputWire,
|
||||
hook_specific_output: Option<crate::schema::SessionStartHookSpecificOutputWire>,
|
||||
additional_context: Option<String>,
|
||||
) -> SessionStartOutput {
|
||||
let additional_context = hook_specific_output.and_then(|output| output.additional_context);
|
||||
SessionStartOutput {
|
||||
universal: UniversalOutput::from(universal),
|
||||
additional_context,
|
||||
|
||||
@@ -182,6 +182,7 @@ pub(crate) struct PostCompactCommandOutputWire {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub(crate) struct PermissionRequestHookSpecificOutputWire {
|
||||
#[schemars(schema_with = "permission_request_hook_event_name_schema")]
|
||||
pub hook_event_name: HookEventNameWire,
|
||||
#[serde(default)]
|
||||
pub decision: Option<PermissionRequestDecisionWire>,
|
||||
@@ -223,6 +224,7 @@ pub(crate) enum PermissionRequestBehaviorWire {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub(crate) struct PostToolUseHookSpecificOutputWire {
|
||||
#[schemars(schema_with = "post_tool_use_hook_event_name_schema")]
|
||||
pub hook_event_name: HookEventNameWire,
|
||||
#[serde(default)]
|
||||
pub additional_context: Option<String>,
|
||||
@@ -235,6 +237,7 @@ pub(crate) struct PostToolUseHookSpecificOutputWire {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub(crate) struct PreToolUseHookSpecificOutputWire {
|
||||
#[schemars(schema_with = "pre_tool_use_hook_event_name_schema")]
|
||||
pub hook_event_name: HookEventNameWire,
|
||||
#[serde(default)]
|
||||
pub permission_decision: Option<PreToolUsePermissionDecisionWire>,
|
||||
@@ -388,6 +391,7 @@ pub(crate) struct SessionStartCommandOutputWire {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub(crate) struct SessionStartHookSpecificOutputWire {
|
||||
#[schemars(schema_with = "session_start_hook_event_name_schema")]
|
||||
pub hook_event_name: HookEventNameWire,
|
||||
#[serde(default)]
|
||||
pub additional_context: Option<String>,
|
||||
@@ -401,7 +405,17 @@ pub(crate) struct SubagentStartCommandOutputWire {
|
||||
#[serde(flatten)]
|
||||
pub universal: HookUniversalOutputWire,
|
||||
#[serde(default)]
|
||||
pub hook_specific_output: Option<SessionStartHookSpecificOutputWire>,
|
||||
pub hook_specific_output: Option<SubagentStartHookSpecificOutputWire>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub(crate) struct SubagentStartHookSpecificOutputWire {
|
||||
#[schemars(schema_with = "subagent_start_hook_event_name_schema")]
|
||||
pub hook_event_name: HookEventNameWire,
|
||||
#[serde(default)]
|
||||
pub additional_context: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -423,6 +437,7 @@ pub(crate) struct UserPromptSubmitCommandOutputWire {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub(crate) struct UserPromptSubmitHookSpecificOutputWire {
|
||||
#[schemars(schema_with = "user_prompt_submit_hook_event_name_schema")]
|
||||
pub hook_event_name: HookEventNameWire,
|
||||
#[serde(default)]
|
||||
pub additional_context: Option<String>,
|
||||
@@ -817,10 +832,13 @@ mod tests {
|
||||
use super::PRE_TOOL_USE_INPUT_FIXTURE;
|
||||
use super::PRE_TOOL_USE_OUTPUT_FIXTURE;
|
||||
use super::PermissionRequestCommandInput;
|
||||
use super::PermissionRequestCommandOutputWire;
|
||||
use super::PostCompactCommandInput;
|
||||
use super::PostToolUseCommandInput;
|
||||
use super::PostToolUseCommandOutputWire;
|
||||
use super::PreCompactCommandInput;
|
||||
use super::PreToolUseCommandInput;
|
||||
use super::PreToolUseCommandOutputWire;
|
||||
use super::SESSION_START_INPUT_FIXTURE;
|
||||
use super::SESSION_START_OUTPUT_FIXTURE;
|
||||
use super::STOP_INPUT_FIXTURE;
|
||||
@@ -829,17 +847,21 @@ mod tests {
|
||||
use super::SUBAGENT_START_OUTPUT_FIXTURE;
|
||||
use super::SUBAGENT_STOP_INPUT_FIXTURE;
|
||||
use super::SUBAGENT_STOP_OUTPUT_FIXTURE;
|
||||
use super::SessionStartCommandOutputWire;
|
||||
use super::StopCommandInput;
|
||||
use super::SubagentCommandInputFields;
|
||||
use super::SubagentStartCommandInput;
|
||||
use super::SubagentStartCommandOutputWire;
|
||||
use super::SubagentStopCommandInput;
|
||||
use super::USER_PROMPT_SUBMIT_INPUT_FIXTURE;
|
||||
use super::USER_PROMPT_SUBMIT_OUTPUT_FIXTURE;
|
||||
use super::UserPromptSubmitCommandInput;
|
||||
use super::UserPromptSubmitCommandOutputWire;
|
||||
use super::schema_json;
|
||||
use super::write_schema_fixtures;
|
||||
use crate::events::common::SubagentHookContext;
|
||||
use pretty_assertions::assert_eq;
|
||||
use schemars::JsonSchema;
|
||||
use serde_json::Value;
|
||||
use serde_json::json;
|
||||
use tempfile::TempDir;
|
||||
@@ -914,6 +936,20 @@ mod tests {
|
||||
value.replace("\r\n", "\n")
|
||||
}
|
||||
|
||||
fn assert_output_hook_event_name_const<T: JsonSchema>(definition: &str, expected: &str) {
|
||||
let schema: Value =
|
||||
serde_json::from_slice(&schema_json::<T>().expect("serialize hook output schema"))
|
||||
.expect("parse hook output schema");
|
||||
|
||||
assert_eq!(
|
||||
schema["definitions"][definition]["properties"]["hookEventName"],
|
||||
json!({
|
||||
"const": expected,
|
||||
"type": "string",
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generated_hook_schemas_match_fixtures() {
|
||||
let temp_dir = TempDir::new().expect("create temp dir");
|
||||
@@ -950,6 +986,34 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hook_specific_output_event_names_are_event_specific_in_output_schemas() {
|
||||
assert_output_hook_event_name_const::<PermissionRequestCommandOutputWire>(
|
||||
"PermissionRequestHookSpecificOutputWire",
|
||||
"PermissionRequest",
|
||||
);
|
||||
assert_output_hook_event_name_const::<PostToolUseCommandOutputWire>(
|
||||
"PostToolUseHookSpecificOutputWire",
|
||||
"PostToolUse",
|
||||
);
|
||||
assert_output_hook_event_name_const::<PreToolUseCommandOutputWire>(
|
||||
"PreToolUseHookSpecificOutputWire",
|
||||
"PreToolUse",
|
||||
);
|
||||
assert_output_hook_event_name_const::<SessionStartCommandOutputWire>(
|
||||
"SessionStartHookSpecificOutputWire",
|
||||
"SessionStart",
|
||||
);
|
||||
assert_output_hook_event_name_const::<SubagentStartCommandOutputWire>(
|
||||
"SubagentStartHookSpecificOutputWire",
|
||||
"SubagentStart",
|
||||
);
|
||||
assert_output_hook_event_name_const::<UserPromptSubmitCommandOutputWire>(
|
||||
"UserPromptSubmitHookSpecificOutputWire",
|
||||
"UserPromptSubmit",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn turn_scoped_hook_inputs_include_codex_turn_id_extension() {
|
||||
// Codex intentionally diverges from Claude's public hook docs here so
|
||||
|
||||
Reference in New Issue
Block a user