1use crate::server::usage::lifecycle::{phase, Phase};
2
3use rmcp::{
4 model::{
5 CacheScope, CallToolRequestParams, CallToolResponse, CancelTaskParams,
6 GetPromptRequestParams, GetPromptResponse, GetPromptResult, GetTaskParams, GetTaskResult,
7 Icon, Implementation, ListPromptsResult, ListResourcesResult, ListToolsResult,
8 PaginatedRequestParams, PromptMessage, ProtocolVersion, ReadResourceRequestParams,
9 ReadResourceResponse, ReadResourceResult, Resource, ResourceContents, Role,
10 ServerCapabilities, ServerConfig, Tool, UpdateTaskParams,
11 },
12 service::{NotificationContext, RequestContext, RoleServer},
13 ErrorData as McpError, ServerHandler,
14};
15
16use super::catalog::{server_icon_data_uri, winx_prompts, winx_tools, winx_tools_for_policy};
17use super::outcomes;
18use super::principal::{
19 conversation_identity_from_context, principal_from_context, scope_tool_request,
20 session_affinity_from_context, task_belongs_to_principal,
21};
22use super::usage::{EffectiveWaitMode, UsageEvent};
23use super::WinxService;
24use crate::runtime::ShellActionOptions;
25use crate::tool_policy::ToolPolicy;
26use crate::tool_registry::ToolKind;
27use crate::tools::edit_files::EditSurface;
28use crate::types::{BashCommand, BashWaitPolicy};
29
30pub(crate) const COMPACT_BASH_OUTPUT_EXTENSION: &str = "io.winx/compact-bash-output";
31const ADAPTIVE_TASK_INLINE_SECONDS: f32 = 2.0;
32const RETURN_EARLY_DEFAULT_SECONDS: f32 = 0.25;
33const RETURN_EARLY_MAX_SECONDS: f32 = 5.0;
34const SYNCHRONOUS_MAX_SECONDS: f32 = 60.0;
35
36#[derive(Clone, Copy, Debug, Eq, PartialEq)]
37enum BashTaskRoute {
38 Synchronous,
39 Adaptive,
40 Immediate,
41}
42
43fn bash_task_route(
44 client_tasks: bool,
45 generation_bound_runtime: bool,
46 eligible: bool,
47 policy: BashWaitPolicy,
48) -> BashTaskRoute {
49 if !client_tasks || !generation_bound_runtime || !eligible {
50 return BashTaskRoute::Synchronous;
51 }
52 match policy {
53 BashWaitPolicy::Adaptive => BashTaskRoute::Adaptive,
54 BashWaitPolicy::UntilComplete => BashTaskRoute::Immediate,
55 BashWaitPolicy::ReturnEarly => BashTaskRoute::Synchronous,
56 }
57}
58
59fn adaptive_action_options(compact_output: bool) -> ShellActionOptions {
60 ShellActionOptions {
61 compact_output,
62 require_generation_binding: true,
67 ..ShellActionOptions::default()
68 }
69}
70
71fn cache_hints_for_protocol(
74 protocol_version: Option<ProtocolVersion>,
75 cache_scope: CacheScope,
76) -> (Option<u64>, Option<CacheScope>) {
77 if protocol_version.is_some_and(|version| version >= ProtocolVersion::V_2026_07_28) {
78 (Some(0), Some(cache_scope))
79 } else {
80 (None, None)
81 }
82}
83
84fn supports_compact_bash_output(context: &RequestContext<RoleServer>) -> bool {
85 context.client_capabilities().is_some_and(|capabilities| {
86 capabilities
87 .extensions
88 .as_ref()
89 .is_some_and(|extensions| extensions.contains_key(COMPACT_BASH_OUTPUT_EXTENSION))
90 })
91}
92
93fn normalized_wait_request(
94 mut request: CallToolRequestParams,
95 policy: BashWaitPolicy,
96 task_inline: bool,
97) -> Result<CallToolRequestParams, McpError> {
98 let mut arguments = request
99 .arguments
100 .clone()
101 .ok_or_else(|| McpError::invalid_request("Missing BashCommand arguments", None))?;
102 let requested = arguments
103 .get("wait_for_seconds")
104 .cloned()
105 .map(serde_json::from_value::<f32>)
106 .transpose()
107 .map_err(|error| {
108 McpError::invalid_request(format!("Invalid wait_for_seconds: {error}"), None)
109 })?;
110 let (default, maximum) = match (policy, task_inline) {
111 (BashWaitPolicy::Adaptive, true) => {
112 (ADAPTIVE_TASK_INLINE_SECONDS, ADAPTIVE_TASK_INLINE_SECONDS)
113 }
114 (BashWaitPolicy::ReturnEarly, _) => {
115 (RETURN_EARLY_DEFAULT_SECONDS, RETURN_EARLY_MAX_SECONDS)
116 }
117 (BashWaitPolicy::Adaptive, false) => (15.0, SYNCHRONOUS_MAX_SECONDS),
118 (BashWaitPolicy::UntilComplete, false) => {
119 (SYNCHRONOUS_MAX_SECONDS, SYNCHRONOUS_MAX_SECONDS)
120 }
121 (BashWaitPolicy::UntilComplete, true) => unreachable!("until_complete starts a Task"),
122 };
123 arguments.insert(
124 "wait_for_seconds".to_string(),
125 serde_json::Value::from(requested.unwrap_or(default).clamp(0.0, maximum)),
126 );
127 request.arguments = Some(arguments);
128 Ok(request)
129}
130
131#[allow(clippy::unused_async_trait_impl)] impl ServerHandler for WinxService {
133 fn get_info(&self) -> ServerConfig {
134 let mut capabilities = ServerCapabilities::builder()
135 .enable_tools()
136 .enable_resources()
137 .enable_prompts()
138 .enable_tasks()
139 .build();
140 capabilities
141 .extensions
142 .get_or_insert_default()
143 .insert(COMPACT_BASH_OUTPUT_EXTENSION.to_string(), rmcp::model::JsonObject::new());
144 ServerConfig::new(capabilities)
145 .with_server_info(
146 Implementation::new("winx-mcp-server", self.version.clone())
147 .with_title("Winx High-Performance MCP")
148 .with_icons(vec![Icon::new(server_icon_data_uri())
149 .with_mime_type("image/png")
150 .with_sizes(vec!["96x96".to_string()])]),
151 )
152 .with_protocol_version(ProtocolVersion::V_2026_07_28)
153 .with_instructions(crate::utils::orchestration::server_instructions())
154 }
155
156 async fn on_initialized(&self, context: NotificationContext<RoleServer>) {
157 self.initialize_from_client_roots(context).await;
158 }
159
160 async fn on_roots_list_changed(&self, context: NotificationContext<RoleServer>) {
161 self.initialize_from_client_roots(context).await;
164 }
165
166 fn get_tool(&self, name: &str) -> Option<Tool> {
167 winx_tools().into_iter().find(|tool| tool.name == name)
168 }
169
170 async fn get_task(
171 &self,
172 request: GetTaskParams,
173 context: RequestContext<RoleServer>,
174 ) -> Result<GetTaskResult, McpError> {
175 let principal = principal_from_context(&context);
176 if !task_belongs_to_principal(&request.task_id, principal.as_ref()) {
177 return Err(McpError::invalid_request(
178 format!("Unknown or expired task: {}", request.task_id),
179 None,
180 ));
181 }
182 let mut tasks = self.tasks.lock().await;
183 let entry = tasks.get(&request.task_id).ok_or_else(|| {
184 McpError::invalid_request(format!("Unknown or expired task: {}", request.task_id), None)
185 })?;
186 Ok(GetTaskResult::new(entry.detailed()))
187 }
188
189 async fn update_task(
190 &self,
191 request: UpdateTaskParams,
192 context: RequestContext<RoleServer>,
193 ) -> Result<(), McpError> {
194 let principal = principal_from_context(&context);
195 if !task_belongs_to_principal(&request.task_id, principal.as_ref()) {
196 return Err(McpError::invalid_request(
197 format!("Unknown or expired task: {}", request.task_id),
198 None,
199 ));
200 }
201 let mut tasks = self.tasks.lock().await;
202 tasks.get(&request.task_id).ok_or_else(|| {
203 McpError::invalid_request(format!("Unknown or expired task: {}", request.task_id), None)
204 })?;
205 Ok(())
206 }
207
208 async fn cancel_task(
209 &self,
210 request: CancelTaskParams,
211 context: RequestContext<RoleServer>,
212 ) -> Result<(), McpError> {
213 let principal = principal_from_context(&context);
214 if !task_belongs_to_principal(&request.task_id, principal.as_ref()) {
215 return Err(McpError::invalid_request(
216 format!("Unknown or expired task: {}", request.task_id),
217 None,
218 ));
219 }
220 self.cancel_bash_task(&request.task_id).await
221 }
222
223 async fn list_tools(
224 &self,
225 _request: Option<PaginatedRequestParams>,
226 ctx: RequestContext<RoleServer>,
227 ) -> Result<ListToolsResult, McpError> {
228 let principal = principal_from_context(&ctx);
229 let (tools, scope) = principal.as_ref().map_or_else(
230 || (winx_tools(), CacheScope::Public),
231 |principal| {
232 let policy = principal.tool_policy();
233 let scope = if policy == crate::tool_policy::ToolPolicy::default() {
234 CacheScope::Public
235 } else {
236 CacheScope::Private
237 };
238 (winx_tools_for_policy(policy), scope)
239 },
240 );
241 let (ttl_ms, cache_scope) = cache_hints_for_protocol(ctx.protocol_version(), scope);
242 let mut result = ListToolsResult::with_all_items(tools);
243 result.ttl_ms = ttl_ms;
244 result.cache_scope = cache_scope;
245 Ok(result)
246 }
247
248 async fn list_resources(
249 &self,
250 _request: Option<PaginatedRequestParams>,
251 ctx: RequestContext<RoleServer>,
252 ) -> Result<ListResourcesResult, McpError> {
253 let (ttl_ms, cache_scope) =
254 cache_hints_for_protocol(ctx.protocol_version(), CacheScope::Public);
255 let mut result =
256 ListResourcesResult::with_all_items(vec![Resource::new("file://readme", "README")
257 .with_description("Project README documentation")
258 .with_mime_type("text/markdown")]);
259 result.ttl_ms = ttl_ms;
260 result.cache_scope = cache_scope;
261 Ok(result)
262 }
263
264 async fn list_prompts(
265 &self,
266 _request: Option<PaginatedRequestParams>,
267 ctx: RequestContext<RoleServer>,
268 ) -> Result<ListPromptsResult, McpError> {
269 let (ttl_ms, cache_scope) =
270 cache_hints_for_protocol(ctx.protocol_version(), CacheScope::Public);
271 let mut result = ListPromptsResult::with_all_items(winx_prompts());
272 result.ttl_ms = ttl_ms;
273 result.cache_scope = cache_scope;
274 Ok(result)
275 }
276
277 async fn get_prompt(
278 &self,
279 request: GetPromptRequestParams,
280 context: RequestContext<RoleServer>,
281 ) -> Result<GetPromptResponse, McpError> {
282 if request.name != "KnowledgeTransfer" {
283 return Err(McpError::invalid_request(
284 format!("Unknown prompt: {}", request.name),
285 None,
286 ));
287 }
288
289 let principal = principal_from_context(&context);
290 let session_prefix = principal.as_ref().map(crate::config::HttpPrincipal::session_prefix);
291 let text = crate::utils::redact::redact(
292 &self.knowledge_transfer_prompt_text(session_prefix.as_deref()).await,
293 )
294 .into_owned();
295 Ok(GetPromptResult::new(vec![PromptMessage::new_text(Role::User, text)])
296 .with_description("Knowledge transfer handoff prompt")
297 .into())
298 }
299
300 async fn read_resource(
301 &self,
302 request: ReadResourceRequestParams,
303 ctx: RequestContext<RoleServer>,
304 ) -> Result<ReadResourceResponse, McpError> {
305 let content = match request.uri.as_ref() {
306 "file://readme" => match tokio::fs::read_to_string("README.md").await {
307 Ok(content) => vec![ResourceContents::text(
308 crate::utils::redact::redact(&content).into_owned(),
309 request.uri.clone(),
310 )],
311 Err(_) => vec![ResourceContents::text(
312 "README.md not found".to_string(),
313 request.uri.clone(),
314 )],
315 },
316 _ => {
317 return Err(McpError::invalid_request(
318 format!("Unknown resource URI: {}", request.uri),
319 None,
320 ));
321 }
322 };
323 let (ttl_ms, cache_scope) =
324 cache_hints_for_protocol(ctx.protocol_version(), CacheScope::Private);
325 let mut result = ReadResourceResult::new(content);
326 result.ttl_ms = ttl_ms;
327 result.cache_scope = cache_scope;
328 Ok(result.into())
329 }
330
331 #[allow(clippy::too_many_lines)] async fn call_tool(
333 &self,
334 request: CallToolRequestParams,
335 context: RequestContext<RoleServer>,
336 ) -> Result<CallToolResponse, McpError> {
337 let tool_kind = ToolKind::parse(request.name.as_ref());
338 if tool_kind.is_none() {
339 return Err(McpError::invalid_request(format!("Unknown tool: {}", request.name), None));
340 }
341 let principal = principal_from_context(&context);
342 let effective_tool_policy = principal
343 .as_ref()
344 .map_or_else(ToolPolicy::default, super::super::config::HttpPrincipal::tool_policy);
345
346 let edit_surface = tool_kind.and_then(EditSurface::from_public_tool);
352 let normalized_edit = if let Some(surface) = edit_surface {
353 let arguments = request
354 .arguments
355 .clone()
356 .map_or(serde_json::Value::Null, serde_json::Value::Object);
357 match crate::tools::edit_files::normalize_edit_call(surface, arguments.clone()) {
358 Ok(normalized) => Some(normalized),
359 Err(error) => {
360 let mut result =
361 outcomes::tool_failure(request.name.as_ref(), &error, Some(&arguments))?;
362 outcomes::enforce_next_action_policy(&mut result, effective_tool_policy);
363 outcomes::finalize_tool_result(&mut result);
364 return Ok(result.into());
365 }
366 }
367 } else {
368 None
369 };
370 let policy_allows_call =
371 tool_kind.is_some_and(|kind| effective_tool_policy.allows_call_kind(kind));
372 if !policy_allows_call {
373 return Err(McpError::invalid_request(
374 format!("Tool is not available for this principal: {}", request.name),
375 None,
376 ));
377 }
378 if normalized_edit.as_ref().is_some_and(|edit| edit.verification.is_some())
379 && !effective_tool_policy.edit_permissions().allows_verification()
380 {
381 return Err(McpError::invalid_request(
382 format!("{} verification requires BashCommand authority", request.name),
383 None,
384 ));
385 }
386 let affinity = session_affinity_from_context(&context);
387 let conversation_identity = conversation_identity_from_context(&context);
388 let (request, scope) =
389 scope_tool_request(request, principal, affinity, conversation_identity.as_deref())
390 .map_err(|error| {
391 McpError::invalid_request(format!("Cannot scope remote request: {error}"), None)
392 })?;
393 let mut usage = UsageEvent::start(&request, &scope, &context);
394 let trace = usage.trace();
395 Box::pin(trace.scope(async {
396 let guard = phase(Phase::WorkspaceCheck);
397 let workspace_result = self
398 .validate_workspace_coherence(
399 &request,
400 &scope,
401 affinity,
402 conversation_identity.as_deref(),
403 )
404 .await;
405 guard.finish(workspace_result.is_ok());
406 match workspace_result {
407 Ok(coherence) => usage.set_workspace_coherence(coherence.as_str()),
408 Err(error) => {
409 usage.set_workspace_coherence("rejected");
410 let arguments = request.arguments.clone().map(serde_json::Value::Object);
411 let guard = phase(Phase::ResponseFinalize);
412 let mut result =
413 outcomes::tool_failure(request.name.as_ref(), &error, arguments.as_ref())?;
414 outcomes::enforce_next_action_policy(&mut result, effective_tool_policy);
415 outcomes::finalize_tool_result(&mut result);
416 scope.unscope_result(&mut result);
417 let status = outcomes::result_status(&result);
418 let response_bytes = outcomes::result_size_bytes(&result);
419 guard.finish(true);
420 usage.emit("tool_error", &status, response_bytes, Some(&result));
421 return Ok(result.into());
422 }
423 }
424 let compact_bash_output = supports_compact_bash_output(&context);
425 let supports_tasks = context
426 .client_capabilities()
427 .is_some_and(|capabilities| capabilities.supports_tasks());
428 let wait_policy = if tool_kind == Some(ToolKind::BashCommand) {
429 Some(Self::bash_wait_policy(&request)?)
430 } else {
431 None
432 };
433 if wait_policy == Some(BashWaitPolicy::UntilComplete) {
434 let parsed_bash: Option<BashCommand> = Self::parse_ordinary_bash_request(&request);
435 if let Some(bash) = parsed_bash.as_ref() {
436 let is_foreground_command = matches!(
437 bash.action_json,
438 crate::types::BashCommandAction::Command { is_background: false, .. }
439 );
440 if !is_foreground_command {
441 let action = match &bash.action_json {
442 crate::types::BashCommandAction::Command {
443 is_background: true,
444 ..
445 } => "background_command",
446 crate::types::BashCommandAction::Command { .. } => "foreground_command",
447 crate::types::BashCommandAction::StatusCheck { .. } => "status_check",
448 crate::types::BashCommandAction::SendText { .. } => "send_text",
449 crate::types::BashCommandAction::SendSpecials { .. } => "send_specials",
450 crate::types::BashCommandAction::SendAscii { .. } => "send_ascii",
451 crate::types::BashCommandAction::Screen { .. } => "screen",
452 crate::types::BashCommandAction::WaitForTurn { .. } => "wait_for_turn",
453 crate::types::BashCommandAction::Inspect { .. } => "inspect",
454 crate::types::BashCommandAction::Prompt { .. } => "prompt",
455 };
456 let error = crate::errors::WinxError::InvalidWaitPolicyForAction {
457 wait_policy: "until_complete".to_string(),
458 action: action.to_string(),
459 };
460 let arguments = request.arguments.clone().map(serde_json::Value::Object);
461 let guard = phase(Phase::ResponseFinalize);
462 let mut result =
463 outcomes::tool_failure("BashCommand", &error, arguments.as_ref())?;
464 outcomes::enforce_next_action_policy(&mut result, effective_tool_policy);
465 outcomes::finalize_tool_result(&mut result);
466 scope.unscope_result(&mut result);
467 let status = outcomes::result_status(&result);
468 let response_bytes = outcomes::result_size_bytes(&result);
469 guard.finish(true);
470 usage.emit("tool_error", &status, response_bytes, Some(&result));
471 return Ok(result.into());
472 }
473 }
474 }
475
476 let eligible = Self::bash_task_is_eligible(&request);
477 let generation_bound_runtime = if supports_tasks && eligible {
478 let thread_id = request
479 .arguments
480 .as_ref()
481 .and_then(|arguments| arguments.get("thread_id"))
482 .and_then(serde_json::Value::as_str)
483 .unwrap_or_default();
484 let (slot, session_guard) = self.tool_session_for(thread_id).await;
485 let supported = self
486 .shell_runtime
487 .supports_generation_bound_actions_for(&slot)
488 .await
489 .unwrap_or(false);
490 drop(session_guard);
491 supported
492 } else {
493 false
494 };
495 let route = wait_policy.map_or(BashTaskRoute::Synchronous, |policy| {
496 bash_task_route(supports_tasks, generation_bound_runtime, eligible, policy)
497 });
498
499 match route {
500 BashTaskRoute::Immediate => {
501 let reservation = self.reserve_bash_task(&request, &scope).await?;
502 let effective_wait = usage.effective_wait_for(
503 &request,
504 BashWaitPolicy::UntilComplete,
505 EffectiveWaitMode::Task,
506 );
507 return match self
508 .start_reserved_bash_task(
509 reservation,
510 request,
511 scope,
512 None,
513 compact_bash_output,
514 )
515 .await
516 {
517 Ok(task) => {
518 usage.record_effective_wait(effective_wait);
519 usage.emit("task", "working", 0, None);
520 Ok(CallToolResponse::Task(task))
521 }
522 Err(error) => {
523 usage.emit("protocol_error", "failed", 0, None);
524 Err(error)
525 }
526 };
527 }
528 BashTaskRoute::Adaptive => {
529 let reservation = self.reserve_bash_task(&request, &scope).await?;
530 let task_request = request.clone();
531 let inline_request =
532 normalized_wait_request(request, BashWaitPolicy::Adaptive, true)?;
533 let effective_wait = usage.effective_wait_for(
534 &inline_request,
535 BashWaitPolicy::Adaptive,
536 EffectiveWaitMode::TaskInline,
537 );
538 match self
539 .execute_tool_call(
540 inline_request,
541 adaptive_action_options(compact_bash_output),
542 effective_tool_policy.edit_permissions(),
543 )
544 .await
545 {
546 Ok(execution)
547 if outcomes::result_status(&execution.result) == "running" =>
548 {
549 usage.record_effective_wait(effective_wait);
550 return match self
551 .start_reserved_bash_task(
552 reservation,
553 task_request,
554 scope,
555 Some(execution),
556 compact_bash_output,
557 )
558 .await
559 {
560 Ok(task) => {
561 usage.emit("task", "working", 0, None);
562 Ok(CallToolResponse::Task(task))
563 }
564 Err(error) => {
565 usage.emit("protocol_error", "failed", 0, None);
566 Err(error)
567 }
568 };
569 }
570 Ok(mut execution) => {
571 usage.record_effective_wait(effective_wait);
572 self.release_bash_task(&reservation).await;
573 let guard = phase(Phase::ResponseFinalize);
574 outcomes::enforce_next_action_policy(
575 &mut execution.result,
576 effective_tool_policy,
577 );
578 scope.unscope_result(&mut execution.result);
579 let status = outcomes::result_status(&execution.result);
580 let outcome = if execution.result.is_error == Some(true) {
581 "tool_error"
582 } else {
583 "ok"
584 };
585 let response_bytes = outcomes::result_size_bytes(&execution.result);
586 guard.finish(true);
587 usage.emit(outcome, &status, response_bytes, Some(&execution.result));
588 return Ok(execution.result.into());
589 }
590 Err(mut error) => {
591 self.release_bash_task(&reservation).await;
592 let guard = phase(Phase::ResponseFinalize);
593 scope.unscope_error(&mut error);
594 guard.finish(true);
595 usage.emit("protocol_error", "failed", 0, None);
596 return Err(error);
597 }
598 }
599 }
600 BashTaskRoute::Synchronous => {}
601 }
602
603 let workflow_authority = super::workflow_types::WorkflowAuthority {
604 principal_key: scope.principal().map_or_else(
605 || "local".to_string(),
606 |principal| format!("http:{}", principal.id()),
607 ),
608 bash_allowed: effective_tool_policy.allows_call_kind(ToolKind::BashCommand),
609 };
610 let request = match wait_policy {
611 Some(policy) => normalized_wait_request(request, policy, false)?,
612 None => request,
613 };
614 let effective_wait = wait_policy.and_then(|policy| {
615 usage.effective_wait_for(&request, policy, EffectiveWaitMode::Synchronous)
616 });
617 match self
618 .execute_tool_call_with_authority(
619 request,
620 ShellActionOptions {
621 compact_output: compact_bash_output,
622 ..ShellActionOptions::default()
623 },
624 effective_tool_policy.edit_permissions(),
625 workflow_authority,
626 )
627 .await
628 {
629 Ok(mut execution) => {
630 usage.record_effective_wait(effective_wait);
631 let guard = phase(Phase::ResponseFinalize);
632 let result = &mut execution.result;
633 outcomes::enforce_next_action_policy(result, effective_tool_policy);
634 scope.unscope_result(result);
635 let status = outcomes::result_status(result);
636 let outcome = if result.is_error == Some(true) { "tool_error" } else { "ok" };
637 let response_bytes = outcomes::result_size_bytes(result);
638 guard.finish(true);
639 usage.emit(outcome, &status, response_bytes, Some(result));
640 Ok(execution.result.into())
641 }
642 Err(mut error) => {
643 let guard = phase(Phase::ResponseFinalize);
644 scope.unscope_error(&mut error);
645 guard.finish(true);
646 usage.emit("protocol_error", "failed", 0, None);
647 Err(error)
648 }
649 }
650 }))
651 .await
652 }
653}
654
655#[cfg(test)]
656mod tests {
657 use super::super::usage::usage_result_metadata;
658 use super::*;
659 use rmcp::model::CallToolResult;
660
661 mod wait_metrics;
662
663 #[test]
664 fn cache_hints_follow_negotiated_protocol_version() {
665 assert_eq!(
666 cache_hints_for_protocol(Some(ProtocolVersion::V_2026_07_28), CacheScope::Private,),
667 (Some(0), Some(CacheScope::Private))
668 );
669 assert_eq!(
670 cache_hints_for_protocol(Some(ProtocolVersion::V_2026_07_28), CacheScope::Public),
671 (Some(0), Some(CacheScope::Public))
672 );
673 assert_eq!(
674 cache_hints_for_protocol(Some(ProtocolVersion::V_2025_11_25), CacheScope::Private,),
675 (None, None)
676 );
677 assert_eq!(cache_hints_for_protocol(None, CacheScope::Private), (None, None));
678 }
679
680 #[test]
681 fn guardian_1_4_uses_synchronous_policy_fallback_even_for_task_clients() {
682 assert_eq!(
683 bash_task_route(true, false, true, BashWaitPolicy::Adaptive),
684 BashTaskRoute::Synchronous
685 );
686 assert_eq!(
687 bash_task_route(true, false, true, BashWaitPolicy::UntilComplete),
688 BashTaskRoute::Synchronous
689 );
690 }
691
692 #[test]
693 fn task_routing_requires_client_runtime_and_foreground_eligibility() {
694 assert_eq!(
695 bash_task_route(true, true, true, BashWaitPolicy::Adaptive),
696 BashTaskRoute::Adaptive
697 );
698 assert_eq!(
699 bash_task_route(true, true, true, BashWaitPolicy::UntilComplete),
700 BashTaskRoute::Immediate
701 );
702 assert_eq!(
703 bash_task_route(true, true, true, BashWaitPolicy::ReturnEarly),
704 BashTaskRoute::Synchronous
705 );
706 assert_eq!(
707 bash_task_route(false, true, true, BashWaitPolicy::UntilComplete),
708 BashTaskRoute::Synchronous
709 );
710 assert_eq!(
711 bash_task_route(true, true, false, BashWaitPolicy::UntilComplete),
712 BashTaskRoute::Synchronous
713 );
714 }
715
716 #[test]
717 #[allow(clippy::expect_used)]
718 fn receipt_bound_verify_routing_does_not_emit_false_errors() {
719 use std::sync::atomic::{AtomicUsize, Ordering};
720 use std::sync::Arc;
721 use tracing_subscriber::{layer::SubscriberExt, Layer};
722
723 struct ErrorCounter(Arc<AtomicUsize>);
724 impl<S: tracing::Subscriber> Layer<S> for ErrorCounter {
725 fn on_event(
726 &self,
727 event: &tracing::Event<'_>,
728 _context: tracing_subscriber::layer::Context<'_, S>,
729 ) {
730 if *event.metadata().level() == tracing::Level::ERROR {
731 self.0.fetch_add(1, Ordering::SeqCst);
732 }
733 }
734 }
735 let errors = Arc::new(AtomicUsize::new(0));
736 let subscriber = tracing_subscriber::registry().with(ErrorCounter(Arc::clone(&errors)));
737 let arguments = serde_json::json!({
738 "action_json": {
739 "type": "verify",
740 "verification_id": "verify_123",
741 "command": "cargo test"
742 },
743 "thread_id": "thread"
744 });
745 let request = CallToolRequestParams::new("BashCommand")
746 .with_arguments(arguments.as_object().expect("arguments object").clone());
747 tracing::subscriber::with_default(subscriber, || {
748 assert!(WinxService::parse_ordinary_bash_request(&request).is_none());
749 assert!(!WinxService::bash_task_is_eligible(&request));
750 for (action, eligible) in [
751 (serde_json::json!({"type": "status_check", "status_check": true}), false),
752 (
753 serde_json::json!({"type": "command", "command": "true", "is_background": true}),
754 false,
755 ),
756 (serde_json::json!({"type": "command", "command": "true"}), true),
757 ] {
758 let arguments = serde_json::json!({"action_json": action, "thread_id": "thread"});
759 let ordinary = CallToolRequestParams::new("BashCommand")
760 .with_arguments(arguments.as_object().expect("arguments object").clone());
761 assert!(WinxService::parse_ordinary_bash_request(&ordinary).is_some());
762 assert_eq!(WinxService::bash_task_is_eligible(&ordinary), eligible);
763 }
764 });
765 assert_eq!(
766 errors.load(Ordering::SeqCst),
767 0,
768 "a valid verify action must not emit a parser error during task routing"
769 );
770 }
771
772 #[test]
773 fn adaptive_action_requires_live_generation_binding() {
774 let options = adaptive_action_options(true);
775 assert!(options.compact_output);
776 assert!(options.require_generation_binding);
777 }
778
779 #[test]
780 fn usage_metadata_reads_image_code_map_and_temp_envelopes() {
781 let result_with = |structured| {
782 let mut result = CallToolResult::success(Vec::new());
783 result.structured_content = Some(structured);
784 result
785 };
786 let image = result_with(serde_json::json!({
787 "data": {"result": {
788 "source_bytes": 17_000_000,
789 "delivered_bytes": 2_000_000,
790 "transcoded": true,
791 "deduplicated": false
792 }}
793 }));
794 let metadata = usage_result_metadata(Some(&image));
795 assert_eq!(metadata.source_bytes, 17_000_000);
796 assert_eq!(metadata.payload_bytes, 2_000_000);
797 assert!(metadata.image_transcoded);
798 assert!(!metadata.image_deduplicated);
799
800 let code_map = result_with(serde_json::json!({
801 "source_kind": "canonical",
802 "payload_bytes": 12_345
803 }));
804 let metadata = usage_result_metadata(Some(&code_map));
805 assert_eq!(metadata.source_kind, "canonical");
806 assert_eq!(metadata.payload_bytes, 12_345);
807
808 let temporary = result_with(serde_json::json!({
809 "data": {
810 "session_files": 129,
811 "session_bytes": 4096,
812 "stale_pruned_files": 32,
813 "stale_pruned_bytes": 2048,
814 "temporary_artifact_cleanup_required": true
815 }
816 }));
817 let metadata = usage_result_metadata(Some(&temporary));
818 assert_eq!(metadata.temporary_session_files, 129);
819 assert_eq!(metadata.temporary_session_bytes, 4096);
820 assert_eq!(metadata.temporary_stale_pruned_files, 32);
821 assert_eq!(metadata.temporary_stale_pruned_bytes, 2048);
822 assert!(metadata.temporary_over_budget);
823
824 let edit_follow_up = result_with(serde_json::json!({
825 "errorCode": "verification_failed",
826 "retrySameCall": false,
827 "nextAction": {"tool": "VerifyEdit"},
828 "requiredReads": [],
829 "data": {
830 "edit_applied": true,
831 "verification_status": "failed",
832 "mutation_transition": "committed",
833 "mutation_replayed": false,
834 "mutation_receipt_persisted": true
835 }
836 }));
837 let metadata = usage_result_metadata(Some(&edit_follow_up));
838 assert_eq!(metadata.error_code, "verification_failed");
839 assert_eq!(metadata.recovery.next_action_tool, "VerifyEdit");
840 assert_eq!(metadata.recovery.required_read_count, 0);
841 assert!(!metadata.recovery.retry_same_call);
842 assert_eq!(metadata.recovery.edit_applied, Some(true));
843 assert!(!metadata.recovery.fresh_read_required);
844 assert_eq!(metadata.recovery.verification_status, "failed");
845 assert_eq!(metadata.recovery.mutation_transition, "committed");
846 assert_eq!(metadata.recovery.mutation_receipt_state, "persisted");
847
848 let edit_conflict = result_with(serde_json::json!({
849 "errorCode": "search_block_not_found",
850 "retrySameCall": false,
851 "nextAction": {"tool": "ReadFiles"},
852 "requiredReads": [{"path": "/workspace/lib.rs", "ranges": []}],
853 "data": {
854 "edit_applied": false,
855 "fresh_read_required": true,
856 "recovery_attempt": 2,
857 "recovery_escalated": true
858 }
859 }));
860 let metadata = usage_result_metadata(Some(&edit_conflict));
861 assert_eq!(metadata.error_code, "search_block_not_found");
862 assert_eq!(metadata.recovery.next_action_tool, "ReadFiles");
863 assert_eq!(metadata.recovery.required_read_count, 1);
864 assert!(!metadata.recovery.retry_same_call);
865 assert_eq!(metadata.recovery.edit_applied, Some(false));
866 assert!(metadata.recovery.fresh_read_required);
867 assert_eq!(metadata.recovery.verification_status, "");
868 assert_eq!(metadata.recovery.recovery_attempt, 2);
869 assert_eq!(metadata.recovery.recovery_level, "escalated");
870 }
871
872 #[test]
873 #[allow(clippy::expect_used, clippy::float_cmp)]
874 fn synchronous_wait_policies_have_explicit_upper_bounds() {
875 let request = |wait_for_seconds: Option<f32>| {
876 let mut arguments = serde_json::Map::new();
877 arguments.insert("command".to_string(), serde_json::Value::String("pwd".to_string()));
878 if let Some(wait) = wait_for_seconds {
879 arguments.insert("wait_for_seconds".to_string(), serde_json::json!(wait));
880 }
881 CallToolRequestParams::new("BashCommand").with_arguments(arguments)
882 };
883 let normalized = |policy, wait| {
884 normalized_wait_request(request(wait), policy, false)
885 .expect("valid wait request")
886 .arguments
887 .and_then(|arguments| arguments.get("wait_for_seconds").cloned())
888 .and_then(|value| value.as_f64())
889 .expect("normalized wait")
890 };
891
892 assert_eq!(normalized(BashWaitPolicy::Adaptive, Some(500.0)), 60.0);
893 assert_eq!(normalized(BashWaitPolicy::UntilComplete, None), 60.0);
894 assert_eq!(normalized(BashWaitPolicy::ReturnEarly, Some(500.0)), 5.0);
895 assert_eq!(normalized(BashWaitPolicy::ReturnEarly, None), 0.25);
896 }
897}