1use pmcp::types::{ToolAnnotations, ToolInfo};
12use serde::{Deserialize, Serialize};
13use serde_json::{json, Value};
14
15use crate::types::{
16 PolicyViolation, RiskLevel, UnifiedAction, ValidationMetadata, ValidationResult,
17};
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
25pub struct ValidationResponse {
26 #[serde(flatten)]
28 pub result: ValidationResult,
29
30 pub auto_approved: bool,
32
33 pub action: Option<UnifiedAction>,
35
36 #[serde(skip_serializing_if = "Option::is_none")]
38 pub validated_code_hash: Option<String>,
39}
40
41impl ValidationResponse {
42 pub fn success(
44 explanation: String,
45 risk_level: RiskLevel,
46 approval_token: String,
47 metadata: ValidationMetadata,
48 ) -> Self {
49 Self {
50 result: ValidationResult::success(explanation, risk_level, approval_token, metadata),
51 auto_approved: false,
52 action: None,
53 validated_code_hash: None,
54 }
55 }
56
57 pub fn failure(violations: Vec<PolicyViolation>, metadata: ValidationMetadata) -> Self {
59 Self {
60 result: ValidationResult::failure(violations, metadata),
61 auto_approved: false,
62 action: None,
63 validated_code_hash: None,
64 }
65 }
66
67 pub fn from_result(result: ValidationResult) -> Self {
69 Self {
70 result,
71 auto_approved: false,
72 action: None,
73 validated_code_hash: None,
74 }
75 }
76
77 pub fn with_code_hash(mut self, hash: String) -> Self {
79 self.validated_code_hash = Some(hash);
80 self
81 }
82
83 pub fn with_action(mut self, action: UnifiedAction) -> Self {
85 self.action = Some(action);
86 self
87 }
88
89 pub fn with_auto_approved(mut self, auto_approved: bool) -> Self {
91 self.auto_approved = auto_approved;
92 self
93 }
94
95 pub fn with_warnings(mut self, warnings: Vec<String>) -> Self {
97 self.result.warnings = warnings;
98 self
99 }
100
101 pub fn to_json_response(&self) -> (Value, bool) {
105 let response = json!({
106 "valid": self.result.is_valid,
107 "explanation": self.result.explanation,
108 "risk_level": format!("{}", self.result.risk_level),
109 "approval_token": self.result.approval_token,
110 "action": self.action.as_ref().map(|a| a.to_string()),
111 "auto_approved": self.auto_approved,
112 "warnings": self.result.warnings,
113 "violations": self.result.violations.iter().map(|v| json!({
114 "policy": v.policy_name,
115 "rule": v.rule,
116 "message": v.message,
117 "suggestion": v.suggestion
118 })).collect::<Vec<_>>(),
119 "validated_code_hash": self.validated_code_hash,
120 "metadata": {
121 "is_read_only": self.result.metadata.is_read_only,
122 "accessed_types": self.result.metadata.accessed_types,
123 "accessed_fields": self.result.metadata.accessed_fields,
124 "validation_time_ms": self.result.metadata.validation_time_ms
125 }
126 });
127
128 (response, !self.result.is_valid)
129 }
130}
131
132#[async_trait::async_trait]
134pub trait CodeModeHandler: Send + Sync {
135 fn server_name(&self) -> &str;
137
138 fn is_enabled(&self) -> bool;
140
141 fn code_format(&self) -> &str;
143
144 async fn validate_code_impl(
146 &self,
147 code: &str,
148 variables: Option<&Value>,
149 dry_run: bool,
150 user_id: &str,
151 session_id: &str,
152 ) -> Result<ValidationResponse, String>;
153
154 async fn execute_code_impl(
156 &self,
157 code: &str,
158 approval_token: &str,
159 variables: Option<&Value>,
160 ) -> Result<Value, String>;
161
162 fn is_policy_configured(&self) -> bool {
168 false
169 }
170
171 fn is_avp_configured(&self) -> bool {
173 self.is_policy_configured()
174 }
175
176 async fn pre_handle_hook(&self) -> Result<Option<(Value, bool)>, String> {
182 Ok(None)
183 }
184
185 fn is_code_mode_tool(&self, name: &str) -> bool {
191 name == "validate_code" || name == "execute_code"
192 }
193
194 fn get_tools(&self) -> Vec<ToolInfo> {
196 if !self.is_enabled() {
197 return vec![];
198 }
199
200 CodeModeToolBuilder::new(self.code_format()).build_tools()
201 }
202
203 async fn handle_tool(
205 &self,
206 name: &str,
207 arguments: Value,
208 user_id: &str,
209 session_id: &str,
210 ) -> Result<(Value, bool), String> {
211 if !self.is_policy_configured() {
213 return Ok((
214 json!({
215 "error": "Code Mode requires a policy evaluator to be configured. \
216 Configure AVP, local Cedar, or another policy backend.",
217 "valid": false
218 }),
219 true,
220 ));
221 }
222
223 if let Some(response) = self.pre_handle_hook().await? {
225 return Ok(response);
226 }
227
228 match name {
229 "validate_code" => {
230 self.handle_validate_code(arguments, user_id, session_id)
231 .await
232 },
233 "execute_code" => self.handle_execute_code(arguments).await,
234 _ => Err(format!("Unknown Code Mode tool: {}", name)),
235 }
236 }
237
238 async fn handle_validate_code(
240 &self,
241 arguments: Value,
242 user_id: &str,
243 session_id: &str,
244 ) -> Result<(Value, bool), String> {
245 let mut input: ValidateCodeInput =
246 serde_json::from_value(arguments).map_err(|e| format!("Invalid arguments: {}", e))?;
247
248 input.code = input.code.trim().to_string();
249
250 let response = self
251 .validate_code_impl(
252 &input.code,
253 input.variables.as_ref(),
254 input.dry_run.unwrap_or(false),
255 user_id,
256 session_id,
257 )
258 .await?;
259
260 Ok(response.to_json_response())
261 }
262
263 async fn handle_execute_code(&self, arguments: Value) -> Result<(Value, bool), String> {
265 let mut input: ExecuteCodeInput =
266 serde_json::from_value(arguments).map_err(|e| format!("Invalid arguments: {}", e))?;
267
268 input.code = input.code.trim().to_string();
269
270 let result = self
271 .execute_code_impl(&input.code, &input.approval_token, input.variables.as_ref())
272 .await?;
273
274 Ok((result, false))
275 }
276}
277
278#[derive(Debug, Deserialize)]
280pub struct ValidateCodeInput {
281 pub code: String,
282 #[serde(default)]
283 pub variables: Option<Value>,
284 #[serde(default)]
285 pub format: Option<String>,
286 #[serde(default)]
287 pub dry_run: Option<bool>,
288}
289
290#[derive(Debug, Deserialize)]
292pub struct ExecuteCodeInput {
293 pub code: String,
294 pub approval_token: String,
295 #[serde(default)]
296 pub variables: Option<Value>,
297}
298
299pub struct CodeModeToolBuilder {
301 code_format: String,
302}
303
304impl CodeModeToolBuilder {
305 pub fn new(code_format: &str) -> Self {
307 Self {
308 code_format: code_format.to_string(),
309 }
310 }
311
312 pub fn build_tools(&self) -> Vec<ToolInfo> {
314 vec![self.build_validate_tool(), self.build_execute_tool()]
315 }
316
317 fn safe_read_annotations() -> ToolAnnotations {
332 ToolAnnotations::new()
333 .with_read_only(true)
334 .with_destructive(false)
335 .with_open_world(false)
336 .with_idempotent(true)
337 }
338
339 pub fn build_validate_tool(&self) -> ToolInfo {
340 ToolInfo::with_annotations(
341 "validate_code",
342 Some(
343 "Validates code and returns a business-language explanation with an approval token. \
344 The code is analyzed for security, complexity, and data access patterns. \
345 You MUST call this before execute_code."
346 .to_string(),
347 ),
348 json!({
349 "type": "object",
350 "properties": {
351 "code": {
352 "type": "string",
353 "description": "The code to validate"
354 },
355 "variables": {
356 "type": "object",
357 "description": "Optional variables for the query"
358 },
359 "format": {
360 "type": "string",
361 "enum": [&self.code_format],
362 "description": format!("Code format. Defaults to '{}' for this server.", self.code_format)
363 },
364 "dry_run": {
365 "type": "boolean",
366 "description": "If true, validate without generating approval token"
367 }
368 },
369 "required": ["code"]
370 }),
371 Self::safe_read_annotations(),
372 )
373 }
374
375 pub fn build_execute_tool(&self) -> ToolInfo {
392 ToolInfo::new(
393 "execute_code",
394 Some(
395 "Executes validated code using an approval token. \
396 The token must be obtained from validate_code and the code must match exactly."
397 .into(),
398 ),
399 json!({
400 "type": "object",
401 "properties": {
402 "code": {
403 "type": "string",
404 "description": "The code to execute (must match validated code)"
405 },
406 "approval_token": {
407 "type": "string",
408 "description": "The approval token from validate_code"
409 },
410 "variables": {
411 "type": "object",
412 "description": "Optional variables for the query"
413 }
414 },
415 "required": ["code", "approval_token"]
416 }),
417 )
418 }
419}
420
421pub fn format_error_response(error: &str) -> (Value, bool) {
423 (
424 json!({
425 "error": error,
426 "valid": false
427 }),
428 true,
429 )
430}
431
432pub fn format_execution_error(error: &str) -> (Value, bool) {
434 (
435 json!({
436 "error": error
437 }),
438 true,
439 )
440}
441
442#[cfg(test)]
443mod tests {
444 use super::*;
445
446 #[test]
447 fn test_validation_response_to_json() {
448 let response = ValidationResponse::success(
449 "Test explanation".into(),
450 RiskLevel::Low,
451 "token123".into(),
452 ValidationMetadata::default(),
453 )
454 .with_action(UnifiedAction::Read)
455 .with_auto_approved(true);
456
457 let (json, is_error) = response.to_json_response();
458
459 assert!(!is_error);
460 assert_eq!(json["valid"], true);
461 assert_eq!(json["explanation"], "Test explanation");
462 assert_eq!(json["risk_level"], "LOW");
463 assert_eq!(json["approval_token"], "token123");
464 assert_eq!(json["action"], "Read");
465 assert_eq!(json["auto_approved"], true);
466 }
467
468 #[test]
469 fn test_validation_response_failure() {
470 let violations = vec![PolicyViolation::new("policy", "rule", "message")];
471 let response = ValidationResponse::failure(violations, ValidationMetadata::default());
472
473 let (json, is_error) = response.to_json_response();
474
475 assert!(is_error);
476 assert_eq!(json["valid"], false);
477 }
478
479 #[test]
480 fn test_tool_builder() {
481 let builder = CodeModeToolBuilder::new("graphql");
482 let tools = builder.build_tools();
483
484 assert_eq!(tools.len(), 2);
485 assert_eq!(tools[0].name, "validate_code");
486 assert_eq!(tools[1].name, "execute_code");
487 }
488}