use schemars::JsonSchema; use schemars::r#gen::SchemaGenerator; use schemars::r#gen::SchemaSettings; use schemars::schema::InstanceType; use schemars::schema::RootSchema; use schemars::schema::Schema; use schemars::schema::SchemaObject; use serde::Deserialize; use serde::Serialize; use serde_json::Map; use serde_json::Value; use std::path::Path; use std::path::PathBuf; use crate::events::common::SubagentHookContext; const GENERATED_DIR: &str = "generated"; const POST_TOOL_USE_INPUT_FIXTURE: &str = "post-tool-use.command.input.schema.json"; const POST_TOOL_USE_OUTPUT_FIXTURE: &str = "post-tool-use.command.output.schema.json"; const PERMISSION_REQUEST_INPUT_FIXTURE: &str = "permission-request.command.input.schema.json"; const PERMISSION_REQUEST_OUTPUT_FIXTURE: &str = "permission-request.command.output.schema.json"; const POST_COMPACT_INPUT_FIXTURE: &str = "post-compact.command.input.schema.json"; const POST_COMPACT_OUTPUT_FIXTURE: &str = "post-compact.command.output.schema.json"; const PRE_TOOL_USE_INPUT_FIXTURE: &str = "pre-tool-use.command.input.schema.json"; const PRE_TOOL_USE_OUTPUT_FIXTURE: &str = "pre-tool-use.command.output.schema.json"; const PRE_COMPACT_INPUT_FIXTURE: &str = "pre-compact.command.input.schema.json"; const PRE_COMPACT_OUTPUT_FIXTURE: &str = "pre-compact.command.output.schema.json"; const SESSION_START_INPUT_FIXTURE: &str = "session-start.command.input.schema.json"; const SESSION_START_OUTPUT_FIXTURE: &str = "session-start.command.output.schema.json"; const USER_PROMPT_SUBMIT_INPUT_FIXTURE: &str = "user-prompt-submit.command.input.schema.json"; const USER_PROMPT_SUBMIT_OUTPUT_FIXTURE: &str = "user-prompt-submit.command.output.schema.json"; const SUBAGENT_START_INPUT_FIXTURE: &str = "subagent-start.command.input.schema.json"; const SUBAGENT_START_OUTPUT_FIXTURE: &str = "subagent-start.command.output.schema.json"; const SUBAGENT_STOP_INPUT_FIXTURE: &str = "subagent-stop.command.input.schema.json"; const SUBAGENT_STOP_OUTPUT_FIXTURE: &str = "subagent-stop.command.output.schema.json"; const STOP_INPUT_FIXTURE: &str = "stop.command.input.schema.json"; const STOP_OUTPUT_FIXTURE: &str = "stop.command.output.schema.json"; #[derive(Debug, Clone, Serialize)] #[serde(transparent)] pub(crate) struct NullableString(Option); impl NullableString { pub(crate) fn from_path(path: Option) -> Self { Self(path.map(|path| path.display().to_string())) } pub(crate) fn from_string(value: Option) -> Self { Self(value) } } impl JsonSchema for NullableString { fn schema_name() -> String { "NullableString".to_string() } fn json_schema(_gen: &mut SchemaGenerator) -> Schema { Schema::Object(SchemaObject { instance_type: Some(vec![InstanceType::String, InstanceType::Null].into()), ..Default::default() }) } } #[derive(Debug, Clone, Default)] pub(crate) struct SubagentCommandInputFields { pub agent_id: Option, pub agent_type: Option, } impl From> for SubagentCommandInputFields { fn from(value: Option<&SubagentHookContext>) -> Self { match value { Some(context) => Self { agent_id: Some(context.agent_id.clone()), agent_type: Some(context.agent_type.clone()), }, None => Self::default(), } } } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] #[serde(deny_unknown_fields)] pub(crate) struct HookUniversalOutputWire { #[serde(default = "default_continue")] pub r#continue: bool, #[serde(default)] pub stop_reason: Option, #[serde(default)] pub suppress_output: bool, #[serde(default)] pub system_message: Option, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub(crate) enum HookEventNameWire { #[serde(rename = "PreToolUse")] PreToolUse, #[serde(rename = "PermissionRequest")] PermissionRequest, #[serde(rename = "PostToolUse")] PostToolUse, #[serde(rename = "PreCompact")] PreCompact, #[serde(rename = "PostCompact")] PostCompact, #[serde(rename = "SessionStart")] SessionStart, #[serde(rename = "UserPromptSubmit")] UserPromptSubmit, #[serde(rename = "SubagentStart")] SubagentStart, #[serde(rename = "SubagentStop")] SubagentStop, #[serde(rename = "Stop")] Stop, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] #[serde(deny_unknown_fields)] #[schemars(rename = "pre-tool-use.command.output")] pub(crate) struct PreToolUseCommandOutputWire { #[serde(flatten)] pub universal: HookUniversalOutputWire, #[serde(default)] pub decision: Option, #[serde(default)] pub reason: Option, #[serde(default)] pub hook_specific_output: Option, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] #[serde(deny_unknown_fields)] #[schemars(rename = "post-tool-use.command.output")] pub(crate) struct PostToolUseCommandOutputWire { #[serde(flatten)] pub universal: HookUniversalOutputWire, #[serde(default)] pub decision: Option, #[serde(default)] pub reason: Option, #[serde(default)] pub hook_specific_output: Option, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] #[serde(deny_unknown_fields)] #[schemars(rename = "permission-request.command.output")] pub(crate) struct PermissionRequestCommandOutputWire { #[serde(flatten)] pub universal: HookUniversalOutputWire, #[serde(default)] pub hook_specific_output: Option, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] #[serde(deny_unknown_fields)] #[schemars(rename = "pre-compact.command.output")] pub(crate) struct PreCompactCommandOutputWire { #[serde(flatten)] pub universal: HookUniversalOutputWire, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] #[serde(deny_unknown_fields)] #[schemars(rename = "post-compact.command.output")] pub(crate) struct PostCompactCommandOutputWire { #[serde(flatten)] pub universal: HookUniversalOutputWire, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[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, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] #[serde(deny_unknown_fields)] pub(crate) struct PermissionRequestDecisionWire { pub behavior: PermissionRequestBehaviorWire, /// Reserved for a future input-rewrite capability. /// /// PermissionRequest hooks currently fail closed if this field is present. #[serde(default)] pub updated_input: Option, /// Reserved for a future permission-rewrite capability. /// /// PermissionRequest hooks currently fail closed if this field is present. #[serde(default)] pub updated_permissions: Option, #[serde(default)] pub message: Option, /// Reserved for future short-circuiting semantics. /// /// PermissionRequest hooks currently fail closed if this field is `true`. #[serde(default)] pub interrupt: bool, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub(crate) enum PermissionRequestBehaviorWire { #[serde(rename = "allow")] Allow, #[serde(rename = "deny")] Deny, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[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, #[serde(default)] #[serde(rename = "updatedMCPToolOutput")] pub updated_mcp_tool_output: Option, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[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, #[serde(default)] pub permission_decision_reason: Option, #[serde(default)] pub updated_input: Option, #[serde(default)] pub additional_context: Option, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub(crate) enum PreToolUsePermissionDecisionWire { #[serde(rename = "allow")] Allow, #[serde(rename = "deny")] Deny, #[serde(rename = "ask")] Ask, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub(crate) enum PreToolUseDecisionWire { #[serde(rename = "approve")] Approve, #[serde(rename = "block")] Block, } #[derive(Debug, Clone, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] #[schemars(rename = "pre-tool-use.command.input")] pub(crate) struct PreToolUseCommandInput { pub session_id: String, /// Codex extension: expose the active turn id to internal turn-scoped hooks. pub turn_id: String, #[serde(skip_serializing_if = "Option::is_none")] pub agent_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub agent_type: Option, pub transcript_path: NullableString, pub cwd: String, #[schemars(schema_with = "pre_tool_use_hook_event_name_schema")] pub hook_event_name: String, pub model: String, #[schemars(schema_with = "permission_mode_schema")] pub permission_mode: String, pub tool_name: String, pub tool_input: Value, pub tool_use_id: String, } #[derive(Debug, Clone, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] #[schemars(rename = "permission-request.command.input")] pub(crate) struct PermissionRequestCommandInput { pub session_id: String, /// Codex extension: expose the active turn id to internal turn-scoped hooks. pub turn_id: String, #[serde(skip_serializing_if = "Option::is_none")] pub agent_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub agent_type: Option, pub transcript_path: NullableString, pub cwd: String, #[schemars(schema_with = "permission_request_hook_event_name_schema")] pub hook_event_name: String, pub model: String, #[schemars(schema_with = "permission_mode_schema")] pub permission_mode: String, pub tool_name: String, pub tool_input: Value, } #[derive(Debug, Clone, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] #[schemars(rename = "post-tool-use.command.input")] pub(crate) struct PostToolUseCommandInput { pub session_id: String, /// Codex extension: expose the active turn id to internal turn-scoped hooks. pub turn_id: String, #[serde(skip_serializing_if = "Option::is_none")] pub agent_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub agent_type: Option, pub transcript_path: NullableString, pub cwd: String, #[schemars(schema_with = "post_tool_use_hook_event_name_schema")] pub hook_event_name: String, pub model: String, #[schemars(schema_with = "permission_mode_schema")] pub permission_mode: String, pub tool_name: String, pub tool_input: Value, pub tool_response: Value, pub tool_use_id: String, } #[derive(Debug, Clone, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] #[schemars(rename = "pre-compact.command.input")] pub(crate) struct PreCompactCommandInput { pub session_id: String, /// Codex extension: expose the active turn id to internal turn-scoped hooks. pub turn_id: String, #[serde(skip_serializing_if = "Option::is_none")] pub agent_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub agent_type: Option, pub transcript_path: NullableString, pub cwd: String, #[schemars(schema_with = "pre_compact_hook_event_name_schema")] pub hook_event_name: String, pub model: String, #[schemars(schema_with = "compaction_trigger_schema")] pub trigger: String, } #[derive(Debug, Clone, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] #[schemars(rename = "post-compact.command.input")] pub(crate) struct PostCompactCommandInput { pub session_id: String, /// Codex extension: expose the active turn id to internal turn-scoped hooks. pub turn_id: String, #[serde(skip_serializing_if = "Option::is_none")] pub agent_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub agent_type: Option, pub transcript_path: NullableString, pub cwd: String, #[schemars(schema_with = "post_compact_hook_event_name_schema")] pub hook_event_name: String, pub model: String, #[schemars(schema_with = "compaction_trigger_schema")] pub trigger: String, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] #[serde(deny_unknown_fields)] #[schemars(rename = "session-start.command.output")] pub(crate) struct SessionStartCommandOutputWire { #[serde(flatten)] pub universal: HookUniversalOutputWire, #[serde(default)] pub hook_specific_output: Option, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[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, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] #[serde(deny_unknown_fields)] #[schemars(rename = "subagent-start.command.output")] pub(crate) struct SubagentStartCommandOutputWire { #[serde(flatten)] pub universal: HookUniversalOutputWire, #[serde(default)] pub hook_specific_output: Option, } #[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, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] #[serde(deny_unknown_fields)] #[schemars(rename = "user-prompt-submit.command.output")] pub(crate) struct UserPromptSubmitCommandOutputWire { #[serde(flatten)] pub universal: HookUniversalOutputWire, #[serde(default)] pub decision: Option, #[serde(default)] pub reason: Option, #[serde(default)] pub hook_specific_output: Option, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[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, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] #[serde(deny_unknown_fields)] #[schemars(rename = "stop.command.output")] pub(crate) struct StopCommandOutputWire { #[serde(flatten)] pub universal: HookUniversalOutputWire, #[serde(default)] pub decision: Option, /// Claude requires `reason` when `decision` is `block`; we enforce that /// semantic rule during output parsing rather than in the JSON schema. #[serde(default)] pub reason: Option, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)] #[serde(rename_all = "camelCase")] #[serde(deny_unknown_fields)] #[schemars(rename = "subagent-stop.command.output")] pub(crate) struct SubagentStopCommandOutputWire { #[serde(flatten)] pub universal: HookUniversalOutputWire, #[serde(default)] pub decision: Option, /// Claude requires `reason` when `decision` is `block`; we enforce that /// semantic rule during output parsing rather than in the JSON schema. #[serde(default)] pub reason: Option, } #[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] pub(crate) enum BlockDecisionWire { #[serde(rename = "block")] Block, } #[derive(Debug, Clone, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] #[schemars(rename = "session-start.command.input")] pub(crate) struct SessionStartCommandInput { pub session_id: String, pub transcript_path: NullableString, pub cwd: String, #[schemars(schema_with = "session_start_hook_event_name_schema")] pub hook_event_name: String, pub model: String, #[schemars(schema_with = "permission_mode_schema")] pub permission_mode: String, #[schemars(schema_with = "session_start_source_schema")] pub source: String, } impl SessionStartCommandInput { pub(crate) fn new( session_id: impl Into, transcript_path: Option, cwd: impl Into, model: impl Into, permission_mode: impl Into, source: impl Into, ) -> Self { Self { session_id: session_id.into(), transcript_path: NullableString::from_path(transcript_path), cwd: cwd.into(), hook_event_name: "SessionStart".to_string(), model: model.into(), permission_mode: permission_mode.into(), source: source.into(), } } } #[derive(Debug, Clone, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] #[schemars(rename = "subagent-start.command.input")] pub(crate) struct SubagentStartCommandInput { pub session_id: String, /// Codex extension: expose the active turn id to internal turn-scoped hooks. pub turn_id: String, pub transcript_path: NullableString, pub cwd: String, #[schemars(schema_with = "subagent_start_hook_event_name_schema")] pub hook_event_name: String, pub model: String, #[schemars(schema_with = "permission_mode_schema")] pub permission_mode: String, pub agent_id: String, pub agent_type: String, } #[derive(Debug, Clone, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] #[schemars(rename = "user-prompt-submit.command.input")] pub(crate) struct UserPromptSubmitCommandInput { pub session_id: String, /// Codex extension: expose the active turn id to internal turn-scoped hooks. pub turn_id: String, #[serde(skip_serializing_if = "Option::is_none")] pub agent_id: Option, #[serde(skip_serializing_if = "Option::is_none")] pub agent_type: Option, pub transcript_path: NullableString, pub cwd: String, #[schemars(schema_with = "user_prompt_submit_hook_event_name_schema")] pub hook_event_name: String, pub model: String, #[schemars(schema_with = "permission_mode_schema")] pub permission_mode: String, pub prompt: String, } #[derive(Debug, Clone, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] #[schemars(rename = "stop.command.input")] pub(crate) struct StopCommandInput { pub session_id: String, /// Codex extension: expose the active turn id to internal turn-scoped hooks. pub turn_id: String, pub transcript_path: NullableString, pub cwd: String, #[schemars(schema_with = "stop_hook_event_name_schema")] pub hook_event_name: String, pub model: String, #[schemars(schema_with = "permission_mode_schema")] pub permission_mode: String, pub stop_hook_active: bool, pub last_assistant_message: NullableString, } #[derive(Debug, Clone, Serialize, JsonSchema)] #[serde(deny_unknown_fields)] #[schemars(rename = "subagent-stop.command.input")] pub(crate) struct SubagentStopCommandInput { pub session_id: String, /// Codex extension: expose the active turn id to internal turn-scoped hooks. pub turn_id: String, pub transcript_path: NullableString, pub agent_transcript_path: NullableString, pub cwd: String, #[schemars(schema_with = "subagent_stop_hook_event_name_schema")] pub hook_event_name: String, pub model: String, #[schemars(schema_with = "permission_mode_schema")] pub permission_mode: String, pub stop_hook_active: bool, pub agent_id: String, pub agent_type: String, pub last_assistant_message: NullableString, } pub fn write_schema_fixtures(schema_root: &Path) -> anyhow::Result<()> { let generated_dir = schema_root.join(GENERATED_DIR); ensure_empty_dir(&generated_dir)?; write_schema( &generated_dir.join(POST_TOOL_USE_INPUT_FIXTURE), schema_json::()?, )?; write_schema( &generated_dir.join(POST_TOOL_USE_OUTPUT_FIXTURE), schema_json::()?, )?; write_schema( &generated_dir.join(PERMISSION_REQUEST_INPUT_FIXTURE), schema_json::()?, )?; write_schema( &generated_dir.join(PERMISSION_REQUEST_OUTPUT_FIXTURE), schema_json::()?, )?; write_schema( &generated_dir.join(POST_COMPACT_INPUT_FIXTURE), schema_json::()?, )?; write_schema( &generated_dir.join(POST_COMPACT_OUTPUT_FIXTURE), schema_json::()?, )?; write_schema( &generated_dir.join(PRE_COMPACT_INPUT_FIXTURE), schema_json::()?, )?; write_schema( &generated_dir.join(PRE_COMPACT_OUTPUT_FIXTURE), schema_json::()?, )?; write_schema( &generated_dir.join(PRE_TOOL_USE_INPUT_FIXTURE), schema_json::()?, )?; write_schema( &generated_dir.join(PRE_TOOL_USE_OUTPUT_FIXTURE), schema_json::()?, )?; write_schema( &generated_dir.join(SESSION_START_INPUT_FIXTURE), schema_json::()?, )?; write_schema( &generated_dir.join(SESSION_START_OUTPUT_FIXTURE), schema_json::()?, )?; write_schema( &generated_dir.join(USER_PROMPT_SUBMIT_INPUT_FIXTURE), schema_json::()?, )?; write_schema( &generated_dir.join(USER_PROMPT_SUBMIT_OUTPUT_FIXTURE), schema_json::()?, )?; write_schema( &generated_dir.join(SUBAGENT_START_INPUT_FIXTURE), schema_json::()?, )?; write_schema( &generated_dir.join(SUBAGENT_START_OUTPUT_FIXTURE), schema_json::()?, )?; write_schema( &generated_dir.join(SUBAGENT_STOP_INPUT_FIXTURE), schema_json::()?, )?; write_schema( &generated_dir.join(SUBAGENT_STOP_OUTPUT_FIXTURE), schema_json::()?, )?; write_schema( &generated_dir.join(STOP_INPUT_FIXTURE), schema_json::()?, )?; write_schema( &generated_dir.join(STOP_OUTPUT_FIXTURE), schema_json::()?, )?; Ok(()) } fn write_schema(path: &Path, json: Vec) -> anyhow::Result<()> { std::fs::write(path, json)?; Ok(()) } fn ensure_empty_dir(dir: &Path) -> anyhow::Result<()> { if dir.exists() { std::fs::remove_dir_all(dir)?; } std::fs::create_dir_all(dir)?; Ok(()) } fn schema_json() -> anyhow::Result> where T: JsonSchema, { let schema = schema_for_type::(); let value = serde_json::to_value(schema)?; let value = canonicalize_json(&value); Ok(serde_json::to_vec_pretty(&value)?) } fn schema_for_type() -> RootSchema where T: JsonSchema, { SchemaSettings::draft07() .with(|settings| { settings.option_add_null_type = false; }) .into_generator() .into_root_schema_for::() } fn canonicalize_json(value: &Value) -> Value { match value { Value::Array(items) => Value::Array(items.iter().map(canonicalize_json).collect()), Value::Object(map) => { let mut entries: Vec<_> = map.iter().collect(); entries.sort_by_key(|(key, _)| *key); let mut sorted = Map::with_capacity(map.len()); for (key, child) in entries { sorted.insert(key.clone(), canonicalize_json(child)); } Value::Object(sorted) } _ => value.clone(), } } fn session_start_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema { string_const_schema("SessionStart") } fn post_tool_use_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema { string_const_schema("PostToolUse") } fn pre_compact_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema { string_const_schema("PreCompact") } fn post_compact_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema { string_const_schema("PostCompact") } fn pre_tool_use_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema { string_const_schema("PreToolUse") } fn permission_request_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema { string_const_schema("PermissionRequest") } fn user_prompt_submit_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema { string_const_schema("UserPromptSubmit") } fn subagent_start_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema { string_const_schema("SubagentStart") } fn subagent_stop_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema { string_const_schema("SubagentStop") } fn stop_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema { string_const_schema("Stop") } fn permission_mode_schema(_gen: &mut SchemaGenerator) -> Schema { string_enum_schema(&[ "default", "acceptEdits", "plan", "dontAsk", "bypassPermissions", ]) } fn session_start_source_schema(_gen: &mut SchemaGenerator) -> Schema { string_enum_schema(&["startup", "resume", "clear", "compact"]) } fn compaction_trigger_schema(_gen: &mut SchemaGenerator) -> Schema { string_enum_schema(&["manual", "auto"]) } fn string_const_schema(value: &str) -> Schema { let mut schema = SchemaObject { instance_type: Some(InstanceType::String.into()), ..Default::default() }; schema.const_value = Some(Value::String(value.to_string())); Schema::Object(schema) } fn string_enum_schema(values: &[&str]) -> Schema { let mut schema = SchemaObject { instance_type: Some(InstanceType::String.into()), ..Default::default() }; schema.enum_values = Some( values .iter() .map(|value| Value::String((*value).to_string())) .collect(), ); Schema::Object(schema) } fn default_continue() -> bool { true } #[cfg(test)] mod tests { use super::NullableString; use super::PERMISSION_REQUEST_INPUT_FIXTURE; use super::PERMISSION_REQUEST_OUTPUT_FIXTURE; use super::POST_COMPACT_INPUT_FIXTURE; use super::POST_COMPACT_OUTPUT_FIXTURE; use super::POST_TOOL_USE_INPUT_FIXTURE; use super::POST_TOOL_USE_OUTPUT_FIXTURE; use super::PRE_COMPACT_INPUT_FIXTURE; use super::PRE_COMPACT_OUTPUT_FIXTURE; 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; use super::STOP_OUTPUT_FIXTURE; use super::SUBAGENT_START_INPUT_FIXTURE; 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; fn expected_fixture(name: &str) -> &'static str { match name { POST_TOOL_USE_INPUT_FIXTURE => { include_str!("../schema/generated/post-tool-use.command.input.schema.json") } POST_TOOL_USE_OUTPUT_FIXTURE => { include_str!("../schema/generated/post-tool-use.command.output.schema.json") } PERMISSION_REQUEST_INPUT_FIXTURE => { include_str!("../schema/generated/permission-request.command.input.schema.json") } PERMISSION_REQUEST_OUTPUT_FIXTURE => { include_str!("../schema/generated/permission-request.command.output.schema.json") } POST_COMPACT_INPUT_FIXTURE => { include_str!("../schema/generated/post-compact.command.input.schema.json") } POST_COMPACT_OUTPUT_FIXTURE => { include_str!("../schema/generated/post-compact.command.output.schema.json") } PRE_COMPACT_INPUT_FIXTURE => { include_str!("../schema/generated/pre-compact.command.input.schema.json") } PRE_COMPACT_OUTPUT_FIXTURE => { include_str!("../schema/generated/pre-compact.command.output.schema.json") } PRE_TOOL_USE_INPUT_FIXTURE => { include_str!("../schema/generated/pre-tool-use.command.input.schema.json") } PRE_TOOL_USE_OUTPUT_FIXTURE => { include_str!("../schema/generated/pre-tool-use.command.output.schema.json") } SESSION_START_INPUT_FIXTURE => { include_str!("../schema/generated/session-start.command.input.schema.json") } SESSION_START_OUTPUT_FIXTURE => { include_str!("../schema/generated/session-start.command.output.schema.json") } USER_PROMPT_SUBMIT_INPUT_FIXTURE => { include_str!("../schema/generated/user-prompt-submit.command.input.schema.json") } USER_PROMPT_SUBMIT_OUTPUT_FIXTURE => { include_str!("../schema/generated/user-prompt-submit.command.output.schema.json") } SUBAGENT_START_INPUT_FIXTURE => { include_str!("../schema/generated/subagent-start.command.input.schema.json") } SUBAGENT_START_OUTPUT_FIXTURE => { include_str!("../schema/generated/subagent-start.command.output.schema.json") } SUBAGENT_STOP_INPUT_FIXTURE => { include_str!("../schema/generated/subagent-stop.command.input.schema.json") } SUBAGENT_STOP_OUTPUT_FIXTURE => { include_str!("../schema/generated/subagent-stop.command.output.schema.json") } STOP_INPUT_FIXTURE => { include_str!("../schema/generated/stop.command.input.schema.json") } STOP_OUTPUT_FIXTURE => { include_str!("../schema/generated/stop.command.output.schema.json") } _ => panic!("unexpected fixture name: {name}"), } } fn normalize_newlines(value: &str) -> String { value.replace("\r\n", "\n") } fn assert_output_hook_event_name_const(definition: &str, expected: &str) { let schema: Value = serde_json::from_slice(&schema_json::().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"); let schema_root = temp_dir.path().join("schema"); write_schema_fixtures(&schema_root).expect("write generated hook schemas"); for fixture in [ POST_TOOL_USE_INPUT_FIXTURE, POST_TOOL_USE_OUTPUT_FIXTURE, PERMISSION_REQUEST_INPUT_FIXTURE, PERMISSION_REQUEST_OUTPUT_FIXTURE, POST_COMPACT_INPUT_FIXTURE, POST_COMPACT_OUTPUT_FIXTURE, PRE_COMPACT_INPUT_FIXTURE, PRE_COMPACT_OUTPUT_FIXTURE, PRE_TOOL_USE_INPUT_FIXTURE, PRE_TOOL_USE_OUTPUT_FIXTURE, SESSION_START_INPUT_FIXTURE, SESSION_START_OUTPUT_FIXTURE, USER_PROMPT_SUBMIT_INPUT_FIXTURE, USER_PROMPT_SUBMIT_OUTPUT_FIXTURE, SUBAGENT_START_INPUT_FIXTURE, SUBAGENT_START_OUTPUT_FIXTURE, SUBAGENT_STOP_INPUT_FIXTURE, SUBAGENT_STOP_OUTPUT_FIXTURE, STOP_INPUT_FIXTURE, STOP_OUTPUT_FIXTURE, ] { let expected = normalize_newlines(expected_fixture(fixture)); let actual = std::fs::read_to_string(schema_root.join("generated").join(fixture)) .unwrap_or_else(|err| panic!("read generated schema {fixture}: {err}")); let actual = normalize_newlines(&actual); assert_eq!(expected, actual, "fixture should match generated schema"); } } #[test] fn hook_specific_output_event_names_are_event_specific_in_output_schemas() { assert_output_hook_event_name_const::( "PermissionRequestHookSpecificOutputWire", "PermissionRequest", ); assert_output_hook_event_name_const::( "PostToolUseHookSpecificOutputWire", "PostToolUse", ); assert_output_hook_event_name_const::( "PreToolUseHookSpecificOutputWire", "PreToolUse", ); assert_output_hook_event_name_const::( "SessionStartHookSpecificOutputWire", "SessionStart", ); assert_output_hook_event_name_const::( "SubagentStartHookSpecificOutputWire", "SubagentStart", ); assert_output_hook_event_name_const::( "UserPromptSubmitHookSpecificOutputWire", "UserPromptSubmit", ); } #[test] fn turn_scoped_hook_inputs_include_codex_turn_id_extension() { // Codex intentionally diverges from Claude's public hook docs here so // internal hook consumers can key off the active turn. let pre_tool_use: Value = serde_json::from_slice( &schema_json::().expect("serialize pre tool use input schema"), ) .expect("parse pre tool use input schema"); let post_tool_use: Value = serde_json::from_slice( &schema_json::() .expect("serialize post tool use input schema"), ) .expect("parse post tool use input schema"); let pre_compact: Value = serde_json::from_slice( &schema_json::().expect("serialize pre compact input schema"), ) .expect("parse pre compact input schema"); let post_compact: Value = serde_json::from_slice( &schema_json::().expect("serialize post compact input schema"), ) .expect("parse post compact input schema"); let permission_request: Value = serde_json::from_slice( &schema_json::() .expect("serialize permission request input schema"), ) .expect("parse permission request input schema"); let user_prompt_submit: Value = serde_json::from_slice( &schema_json::() .expect("serialize user prompt submit input schema"), ) .expect("parse user prompt submit input schema"); let subagent_start: Value = serde_json::from_slice( &schema_json::() .expect("serialize subagent start input schema"), ) .expect("parse subagent start input schema"); let subagent_stop: Value = serde_json::from_slice( &schema_json::() .expect("serialize subagent stop input schema"), ) .expect("parse subagent stop input schema"); let stop: Value = serde_json::from_slice( &schema_json::().expect("serialize stop input schema"), ) .expect("parse stop input schema"); for schema in [ &pre_tool_use, &permission_request, &post_tool_use, &pre_compact, &post_compact, &user_prompt_submit, &subagent_start, &subagent_stop, &stop, ] { assert_eq!(schema["properties"]["turn_id"]["type"], "string"); assert!( schema["required"] .as_array() .expect("schema required fields") .contains(&Value::String("turn_id".to_string())) ); } } #[test] fn subagent_context_fields_are_optional_for_hooks_that_run_inside_subagents() { let schemas = [ schema_json::().expect("serialize pre tool use input schema"), schema_json::() .expect("serialize permission request input schema"), schema_json::().expect("serialize post tool use input schema"), schema_json::().expect("serialize pre compact input schema"), schema_json::().expect("serialize post compact input schema"), schema_json::() .expect("serialize user prompt submit input schema"), ]; for schema in schemas { let schema: Value = serde_json::from_slice(&schema).expect("parse hook input schema"); assert_eq!(schema["properties"]["agent_id"]["type"], "string"); assert_eq!(schema["properties"]["agent_type"]["type"], "string"); let required = schema["required"] .as_array() .expect("schema required fields"); assert!(!required.contains(&Value::String("agent_id".to_string()))); assert!(!required.contains(&Value::String("agent_type".to_string()))); } } #[test] fn subagent_context_fields_serialize_flat_and_omit_when_absent() { let subagent = SubagentCommandInputFields::from(Some(&SubagentHookContext { agent_id: "agent-1".to_string(), agent_type: "worker".to_string(), })); let input = PreToolUseCommandInput { session_id: "session-1".to_string(), turn_id: "turn-1".to_string(), agent_id: subagent.agent_id, agent_type: subagent.agent_type, transcript_path: NullableString::from_path(/*path*/ None), cwd: "/tmp".to_string(), hook_event_name: "PreToolUse".to_string(), model: "gpt-test".to_string(), permission_mode: "default".to_string(), tool_name: "Bash".to_string(), tool_input: json!({ "command": "echo hello" }), tool_use_id: "tool-1".to_string(), }; assert_eq!( serde_json::to_value(input).expect("serialize subagent hook input"), json!({ "session_id": "session-1", "turn_id": "turn-1", "agent_id": "agent-1", "agent_type": "worker", "transcript_path": null, "cwd": "/tmp", "hook_event_name": "PreToolUse", "model": "gpt-test", "permission_mode": "default", "tool_name": "Bash", "tool_input": { "command": "echo hello" }, "tool_use_id": "tool-1", }) ); let root_input = PreToolUseCommandInput { session_id: "session-1".to_string(), turn_id: "turn-1".to_string(), agent_id: None, agent_type: None, transcript_path: NullableString::from_path(/*path*/ None), cwd: "/tmp".to_string(), hook_event_name: "PreToolUse".to_string(), model: "gpt-test".to_string(), permission_mode: "default".to_string(), tool_name: "Bash".to_string(), tool_input: json!({ "command": "echo hello" }), tool_use_id: "tool-1".to_string(), }; let root_input = serde_json::to_value(root_input).expect("serialize root hook input"); assert_eq!(root_input.get("agent_id"), None); assert_eq!(root_input.get("agent_type"), None); } }