Skip to main content

winx_code_agent/server/
handler.rs

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        // Adaptive promotion is already backed by a Task reservation. Require
63        // the effective guardian epoch to be checked live before this first
64        // action so a recreated guardian cannot launch an untracked process
65        // under a stale adapter cache.
66        require_generation_binding: true,
67        ..ShellActionOptions::default()
68    }
69}
70
71/// Emit the cache fields required by MCP 2026-07-28 without changing the wire
72/// shape for clients that negotiated an older protocol revision.
73fn 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)] // rmcp's trait requires these async signatures
132impl 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        // A changed Roots list is only used as a retry if Winx is still
162        // uninitialized. Never switch an active shell's workspace implicitly.
163        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)] // request scoping, negotiated routing, execution, and telemetry
332    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        // Parse edit arguments once into the typed domain before any capability
347        // decision. This avoids policy decisions based on loosely typed/raw
348        // JSON fields and makes malformed edit calls recoverable tool results,
349        // even when another gate (workspace or capability) would also reject
350        // the request.
351        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}