1use schemars::JsonSchema;
2use schemars::r#gen::SchemaGenerator;
3use schemars::r#gen::SchemaSettings;
4use schemars::schema::InstanceType;
5use schemars::schema::RootSchema;
6use schemars::schema::Schema;
7use schemars::schema::SchemaObject;
8use serde::Deserialize;
9use serde::Serialize;
10use serde_json::Map;
11use serde_json::Value;
12use std::path::Path;
13use std::path::PathBuf;
14
15use crate::events::common::SubagentHookContext;
16
17const GENERATED_DIR: &str = "generated";
18const POST_TOOL_USE_INPUT_FIXTURE: &str = "post-tool-use.command.input.schema.json";
19const POST_TOOL_USE_OUTPUT_FIXTURE: &str = "post-tool-use.command.output.schema.json";
20const PERMISSION_REQUEST_INPUT_FIXTURE: &str = "permission-request.command.input.schema.json";
21const PERMISSION_REQUEST_OUTPUT_FIXTURE: &str = "permission-request.command.output.schema.json";
22const POST_COMPACT_INPUT_FIXTURE: &str = "post-compact.command.input.schema.json";
23const POST_COMPACT_OUTPUT_FIXTURE: &str = "post-compact.command.output.schema.json";
24const PRE_TOOL_USE_INPUT_FIXTURE: &str = "pre-tool-use.command.input.schema.json";
25const PRE_TOOL_USE_OUTPUT_FIXTURE: &str = "pre-tool-use.command.output.schema.json";
26const PRE_COMPACT_INPUT_FIXTURE: &str = "pre-compact.command.input.schema.json";
27const PRE_COMPACT_OUTPUT_FIXTURE: &str = "pre-compact.command.output.schema.json";
28const SESSION_START_INPUT_FIXTURE: &str = "session-start.command.input.schema.json";
29const SESSION_START_OUTPUT_FIXTURE: &str = "session-start.command.output.schema.json";
30const SESSION_END_INPUT_FIXTURE: &str = "session-end.command.input.schema.json";
31const USER_PROMPT_SUBMIT_INPUT_FIXTURE: &str = "user-prompt-submit.command.input.schema.json";
32const USER_PROMPT_SUBMIT_OUTPUT_FIXTURE: &str = "user-prompt-submit.command.output.schema.json";
33const SUBAGENT_START_INPUT_FIXTURE: &str = "subagent-start.command.input.schema.json";
34const SUBAGENT_START_OUTPUT_FIXTURE: &str = "subagent-start.command.output.schema.json";
35const SUBAGENT_STOP_INPUT_FIXTURE: &str = "subagent-stop.command.input.schema.json";
36const SUBAGENT_STOP_OUTPUT_FIXTURE: &str = "subagent-stop.command.output.schema.json";
37const STOP_INPUT_FIXTURE: &str = "stop.command.input.schema.json";
38const STOP_OUTPUT_FIXTURE: &str = "stop.command.output.schema.json";
39
40#[derive(Debug, Clone, Serialize)]
41#[serde(transparent)]
42pub(crate) struct NullableString(Option<String>);
43
44impl NullableString {
45 pub(crate) fn from_path(path: Option<PathBuf>) -> Self {
46 Self(path.map(|path| path.display().to_string()))
47 }
48
49 pub(crate) fn from_string(value: Option<String>) -> Self {
50 Self(value)
51 }
52}
53
54impl JsonSchema for NullableString {
55 fn schema_name() -> String {
56 "NullableString".to_string()
57 }
58
59 fn json_schema(_gen: &mut SchemaGenerator) -> Schema {
60 Schema::Object(SchemaObject {
61 instance_type: Some(vec![InstanceType::String, InstanceType::Null].into()),
62 ..Default::default()
63 })
64 }
65}
66
67#[derive(Debug, Clone, Default)]
68pub(crate) struct SubagentCommandInputFields {
69 pub agent_id: Option<String>,
70 pub agent_type: Option<String>,
71}
72
73impl From<Option<&SubagentHookContext>> for SubagentCommandInputFields {
74 fn from(value: Option<&SubagentHookContext>) -> Self {
75 match value {
76 Some(context) => Self {
77 agent_id: Some(context.agent_id.clone()),
78 agent_type: Some(context.agent_type.clone()),
79 },
80 None => Self::default(),
81 }
82 }
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
86#[serde(rename_all = "camelCase")]
87#[serde(deny_unknown_fields)]
88pub(crate) struct HookUniversalOutputWire {
89 #[serde(default = "default_continue")]
90 pub r#continue: bool,
91 #[serde(default)]
92 pub stop_reason: Option<String>,
93 #[serde(default)]
94 pub suppress_output: bool,
95 #[serde(default)]
96 pub system_message: Option<String>,
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
100pub(crate) enum HookEventNameWire {
101 #[serde(rename = "PreToolUse")]
102 PreToolUse,
103 #[serde(rename = "PermissionRequest")]
104 PermissionRequest,
105 #[serde(rename = "PostToolUse")]
106 PostToolUse,
107 #[serde(rename = "PreCompact")]
108 PreCompact,
109 #[serde(rename = "PostCompact")]
110 PostCompact,
111 #[serde(rename = "SessionStart")]
112 SessionStart,
113 #[serde(rename = "UserPromptSubmit")]
114 UserPromptSubmit,
115 #[serde(rename = "SubagentStart")]
116 SubagentStart,
117 #[serde(rename = "SubagentStop")]
118 SubagentStop,
119 #[serde(rename = "Stop")]
120 Stop,
121}
122
123#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
124#[serde(rename_all = "camelCase")]
125#[serde(deny_unknown_fields)]
126#[schemars(rename = "pre-tool-use.command.output")]
127pub(crate) struct PreToolUseCommandOutputWire {
128 #[serde(flatten)]
129 pub universal: HookUniversalOutputWire,
130 #[serde(default)]
131 pub decision: Option<PreToolUseDecisionWire>,
132 #[serde(default)]
133 pub reason: Option<String>,
134 #[serde(default)]
135 pub hook_specific_output: Option<PreToolUseHookSpecificOutputWire>,
136}
137
138#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
139#[serde(rename_all = "camelCase")]
140#[serde(deny_unknown_fields)]
141#[schemars(rename = "post-tool-use.command.output")]
142pub(crate) struct PostToolUseCommandOutputWire {
143 #[serde(flatten)]
144 pub universal: HookUniversalOutputWire,
145 #[serde(default)]
146 pub decision: Option<BlockDecisionWire>,
147 #[serde(default)]
148 pub reason: Option<String>,
149 #[serde(default)]
150 pub hook_specific_output: Option<PostToolUseHookSpecificOutputWire>,
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
154#[serde(rename_all = "camelCase")]
155#[serde(deny_unknown_fields)]
156#[schemars(rename = "permission-request.command.output")]
157pub(crate) struct PermissionRequestCommandOutputWire {
158 #[serde(flatten)]
159 pub universal: HookUniversalOutputWire,
160 #[serde(default)]
161 pub hook_specific_output: Option<PermissionRequestHookSpecificOutputWire>,
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
165#[serde(rename_all = "camelCase")]
166#[serde(deny_unknown_fields)]
167#[schemars(rename = "pre-compact.command.output")]
168pub(crate) struct PreCompactCommandOutputWire {
169 #[serde(flatten)]
170 pub universal: HookUniversalOutputWire,
171}
172
173#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
174#[serde(rename_all = "camelCase")]
175#[serde(deny_unknown_fields)]
176#[schemars(rename = "post-compact.command.output")]
177pub(crate) struct PostCompactCommandOutputWire {
178 #[serde(flatten)]
179 pub universal: HookUniversalOutputWire,
180}
181
182#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
183#[serde(rename_all = "camelCase")]
184#[serde(deny_unknown_fields)]
185pub(crate) struct PermissionRequestHookSpecificOutputWire {
186 #[schemars(schema_with = "permission_request_hook_event_name_schema")]
187 pub hook_event_name: HookEventNameWire,
188 #[serde(default)]
189 pub decision: Option<PermissionRequestDecisionWire>,
190}
191
192#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
193#[serde(rename_all = "camelCase")]
194#[serde(deny_unknown_fields)]
195pub(crate) struct PermissionRequestDecisionWire {
196 pub behavior: PermissionRequestBehaviorWire,
197 #[serde(default)]
201 pub updated_input: Option<Value>,
202 #[serde(default)]
206 pub updated_permissions: Option<Value>,
207 #[serde(default)]
208 pub message: Option<String>,
209 #[serde(default)]
213 pub interrupt: bool,
214}
215
216#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
217pub(crate) enum PermissionRequestBehaviorWire {
218 #[serde(rename = "allow")]
219 Allow,
220 #[serde(rename = "deny")]
221 Deny,
222}
223
224#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
225#[serde(rename_all = "camelCase")]
226#[serde(deny_unknown_fields)]
227pub(crate) struct PostToolUseHookSpecificOutputWire {
228 #[schemars(schema_with = "post_tool_use_hook_event_name_schema")]
229 pub hook_event_name: HookEventNameWire,
230 #[serde(default)]
231 pub additional_context: Option<String>,
232 #[serde(default)]
233 #[serde(rename = "updatedMCPToolOutput")]
234 pub updated_mcp_tool_output: Option<Value>,
235}
236
237#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
238#[serde(rename_all = "camelCase")]
239#[serde(deny_unknown_fields)]
240pub(crate) struct PreToolUseHookSpecificOutputWire {
241 #[schemars(schema_with = "pre_tool_use_hook_event_name_schema")]
242 pub hook_event_name: HookEventNameWire,
243 #[serde(default)]
244 pub permission_decision: Option<PreToolUsePermissionDecisionWire>,
245 #[serde(default)]
246 pub permission_decision_reason: Option<String>,
247 #[serde(default)]
248 pub updated_input: Option<Value>,
249 #[serde(default)]
250 pub additional_context: Option<String>,
251}
252
253#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
254pub(crate) enum PreToolUsePermissionDecisionWire {
255 #[serde(rename = "allow")]
256 Allow,
257 #[serde(rename = "deny")]
258 Deny,
259 #[serde(rename = "ask")]
260 Ask,
261}
262
263#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
264pub(crate) enum PreToolUseDecisionWire {
265 #[serde(rename = "approve")]
266 Approve,
267 #[serde(rename = "block")]
268 Block,
269}
270
271#[derive(Debug, Clone, Serialize, JsonSchema)]
272#[serde(deny_unknown_fields)]
273#[schemars(rename = "pre-tool-use.command.input")]
274pub(crate) struct PreToolUseCommandInput {
275 pub session_id: String,
276 pub turn_id: String,
278 #[serde(skip_serializing_if = "Option::is_none")]
279 pub agent_id: Option<String>,
280 #[serde(skip_serializing_if = "Option::is_none")]
281 pub agent_type: Option<String>,
282 pub transcript_path: NullableString,
283 pub cwd: String,
284 #[schemars(schema_with = "pre_tool_use_hook_event_name_schema")]
285 pub hook_event_name: String,
286 pub model: String,
287 #[schemars(schema_with = "permission_mode_schema")]
288 pub permission_mode: String,
289 pub tool_name: String,
290 pub tool_input: Value,
291 pub tool_use_id: String,
292}
293
294#[derive(Debug, Clone, Serialize, JsonSchema)]
295#[serde(deny_unknown_fields)]
296#[schemars(rename = "permission-request.command.input")]
297pub(crate) struct PermissionRequestCommandInput {
298 pub session_id: String,
299 pub turn_id: String,
301 #[serde(skip_serializing_if = "Option::is_none")]
302 pub agent_id: Option<String>,
303 #[serde(skip_serializing_if = "Option::is_none")]
304 pub agent_type: Option<String>,
305 pub transcript_path: NullableString,
306 pub cwd: String,
307 #[schemars(schema_with = "permission_request_hook_event_name_schema")]
308 pub hook_event_name: String,
309 pub model: String,
310 #[schemars(schema_with = "permission_mode_schema")]
311 pub permission_mode: String,
312 pub tool_name: String,
313 pub tool_input: Value,
314}
315
316#[derive(Debug, Clone, Serialize, JsonSchema)]
317#[serde(deny_unknown_fields)]
318#[schemars(rename = "post-tool-use.command.input")]
319pub(crate) struct PostToolUseCommandInput {
320 pub session_id: String,
321 pub turn_id: String,
323 #[serde(skip_serializing_if = "Option::is_none")]
324 pub agent_id: Option<String>,
325 #[serde(skip_serializing_if = "Option::is_none")]
326 pub agent_type: Option<String>,
327 pub transcript_path: NullableString,
328 pub cwd: String,
329 #[schemars(schema_with = "post_tool_use_hook_event_name_schema")]
330 pub hook_event_name: String,
331 pub model: String,
332 #[schemars(schema_with = "permission_mode_schema")]
333 pub permission_mode: String,
334 pub tool_name: String,
335 pub tool_input: Value,
336 pub tool_response: Value,
337 pub tool_use_id: String,
338}
339
340#[derive(Debug, Clone, Serialize, JsonSchema)]
341#[serde(deny_unknown_fields)]
342#[schemars(rename = "pre-compact.command.input")]
343pub(crate) struct PreCompactCommandInput {
344 pub session_id: String,
345 pub turn_id: String,
347 #[serde(skip_serializing_if = "Option::is_none")]
348 pub agent_id: Option<String>,
349 #[serde(skip_serializing_if = "Option::is_none")]
350 pub agent_type: Option<String>,
351 pub transcript_path: NullableString,
352 pub cwd: String,
353 #[schemars(schema_with = "pre_compact_hook_event_name_schema")]
354 pub hook_event_name: String,
355 pub model: String,
356 #[schemars(schema_with = "compaction_trigger_schema")]
357 pub trigger: String,
358}
359
360#[derive(Debug, Clone, Serialize, JsonSchema)]
361#[serde(deny_unknown_fields)]
362#[schemars(rename = "post-compact.command.input")]
363pub(crate) struct PostCompactCommandInput {
364 pub session_id: String,
365 pub turn_id: String,
367 #[serde(skip_serializing_if = "Option::is_none")]
368 pub agent_id: Option<String>,
369 #[serde(skip_serializing_if = "Option::is_none")]
370 pub agent_type: Option<String>,
371 pub transcript_path: NullableString,
372 pub cwd: String,
373 #[schemars(schema_with = "post_compact_hook_event_name_schema")]
374 pub hook_event_name: String,
375 pub model: String,
376 #[schemars(schema_with = "compaction_trigger_schema")]
377 pub trigger: String,
378}
379
380#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
381#[serde(rename_all = "camelCase")]
382#[serde(deny_unknown_fields)]
383#[schemars(rename = "session-start.command.output")]
384pub(crate) struct SessionStartCommandOutputWire {
385 #[serde(flatten)]
386 pub universal: HookUniversalOutputWire,
387 #[serde(default)]
388 pub hook_specific_output: Option<SessionStartHookSpecificOutputWire>,
389}
390
391#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
392#[serde(rename_all = "camelCase")]
393#[serde(deny_unknown_fields)]
394pub(crate) struct SessionStartHookSpecificOutputWire {
395 #[schemars(schema_with = "session_start_hook_event_name_schema")]
396 pub hook_event_name: HookEventNameWire,
397 #[serde(default)]
398 pub additional_context: Option<String>,
399}
400
401#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
402#[serde(rename_all = "camelCase")]
403#[serde(deny_unknown_fields)]
404#[schemars(rename = "subagent-start.command.output")]
405pub(crate) struct SubagentStartCommandOutputWire {
406 #[serde(flatten)]
407 pub universal: HookUniversalOutputWire,
408 #[serde(default)]
409 pub hook_specific_output: Option<SubagentStartHookSpecificOutputWire>,
410}
411
412#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
413#[serde(rename_all = "camelCase")]
414#[serde(deny_unknown_fields)]
415pub(crate) struct SubagentStartHookSpecificOutputWire {
416 #[schemars(schema_with = "subagent_start_hook_event_name_schema")]
417 pub hook_event_name: HookEventNameWire,
418 #[serde(default)]
419 pub additional_context: Option<String>,
420}
421
422#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
423#[serde(rename_all = "camelCase")]
424#[serde(deny_unknown_fields)]
425#[schemars(rename = "user-prompt-submit.command.output")]
426pub(crate) struct UserPromptSubmitCommandOutputWire {
427 #[serde(flatten)]
428 pub universal: HookUniversalOutputWire,
429 #[serde(default)]
430 pub decision: Option<BlockDecisionWire>,
431 #[serde(default)]
432 pub reason: Option<String>,
433 #[serde(default)]
434 pub hook_specific_output: Option<UserPromptSubmitHookSpecificOutputWire>,
435}
436
437#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
438#[serde(rename_all = "camelCase")]
439#[serde(deny_unknown_fields)]
440pub(crate) struct UserPromptSubmitHookSpecificOutputWire {
441 #[schemars(schema_with = "user_prompt_submit_hook_event_name_schema")]
442 pub hook_event_name: HookEventNameWire,
443 #[serde(default)]
444 pub additional_context: Option<String>,
445}
446
447#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
448#[serde(rename_all = "camelCase")]
449#[serde(deny_unknown_fields)]
450#[schemars(rename = "stop.command.output")]
451pub(crate) struct StopCommandOutputWire {
452 #[serde(flatten)]
453 pub universal: HookUniversalOutputWire,
454 #[serde(default)]
455 pub decision: Option<BlockDecisionWire>,
456 #[serde(default)]
459 pub reason: Option<String>,
460}
461
462#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
463#[serde(rename_all = "camelCase")]
464#[serde(deny_unknown_fields)]
465#[schemars(rename = "subagent-stop.command.output")]
466pub(crate) struct SubagentStopCommandOutputWire {
467 #[serde(flatten)]
468 pub universal: HookUniversalOutputWire,
469 #[serde(default)]
470 pub decision: Option<BlockDecisionWire>,
471 #[serde(default)]
474 pub reason: Option<String>,
475}
476
477#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
478pub(crate) enum BlockDecisionWire {
479 #[serde(rename = "block")]
480 Block,
481}
482
483#[derive(Debug, Clone, Serialize, JsonSchema)]
484#[serde(deny_unknown_fields)]
485#[schemars(rename = "session-start.command.input")]
486pub(crate) struct SessionStartCommandInput {
487 pub session_id: String,
488 pub transcript_path: NullableString,
489 pub cwd: String,
490 #[schemars(schema_with = "session_start_hook_event_name_schema")]
491 pub hook_event_name: String,
492 pub model: String,
493 #[schemars(schema_with = "permission_mode_schema")]
494 pub permission_mode: String,
495 #[schemars(schema_with = "session_start_source_schema")]
496 pub source: String,
497}
498
499#[derive(Debug, Clone, Serialize, JsonSchema)]
500#[serde(deny_unknown_fields)]
501#[schemars(rename = "session-end.command.input")]
502pub(crate) struct SessionEndCommandInput {
503 pub session_id: String,
504 pub transcript_path: NullableString,
505 pub cwd: String,
506 #[schemars(schema_with = "session_end_hook_event_name_schema")]
507 pub hook_event_name: String,
508 #[schemars(schema_with = "session_end_reason_schema")]
509 pub reason: String,
510}
511
512impl SessionStartCommandInput {
513 pub(crate) fn new(
514 session_id: impl Into<String>,
515 transcript_path: Option<PathBuf>,
516 cwd: impl Into<String>,
517 model: impl Into<String>,
518 permission_mode: impl Into<String>,
519 source: impl Into<String>,
520 ) -> Self {
521 Self {
522 session_id: session_id.into(),
523 transcript_path: NullableString::from_path(transcript_path),
524 cwd: cwd.into(),
525 hook_event_name: "SessionStart".to_string(),
526 model: model.into(),
527 permission_mode: permission_mode.into(),
528 source: source.into(),
529 }
530 }
531}
532
533#[derive(Debug, Clone, Serialize, JsonSchema)]
534#[serde(deny_unknown_fields)]
535#[schemars(rename = "subagent-start.command.input")]
536pub(crate) struct SubagentStartCommandInput {
537 pub session_id: String,
538 pub turn_id: String,
540 pub transcript_path: NullableString,
541 pub cwd: String,
542 #[schemars(schema_with = "subagent_start_hook_event_name_schema")]
543 pub hook_event_name: String,
544 pub model: String,
545 #[schemars(schema_with = "permission_mode_schema")]
546 pub permission_mode: String,
547 pub agent_id: String,
548 pub agent_type: String,
549}
550
551#[derive(Debug, Clone, Serialize, JsonSchema)]
552#[serde(deny_unknown_fields)]
553#[schemars(rename = "user-prompt-submit.command.input")]
554pub(crate) struct UserPromptSubmitCommandInput {
555 pub session_id: String,
556 pub turn_id: String,
558 #[serde(skip_serializing_if = "Option::is_none")]
559 pub agent_id: Option<String>,
560 #[serde(skip_serializing_if = "Option::is_none")]
561 pub agent_type: Option<String>,
562 pub transcript_path: NullableString,
563 pub cwd: String,
564 #[schemars(schema_with = "user_prompt_submit_hook_event_name_schema")]
565 pub hook_event_name: String,
566 pub model: String,
567 #[schemars(schema_with = "permission_mode_schema")]
568 pub permission_mode: String,
569 pub prompt: String,
570}
571
572#[derive(Debug, Clone, Serialize, JsonSchema)]
573#[serde(deny_unknown_fields)]
574#[schemars(rename = "stop.command.input")]
575pub(crate) struct StopCommandInput {
576 pub session_id: String,
577 pub turn_id: String,
579 pub transcript_path: NullableString,
580 pub cwd: String,
581 #[schemars(schema_with = "stop_hook_event_name_schema")]
582 pub hook_event_name: String,
583 pub model: String,
584 #[schemars(schema_with = "permission_mode_schema")]
585 pub permission_mode: String,
586 pub stop_hook_active: bool,
587 pub last_assistant_message: NullableString,
588}
589
590#[derive(Debug, Clone, Serialize, JsonSchema)]
591#[serde(deny_unknown_fields)]
592#[schemars(rename = "subagent-stop.command.input")]
593pub(crate) struct SubagentStopCommandInput {
594 pub session_id: String,
595 pub turn_id: String,
597 pub transcript_path: NullableString,
598 pub agent_transcript_path: NullableString,
599 pub cwd: String,
600 #[schemars(schema_with = "subagent_stop_hook_event_name_schema")]
601 pub hook_event_name: String,
602 pub model: String,
603 #[schemars(schema_with = "permission_mode_schema")]
604 pub permission_mode: String,
605 pub stop_hook_active: bool,
606 pub agent_id: String,
607 pub agent_type: String,
608 pub last_assistant_message: NullableString,
609}
610
611pub fn write_schema_fixtures(schema_root: &Path) -> anyhow::Result<()> {
612 let generated_dir = schema_root.join(GENERATED_DIR);
613 ensure_empty_dir(&generated_dir)?;
614
615 write_schema(
616 &generated_dir.join(POST_TOOL_USE_INPUT_FIXTURE),
617 schema_json::<PostToolUseCommandInput>()?,
618 )?;
619 write_schema(
620 &generated_dir.join(POST_TOOL_USE_OUTPUT_FIXTURE),
621 schema_json::<PostToolUseCommandOutputWire>()?,
622 )?;
623 write_schema(
624 &generated_dir.join(PERMISSION_REQUEST_INPUT_FIXTURE),
625 schema_json::<PermissionRequestCommandInput>()?,
626 )?;
627 write_schema(
628 &generated_dir.join(PERMISSION_REQUEST_OUTPUT_FIXTURE),
629 schema_json::<PermissionRequestCommandOutputWire>()?,
630 )?;
631 write_schema(
632 &generated_dir.join(POST_COMPACT_INPUT_FIXTURE),
633 schema_json::<PostCompactCommandInput>()?,
634 )?;
635 write_schema(
636 &generated_dir.join(POST_COMPACT_OUTPUT_FIXTURE),
637 schema_json::<PostCompactCommandOutputWire>()?,
638 )?;
639 write_schema(
640 &generated_dir.join(PRE_COMPACT_INPUT_FIXTURE),
641 schema_json::<PreCompactCommandInput>()?,
642 )?;
643 write_schema(
644 &generated_dir.join(PRE_COMPACT_OUTPUT_FIXTURE),
645 schema_json::<PreCompactCommandOutputWire>()?,
646 )?;
647 write_schema(
648 &generated_dir.join(PRE_TOOL_USE_INPUT_FIXTURE),
649 schema_json::<PreToolUseCommandInput>()?,
650 )?;
651 write_schema(
652 &generated_dir.join(PRE_TOOL_USE_OUTPUT_FIXTURE),
653 schema_json::<PreToolUseCommandOutputWire>()?,
654 )?;
655 write_schema(
656 &generated_dir.join(SESSION_START_INPUT_FIXTURE),
657 schema_json::<SessionStartCommandInput>()?,
658 )?;
659 write_schema(
660 &generated_dir.join(SESSION_START_OUTPUT_FIXTURE),
661 schema_json::<SessionStartCommandOutputWire>()?,
662 )?;
663 write_schema(
664 &generated_dir.join(SESSION_END_INPUT_FIXTURE),
665 schema_json::<SessionEndCommandInput>()?,
666 )?;
667 write_schema(
668 &generated_dir.join(USER_PROMPT_SUBMIT_INPUT_FIXTURE),
669 schema_json::<UserPromptSubmitCommandInput>()?,
670 )?;
671 write_schema(
672 &generated_dir.join(USER_PROMPT_SUBMIT_OUTPUT_FIXTURE),
673 schema_json::<UserPromptSubmitCommandOutputWire>()?,
674 )?;
675 write_schema(
676 &generated_dir.join(SUBAGENT_START_INPUT_FIXTURE),
677 schema_json::<SubagentStartCommandInput>()?,
678 )?;
679 write_schema(
680 &generated_dir.join(SUBAGENT_START_OUTPUT_FIXTURE),
681 schema_json::<SubagentStartCommandOutputWire>()?,
682 )?;
683 write_schema(
684 &generated_dir.join(SUBAGENT_STOP_INPUT_FIXTURE),
685 schema_json::<SubagentStopCommandInput>()?,
686 )?;
687 write_schema(
688 &generated_dir.join(SUBAGENT_STOP_OUTPUT_FIXTURE),
689 schema_json::<SubagentStopCommandOutputWire>()?,
690 )?;
691 write_schema(
692 &generated_dir.join(STOP_INPUT_FIXTURE),
693 schema_json::<StopCommandInput>()?,
694 )?;
695 write_schema(
696 &generated_dir.join(STOP_OUTPUT_FIXTURE),
697 schema_json::<StopCommandOutputWire>()?,
698 )?;
699
700 Ok(())
701}
702
703fn write_schema(path: &Path, json: Vec<u8>) -> anyhow::Result<()> {
704 std::fs::write(path, json)?;
705 Ok(())
706}
707
708fn ensure_empty_dir(dir: &Path) -> anyhow::Result<()> {
709 if dir.exists() {
710 std::fs::remove_dir_all(dir)?;
711 }
712 std::fs::create_dir_all(dir)?;
713 Ok(())
714}
715
716fn schema_json<T>() -> anyhow::Result<Vec<u8>>
717where
718 T: JsonSchema,
719{
720 let schema = schema_for_type::<T>();
721 let value = serde_json::to_value(schema)?;
722 let value = canonicalize_json(&value);
723 Ok(serde_json::to_vec_pretty(&value)?)
724}
725
726fn schema_for_type<T>() -> RootSchema
727where
728 T: JsonSchema,
729{
730 SchemaSettings::draft07()
731 .with(|settings| {
732 settings.option_add_null_type = false;
733 })
734 .into_generator()
735 .into_root_schema_for::<T>()
736}
737
738fn canonicalize_json(value: &Value) -> Value {
739 match value {
740 Value::Array(items) => Value::Array(items.iter().map(canonicalize_json).collect()),
741 Value::Object(map) => {
742 let mut entries: Vec<_> = map.iter().collect();
743 entries.sort_by_key(|(key, _)| *key);
744 let mut sorted = Map::with_capacity(map.len());
745 for (key, child) in entries {
746 sorted.insert(key.clone(), canonicalize_json(child));
747 }
748 Value::Object(sorted)
749 }
750 _ => value.clone(),
751 }
752}
753
754fn session_start_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema {
755 string_const_schema("SessionStart")
756}
757
758fn session_end_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema {
759 string_const_schema("SessionEnd")
760}
761
762fn session_end_reason_schema(_gen: &mut SchemaGenerator) -> Schema {
763 string_const_schema("other")
764}
765
766fn post_tool_use_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema {
767 string_const_schema("PostToolUse")
768}
769
770fn pre_compact_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema {
771 string_const_schema("PreCompact")
772}
773
774fn post_compact_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema {
775 string_const_schema("PostCompact")
776}
777
778fn pre_tool_use_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema {
779 string_const_schema("PreToolUse")
780}
781
782fn permission_request_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema {
783 string_const_schema("PermissionRequest")
784}
785
786fn user_prompt_submit_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema {
787 string_const_schema("UserPromptSubmit")
788}
789
790fn subagent_start_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema {
791 string_const_schema("SubagentStart")
792}
793
794fn subagent_stop_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema {
795 string_const_schema("SubagentStop")
796}
797
798fn stop_hook_event_name_schema(_gen: &mut SchemaGenerator) -> Schema {
799 string_const_schema("Stop")
800}
801
802fn permission_mode_schema(_gen: &mut SchemaGenerator) -> Schema {
803 string_enum_schema(&[
804 "default",
805 "acceptEdits",
806 "plan",
807 "dontAsk",
808 "bypassPermissions",
809 ])
810}
811
812fn session_start_source_schema(_gen: &mut SchemaGenerator) -> Schema {
813 string_enum_schema(&["startup", "resume", "clear", "compact"])
814}
815
816fn compaction_trigger_schema(_gen: &mut SchemaGenerator) -> Schema {
817 string_enum_schema(&["manual", "auto"])
818}
819
820fn string_const_schema(value: &str) -> Schema {
821 let mut schema = SchemaObject {
822 instance_type: Some(InstanceType::String.into()),
823 ..Default::default()
824 };
825 schema.const_value = Some(Value::String(value.to_string()));
826 Schema::Object(schema)
827}
828
829fn string_enum_schema(values: &[&str]) -> Schema {
830 let mut schema = SchemaObject {
831 instance_type: Some(InstanceType::String.into()),
832 ..Default::default()
833 };
834 schema.enum_values = Some(
835 values
836 .iter()
837 .map(|value| Value::String((*value).to_string()))
838 .collect(),
839 );
840 Schema::Object(schema)
841}
842
843fn default_continue() -> bool {
844 true
845}
846
847#[cfg(test)]
848mod tests {
849 use super::NullableString;
850 use super::PERMISSION_REQUEST_INPUT_FIXTURE;
851 use super::PERMISSION_REQUEST_OUTPUT_FIXTURE;
852 use super::POST_COMPACT_INPUT_FIXTURE;
853 use super::POST_COMPACT_OUTPUT_FIXTURE;
854 use super::POST_TOOL_USE_INPUT_FIXTURE;
855 use super::POST_TOOL_USE_OUTPUT_FIXTURE;
856 use super::PRE_COMPACT_INPUT_FIXTURE;
857 use super::PRE_COMPACT_OUTPUT_FIXTURE;
858 use super::PRE_TOOL_USE_INPUT_FIXTURE;
859 use super::PRE_TOOL_USE_OUTPUT_FIXTURE;
860 use super::PermissionRequestCommandInput;
861 use super::PermissionRequestCommandOutputWire;
862 use super::PostCompactCommandInput;
863 use super::PostToolUseCommandInput;
864 use super::PostToolUseCommandOutputWire;
865 use super::PreCompactCommandInput;
866 use super::PreToolUseCommandInput;
867 use super::PreToolUseCommandOutputWire;
868 use super::SESSION_END_INPUT_FIXTURE;
869 use super::SESSION_START_INPUT_FIXTURE;
870 use super::SESSION_START_OUTPUT_FIXTURE;
871 use super::STOP_INPUT_FIXTURE;
872 use super::STOP_OUTPUT_FIXTURE;
873 use super::SUBAGENT_START_INPUT_FIXTURE;
874 use super::SUBAGENT_START_OUTPUT_FIXTURE;
875 use super::SUBAGENT_STOP_INPUT_FIXTURE;
876 use super::SUBAGENT_STOP_OUTPUT_FIXTURE;
877 use super::SessionStartCommandOutputWire;
878 use super::StopCommandInput;
879 use super::SubagentCommandInputFields;
880 use super::SubagentStartCommandInput;
881 use super::SubagentStartCommandOutputWire;
882 use super::SubagentStopCommandInput;
883 use super::USER_PROMPT_SUBMIT_INPUT_FIXTURE;
884 use super::USER_PROMPT_SUBMIT_OUTPUT_FIXTURE;
885 use super::UserPromptSubmitCommandInput;
886 use super::UserPromptSubmitCommandOutputWire;
887 use super::schema_json;
888 use super::write_schema_fixtures;
889 use crate::events::common::SubagentHookContext;
890 use pretty_assertions::assert_eq;
891 use schemars::JsonSchema;
892 use serde_json::Value;
893 use serde_json::json;
894 use tempfile::TempDir;
895
896 fn expected_fixture(name: &str) -> &'static str {
897 match name {
898 POST_TOOL_USE_INPUT_FIXTURE => {
899 include_str!("../schema/generated/post-tool-use.command.input.schema.json")
900 }
901 POST_TOOL_USE_OUTPUT_FIXTURE => {
902 include_str!("../schema/generated/post-tool-use.command.output.schema.json")
903 }
904 PERMISSION_REQUEST_INPUT_FIXTURE => {
905 include_str!("../schema/generated/permission-request.command.input.schema.json")
906 }
907 PERMISSION_REQUEST_OUTPUT_FIXTURE => {
908 include_str!("../schema/generated/permission-request.command.output.schema.json")
909 }
910 POST_COMPACT_INPUT_FIXTURE => {
911 include_str!("../schema/generated/post-compact.command.input.schema.json")
912 }
913 POST_COMPACT_OUTPUT_FIXTURE => {
914 include_str!("../schema/generated/post-compact.command.output.schema.json")
915 }
916 PRE_COMPACT_INPUT_FIXTURE => {
917 include_str!("../schema/generated/pre-compact.command.input.schema.json")
918 }
919 PRE_COMPACT_OUTPUT_FIXTURE => {
920 include_str!("../schema/generated/pre-compact.command.output.schema.json")
921 }
922 PRE_TOOL_USE_INPUT_FIXTURE => {
923 include_str!("../schema/generated/pre-tool-use.command.input.schema.json")
924 }
925 PRE_TOOL_USE_OUTPUT_FIXTURE => {
926 include_str!("../schema/generated/pre-tool-use.command.output.schema.json")
927 }
928 SESSION_START_INPUT_FIXTURE => {
929 include_str!("../schema/generated/session-start.command.input.schema.json")
930 }
931 SESSION_START_OUTPUT_FIXTURE => {
932 include_str!("../schema/generated/session-start.command.output.schema.json")
933 }
934 SESSION_END_INPUT_FIXTURE => {
935 include_str!("../schema/generated/session-end.command.input.schema.json")
936 }
937 USER_PROMPT_SUBMIT_INPUT_FIXTURE => {
938 include_str!("../schema/generated/user-prompt-submit.command.input.schema.json")
939 }
940 USER_PROMPT_SUBMIT_OUTPUT_FIXTURE => {
941 include_str!("../schema/generated/user-prompt-submit.command.output.schema.json")
942 }
943 SUBAGENT_START_INPUT_FIXTURE => {
944 include_str!("../schema/generated/subagent-start.command.input.schema.json")
945 }
946 SUBAGENT_START_OUTPUT_FIXTURE => {
947 include_str!("../schema/generated/subagent-start.command.output.schema.json")
948 }
949 SUBAGENT_STOP_INPUT_FIXTURE => {
950 include_str!("../schema/generated/subagent-stop.command.input.schema.json")
951 }
952 SUBAGENT_STOP_OUTPUT_FIXTURE => {
953 include_str!("../schema/generated/subagent-stop.command.output.schema.json")
954 }
955 STOP_INPUT_FIXTURE => {
956 include_str!("../schema/generated/stop.command.input.schema.json")
957 }
958 STOP_OUTPUT_FIXTURE => {
959 include_str!("../schema/generated/stop.command.output.schema.json")
960 }
961 _ => panic!("unexpected fixture name: {name}"),
962 }
963 }
964
965 fn normalize_newlines(value: &str) -> String {
966 value.replace("\r\n", "\n")
967 }
968
969 fn assert_output_hook_event_name_const<T: JsonSchema>(definition: &str, expected: &str) {
970 let schema: Value =
971 serde_json::from_slice(&schema_json::<T>().expect("serialize hook output schema"))
972 .expect("parse hook output schema");
973
974 assert_eq!(
975 schema["definitions"][definition]["properties"]["hookEventName"],
976 json!({
977 "const": expected,
978 "type": "string",
979 })
980 );
981 }
982
983 #[test]
984 fn generated_hook_schemas_match_fixtures() {
985 let temp_dir = TempDir::new().expect("create temp dir");
986 let schema_root = temp_dir.path().join("schema");
987 write_schema_fixtures(&schema_root).expect("write generated hook schemas");
988
989 for fixture in [
990 POST_TOOL_USE_INPUT_FIXTURE,
991 POST_TOOL_USE_OUTPUT_FIXTURE,
992 PERMISSION_REQUEST_INPUT_FIXTURE,
993 PERMISSION_REQUEST_OUTPUT_FIXTURE,
994 POST_COMPACT_INPUT_FIXTURE,
995 POST_COMPACT_OUTPUT_FIXTURE,
996 PRE_COMPACT_INPUT_FIXTURE,
997 PRE_COMPACT_OUTPUT_FIXTURE,
998 PRE_TOOL_USE_INPUT_FIXTURE,
999 PRE_TOOL_USE_OUTPUT_FIXTURE,
1000 SESSION_START_INPUT_FIXTURE,
1001 SESSION_START_OUTPUT_FIXTURE,
1002 SESSION_END_INPUT_FIXTURE,
1003 USER_PROMPT_SUBMIT_INPUT_FIXTURE,
1004 USER_PROMPT_SUBMIT_OUTPUT_FIXTURE,
1005 SUBAGENT_START_INPUT_FIXTURE,
1006 SUBAGENT_START_OUTPUT_FIXTURE,
1007 SUBAGENT_STOP_INPUT_FIXTURE,
1008 SUBAGENT_STOP_OUTPUT_FIXTURE,
1009 STOP_INPUT_FIXTURE,
1010 STOP_OUTPUT_FIXTURE,
1011 ] {
1012 let expected = normalize_newlines(expected_fixture(fixture));
1013 let actual = std::fs::read_to_string(schema_root.join("generated").join(fixture))
1014 .unwrap_or_else(|err| panic!("read generated schema {fixture}: {err}"));
1015 let actual = normalize_newlines(&actual);
1016 assert_eq!(expected, actual, "fixture should match generated schema");
1017 }
1018 }
1019
1020 #[test]
1021 fn hook_specific_output_event_names_are_event_specific_in_output_schemas() {
1022 assert_output_hook_event_name_const::<PermissionRequestCommandOutputWire>(
1023 "PermissionRequestHookSpecificOutputWire",
1024 "PermissionRequest",
1025 );
1026 assert_output_hook_event_name_const::<PostToolUseCommandOutputWire>(
1027 "PostToolUseHookSpecificOutputWire",
1028 "PostToolUse",
1029 );
1030 assert_output_hook_event_name_const::<PreToolUseCommandOutputWire>(
1031 "PreToolUseHookSpecificOutputWire",
1032 "PreToolUse",
1033 );
1034 assert_output_hook_event_name_const::<SessionStartCommandOutputWire>(
1035 "SessionStartHookSpecificOutputWire",
1036 "SessionStart",
1037 );
1038 assert_output_hook_event_name_const::<SubagentStartCommandOutputWire>(
1039 "SubagentStartHookSpecificOutputWire",
1040 "SubagentStart",
1041 );
1042 assert_output_hook_event_name_const::<UserPromptSubmitCommandOutputWire>(
1043 "UserPromptSubmitHookSpecificOutputWire",
1044 "UserPromptSubmit",
1045 );
1046 }
1047
1048 #[test]
1049 fn turn_scoped_hook_inputs_include_codex_turn_id_extension() {
1050 let pre_tool_use: Value = serde_json::from_slice(
1053 &schema_json::<PreToolUseCommandInput>().expect("serialize pre tool use input schema"),
1054 )
1055 .expect("parse pre tool use input schema");
1056 let post_tool_use: Value = serde_json::from_slice(
1057 &schema_json::<PostToolUseCommandInput>()
1058 .expect("serialize post tool use input schema"),
1059 )
1060 .expect("parse post tool use input schema");
1061 let pre_compact: Value = serde_json::from_slice(
1062 &schema_json::<PreCompactCommandInput>().expect("serialize pre compact input schema"),
1063 )
1064 .expect("parse pre compact input schema");
1065 let post_compact: Value = serde_json::from_slice(
1066 &schema_json::<PostCompactCommandInput>().expect("serialize post compact input schema"),
1067 )
1068 .expect("parse post compact input schema");
1069 let permission_request: Value = serde_json::from_slice(
1070 &schema_json::<PermissionRequestCommandInput>()
1071 .expect("serialize permission request input schema"),
1072 )
1073 .expect("parse permission request input schema");
1074 let user_prompt_submit: Value = serde_json::from_slice(
1075 &schema_json::<UserPromptSubmitCommandInput>()
1076 .expect("serialize user prompt submit input schema"),
1077 )
1078 .expect("parse user prompt submit input schema");
1079 let subagent_start: Value = serde_json::from_slice(
1080 &schema_json::<SubagentStartCommandInput>()
1081 .expect("serialize subagent start input schema"),
1082 )
1083 .expect("parse subagent start input schema");
1084 let subagent_stop: Value = serde_json::from_slice(
1085 &schema_json::<SubagentStopCommandInput>()
1086 .expect("serialize subagent stop input schema"),
1087 )
1088 .expect("parse subagent stop input schema");
1089 let stop: Value = serde_json::from_slice(
1090 &schema_json::<StopCommandInput>().expect("serialize stop input schema"),
1091 )
1092 .expect("parse stop input schema");
1093
1094 for schema in [
1095 &pre_tool_use,
1096 &permission_request,
1097 &post_tool_use,
1098 &pre_compact,
1099 &post_compact,
1100 &user_prompt_submit,
1101 &subagent_start,
1102 &subagent_stop,
1103 &stop,
1104 ] {
1105 assert_eq!(schema["properties"]["turn_id"]["type"], "string");
1106 assert!(
1107 schema["required"]
1108 .as_array()
1109 .expect("schema required fields")
1110 .contains(&Value::String("turn_id".to_string()))
1111 );
1112 }
1113 }
1114
1115 #[test]
1116 fn subagent_context_fields_are_optional_for_hooks_that_run_inside_subagents() {
1117 let schemas = [
1118 schema_json::<PreToolUseCommandInput>().expect("serialize pre tool use input schema"),
1119 schema_json::<PermissionRequestCommandInput>()
1120 .expect("serialize permission request input schema"),
1121 schema_json::<PostToolUseCommandInput>().expect("serialize post tool use input schema"),
1122 schema_json::<PreCompactCommandInput>().expect("serialize pre compact input schema"),
1123 schema_json::<PostCompactCommandInput>().expect("serialize post compact input schema"),
1124 schema_json::<UserPromptSubmitCommandInput>()
1125 .expect("serialize user prompt submit input schema"),
1126 ];
1127
1128 for schema in schemas {
1129 let schema: Value = serde_json::from_slice(&schema).expect("parse hook input schema");
1130 assert_eq!(schema["properties"]["agent_id"]["type"], "string");
1131 assert_eq!(schema["properties"]["agent_type"]["type"], "string");
1132 let required = schema["required"]
1133 .as_array()
1134 .expect("schema required fields");
1135 assert!(!required.contains(&Value::String("agent_id".to_string())));
1136 assert!(!required.contains(&Value::String("agent_type".to_string())));
1137 }
1138 }
1139
1140 #[test]
1141 fn subagent_context_fields_serialize_flat_and_omit_when_absent() {
1142 let subagent = SubagentCommandInputFields::from(Some(&SubagentHookContext {
1143 agent_id: "agent-1".to_string(),
1144 agent_type: "worker".to_string(),
1145 }));
1146 let input = PreToolUseCommandInput {
1147 session_id: "session-1".to_string(),
1148 turn_id: "turn-1".to_string(),
1149 agent_id: subagent.agent_id,
1150 agent_type: subagent.agent_type,
1151 transcript_path: NullableString::from_path(None),
1152 cwd: "/tmp".to_string(),
1153 hook_event_name: "PreToolUse".to_string(),
1154 model: "gpt-test".to_string(),
1155 permission_mode: "default".to_string(),
1156 tool_name: "Bash".to_string(),
1157 tool_input: json!({ "command": "echo hello" }),
1158 tool_use_id: "tool-1".to_string(),
1159 };
1160
1161 assert_eq!(
1162 serde_json::to_value(input).expect("serialize subagent hook input"),
1163 json!({
1164 "session_id": "session-1",
1165 "turn_id": "turn-1",
1166 "agent_id": "agent-1",
1167 "agent_type": "worker",
1168 "transcript_path": null,
1169 "cwd": "/tmp",
1170 "hook_event_name": "PreToolUse",
1171 "model": "gpt-test",
1172 "permission_mode": "default",
1173 "tool_name": "Bash",
1174 "tool_input": { "command": "echo hello" },
1175 "tool_use_id": "tool-1",
1176 })
1177 );
1178
1179 let root_input = PreToolUseCommandInput {
1180 session_id: "session-1".to_string(),
1181 turn_id: "turn-1".to_string(),
1182 agent_id: None,
1183 agent_type: None,
1184 transcript_path: NullableString::from_path(None),
1185 cwd: "/tmp".to_string(),
1186 hook_event_name: "PreToolUse".to_string(),
1187 model: "gpt-test".to_string(),
1188 permission_mode: "default".to_string(),
1189 tool_name: "Bash".to_string(),
1190 tool_input: json!({ "command": "echo hello" }),
1191 tool_use_id: "tool-1".to_string(),
1192 };
1193 let root_input = serde_json::to_value(root_input).expect("serialize root hook input");
1194 assert_eq!(root_input.get("agent_id"), None);
1195 assert_eq!(root_input.get("agent_type"), None);
1196 }
1197}