1use serde::Deserialize;
14
15pub const GATE_SCHEMA_VERSION: u32 = 2;
22
23pub const PLUGIN_PROTOCOL_VERSION: u32 = 0;
30
31pub const KLASP_OUTPUT_SCHEMA: u32 = 1;
41
42#[derive(Debug, Deserialize, PartialEq, Eq)]
44pub struct GateInput {
45 pub tool_name: String,
46 pub tool_input: ToolInput,
47}
48
49#[derive(Debug, Deserialize, PartialEq, Eq)]
53pub struct ToolInput {
54 #[serde(default)]
55 pub command: Option<String>,
56}
57
58#[derive(Debug, thiserror::Error, PartialEq, Eq)]
59pub enum GateError {
60 #[error("could not parse gate input as JSON: {0}")]
61 Parse(String),
62 #[error(
63 "klasp-gate: schema mismatch (script={script}, binary={binary}). \
64 Re-run `klasp install` to update the hook."
65 )]
66 SchemaMismatch { script: u32, binary: u32 },
67 #[error(
68 "KLASP_GATE_SCHEMA is not set. Re-run `klasp install` to regenerate \
69 the hook script."
70 )]
71 SchemaMissing,
72}
73
74pub struct GateProtocol;
75
76impl GateProtocol {
77 pub fn parse(stdin: &str) -> Result<GateInput, GateError> {
79 serde_json::from_str(stdin).map_err(|e| GateError::Parse(e.to_string()))
80 }
81
82 pub fn read_schema_from_env() -> Result<u32, GateError> {
88 match std::env::var("KLASP_GATE_SCHEMA") {
89 Err(std::env::VarError::NotPresent) => Err(GateError::SchemaMissing),
90 Err(e) => Err(GateError::Parse(format!("KLASP_GATE_SCHEMA env var: {e}"))),
91 Ok(s) => s
92 .parse::<u32>()
93 .map_err(|e| GateError::Parse(format!("KLASP_GATE_SCHEMA = {s:?}: {e}"))),
94 }
95 }
96
97 pub fn check_schema_env(env_value: u32) -> Result<(), GateError> {
102 if env_value == GATE_SCHEMA_VERSION {
103 Ok(())
104 } else {
105 Err(GateError::SchemaMismatch {
106 script: env_value,
107 binary: GATE_SCHEMA_VERSION,
108 })
109 }
110 }
111}
112
113#[cfg(test)]
114mod tests {
115 use super::*;
116
117 #[test]
118 fn parses_minimal_claude_payload() {
119 let stdin = r#"{
120 "tool_name": "Bash",
121 "tool_input": { "command": "git commit -m 'wip'" }
122 }"#;
123 let input = GateProtocol::parse(stdin).expect("should parse");
124 assert_eq!(input.tool_name, "Bash");
125 assert_eq!(
126 input.tool_input.command.as_deref(),
127 Some("git commit -m 'wip'")
128 );
129 }
130
131 #[test]
132 fn parses_payload_without_command() {
133 let stdin = r#"{ "tool_name": "Read", "tool_input": {} }"#;
134 let input = GateProtocol::parse(stdin).expect("should parse");
135 assert_eq!(input.tool_name, "Read");
136 assert!(input.tool_input.command.is_none());
137 }
138
139 #[test]
140 fn parses_payload_ignoring_extra_fields() {
141 let stdin = r#"{
143 "tool_name": "Bash",
144 "tool_input": { "command": "ls", "extra": 42 },
145 "session_id": "abc"
146 }"#;
147 let input = GateProtocol::parse(stdin).expect("should parse");
148 assert_eq!(input.tool_input.command.as_deref(), Some("ls"));
149 }
150
151 #[test]
152 fn fails_on_malformed_json() {
153 let err = GateProtocol::parse("{ not json").expect_err("should fail");
154 assert!(matches!(err, GateError::Parse(_)));
155 }
156
157 #[test]
158 fn fails_on_missing_tool_input() {
159 let err = GateProtocol::parse(r#"{ "tool_name": "Bash" }"#).expect_err("should fail");
160 assert!(matches!(err, GateError::Parse(_)));
161 }
162
163 #[test]
164 fn schema_match_passes() {
165 assert!(GateProtocol::check_schema_env(GATE_SCHEMA_VERSION).is_ok());
166 }
167
168 #[test]
169 fn schema_mismatch_returns_error() {
170 let err = GateProtocol::check_schema_env(GATE_SCHEMA_VERSION + 1).expect_err("mismatch");
171 match err {
172 GateError::SchemaMismatch { script, binary } => {
173 assert_eq!(script, GATE_SCHEMA_VERSION + 1);
174 assert_eq!(binary, GATE_SCHEMA_VERSION);
175 }
176 other => panic!("expected SchemaMismatch, got {other:?}"),
177 }
178 }
179
180 #[test]
181 fn schema_zero_is_mismatch() {
182 let err = GateProtocol::check_schema_env(0).expect_err("zero should be mismatch");
185 assert!(matches!(err, GateError::SchemaMismatch { .. }));
186 }
187
188 #[test]
189 fn schema_missing_env_returns_schema_missing() {
190 let saved = std::env::var("KLASP_GATE_SCHEMA").ok();
195 unsafe {
197 std::env::remove_var("KLASP_GATE_SCHEMA");
198 }
199 let result = GateProtocol::read_schema_from_env();
200 if let Some(v) = saved {
201 unsafe {
203 std::env::set_var("KLASP_GATE_SCHEMA", v);
204 }
205 }
206 assert!(
207 matches!(result, Err(GateError::SchemaMissing)),
208 "expected SchemaMissing, got {result:?}",
209 );
210 }
211}