rf-mcp 0.34.0.dev2__tar.gz → 0.34.0.dev4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/PKG-INFO +1 -1
  2. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/pyproject.toml +1 -1
  3. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/components/execution/desktop_display_safety.py +59 -0
  4. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/components/execution/keyword_executor.py +241 -10
  5. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/components/test_builder.py +51 -0
  6. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/plugins/builtin/platynui_plugin.py +104 -0
  7. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/.gitignore +0 -0
  8. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/.kilo/node_modules/@msgpackr-extract/msgpackr-extract-linux-x64/README.md +0 -0
  9. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/.kilo/node_modules/@standard-schema/spec/LICENSE +0 -0
  10. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/.kilo/node_modules/@standard-schema/spec/README.md +0 -0
  11. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/.kilo/node_modules/cross-spawn/LICENSE +0 -0
  12. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/.kilo/node_modules/cross-spawn/README.md +0 -0
  13. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/.kilo/node_modules/detect-libc/LICENSE +0 -0
  14. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/.kilo/node_modules/detect-libc/README.md +0 -0
  15. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/.kilo/node_modules/effect/LICENSE +0 -0
  16. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/.kilo/node_modules/effect/README.md +0 -0
  17. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/.kilo/node_modules/fast-check/LICENSE +0 -0
  18. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/.kilo/node_modules/fast-check/README.md +0 -0
  19. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/.kilo/node_modules/find-my-way-ts/LICENSE +0 -0
  20. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/.kilo/node_modules/find-my-way-ts/README.md +0 -0
  21. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/.kilo/node_modules/ini/LICENSE +0 -0
  22. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/.kilo/node_modules/ini/README.md +0 -0
  23. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/.kilo/node_modules/isexe/LICENSE +0 -0
  24. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/.kilo/node_modules/isexe/README.md +0 -0
  25. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/.kilo/node_modules/kubernetes-types/README.md +0 -0
  26. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/.kilo/node_modules/msgpackr/LICENSE +0 -0
  27. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/.kilo/node_modules/msgpackr/README.md +0 -0
  28. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/.kilo/node_modules/msgpackr-extract/LICENSE +0 -0
  29. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/.kilo/node_modules/msgpackr-extract/README.md +0 -0
  30. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/.kilo/node_modules/multipasta/LICENSE +0 -0
  31. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/.kilo/node_modules/multipasta/README.md +0 -0
  32. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/.kilo/node_modules/node-gyp-build-optional-packages/LICENSE +0 -0
  33. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/.kilo/node_modules/node-gyp-build-optional-packages/README.md +0 -0
  34. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/.kilo/node_modules/pure-rand/LICENSE +0 -0
  35. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/.kilo/node_modules/pure-rand/README.md +0 -0
  36. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/.kilo/node_modules/toml/LICENSE +0 -0
  37. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/.kilo/node_modules/toml/README.md +0 -0
  38. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/.kilo/node_modules/uuid/README.md +0 -0
  39. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/.kilo/node_modules/which/LICENSE +0 -0
  40. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/.kilo/node_modules/which/README.md +0 -0
  41. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/.kilo/node_modules/yaml/LICENSE +0 -0
  42. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/.kilo/node_modules/yaml/README.md +0 -0
  43. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/.kilo/node_modules/zod/LICENSE +0 -0
  44. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/.kilo/node_modules/zod/README.md +0 -0
  45. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/LICENSE +0 -0
  46. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/README.md +0 -0
  47. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/__init__.py +0 -0
  48. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/adapters/__init__.py +0 -0
  49. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/adapters/adapter_factory.py +0 -0
  50. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/adapters/browser_adapter.py +0 -0
  51. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/adapters/page_state_adapter.py +0 -0
  52. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/adapters/playwright_adapter.py +0 -0
  53. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/adapters/recovery_adapter.py +0 -0
  54. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/adapters/selenium_adapter.py +0 -0
  55. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/attach/__init__.py +0 -0
  56. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/attach/mcp_attach.py +0 -0
  57. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/compat/__init__.py +0 -0
  58. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/compat/fastmcp_compat.py +0 -0
  59. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/components/__init__.py +0 -0
  60. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/components/browser/__init__.py +0 -0
  61. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/components/browser/browser_library_manager.py +0 -0
  62. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/components/execution/__init__.py +0 -0
  63. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/components/execution/desktop_execution_signals.py +0 -0
  64. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/components/execution/desktop_guidance.py +0 -0
  65. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/components/execution/desktop_launch_env.py +0 -0
  66. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/components/execution/execution_coordinator.py +0 -0
  67. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/components/execution/external_rf_client.py +0 -0
  68. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/components/execution/locator_arg_introspection.py +0 -0
  69. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/components/execution/locator_converter.py +0 -0
  70. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/components/execution/mobile_capability_service.py +0 -0
  71. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/components/execution/page_source_service.py +0 -0
  72. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/components/execution/platynui_focus.py +0 -0
  73. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/components/execution/rf_native_context_manager.py +0 -0
  74. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/components/execution/session_manager.py +0 -0
  75. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/components/execution/suite_execution_service.py +0 -0
  76. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/components/execution/ui_tree_service.py +0 -0
  77. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/components/keyword_matcher.py +0 -0
  78. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/components/library_recommender.py +0 -0
  79. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/components/nlp_processor.py +0 -0
  80. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/components/state_manager.py +0 -0
  81. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/components/variables/__init__.py +0 -0
  82. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/components/variables/variable_resolver.py +0 -0
  83. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/config/__init__.py +0 -0
  84. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/config/library_registry.py +0 -0
  85. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/container.py +0 -0
  86. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/core/__init__.py +0 -0
  87. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/core/dynamic_keyword_orchestrator.py +0 -0
  88. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/core/event_bus.py +0 -0
  89. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/core/keyword_discovery.py +0 -0
  90. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/core/library_manager.py +0 -0
  91. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/core/session_manager.py +0 -0
  92. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/__init__.py +0 -0
  93. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/artifact_output/__init__.py +0 -0
  94. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/artifact_output/aggregates.py +0 -0
  95. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/artifact_output/entities.py +0 -0
  96. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/artifact_output/events.py +0 -0
  97. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/artifact_output/services.py +0 -0
  98. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/artifact_output/value_objects.py +0 -0
  99. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/batch_execution/__init__.py +0 -0
  100. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/batch_execution/aggregates.py +0 -0
  101. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/batch_execution/entities.py +0 -0
  102. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/batch_execution/events.py +0 -0
  103. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/batch_execution/services.py +0 -0
  104. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/batch_execution/value_objects.py +0 -0
  105. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/instruction/__init__.py +0 -0
  106. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/instruction/adapters/__init__.py +0 -0
  107. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/instruction/adapters/fastmcp_adapter.py +0 -0
  108. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/instruction/aggregates.py +0 -0
  109. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/instruction/entities.py +0 -0
  110. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/instruction/events.py +0 -0
  111. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/instruction/repository.py +0 -0
  112. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/instruction/security.py +0 -0
  113. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/instruction/services.py +0 -0
  114. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/instruction/value_objects.py +0 -0
  115. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/intent/__init__.py +0 -0
  116. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/intent/adapters/__init__.py +0 -0
  117. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/intent/adapters/locator_normalizer_adapter.py +0 -0
  118. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/intent/adapters/mcp_tool.py +0 -0
  119. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/intent/adapters/session_lookup_adapter.py +0 -0
  120. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/intent/aggregates.py +0 -0
  121. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/intent/entities.py +0 -0
  122. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/intent/events.py +0 -0
  123. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/intent/services.py +0 -0
  124. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/intent/value_objects.py +0 -0
  125. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/keyword_resolution/__init__.py +0 -0
  126. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/keyword_resolution/aggregates.py +0 -0
  127. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/keyword_resolution/events.py +0 -0
  128. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/keyword_resolution/services.py +0 -0
  129. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/keyword_resolution/value_objects.py +0 -0
  130. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/memory/__init__.py +0 -0
  131. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/memory/adapters/__init__.py +0 -0
  132. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/memory/adapters/fastmcp_adapter.py +0 -0
  133. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/memory/adapters/sqlite_vec_adapter.py +0 -0
  134. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/memory/aggregates.py +0 -0
  135. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/memory/entities.py +0 -0
  136. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/memory/events.py +0 -0
  137. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/memory/repository.py +0 -0
  138. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/memory/services.py +0 -0
  139. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/memory/value_objects.py +0 -0
  140. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/recovery/__init__.py +0 -0
  141. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/recovery/aggregates.py +0 -0
  142. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/recovery/entities.py +0 -0
  143. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/recovery/events.py +0 -0
  144. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/recovery/services.py +0 -0
  145. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/recovery/value_objects.py +0 -0
  146. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/response_optimization/__init__.py +0 -0
  147. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/response_optimization/aggregates.py +0 -0
  148. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/response_optimization/events.py +0 -0
  149. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/response_optimization/repository.py +0 -0
  150. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/response_optimization/services.py +0 -0
  151. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/response_optimization/value_objects.py +0 -0
  152. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/shared/__init__.py +0 -0
  153. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/shared/kernel.py +0 -0
  154. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/snapshot/__init__.py +0 -0
  155. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/snapshot/aggregates.py +0 -0
  156. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/snapshot/delta_aggregates.py +0 -0
  157. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/snapshot/delta_entities.py +0 -0
  158. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/snapshot/delta_events.py +0 -0
  159. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/snapshot/delta_service.py +0 -0
  160. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/snapshot/delta_value_objects.py +0 -0
  161. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/snapshot/diff_service.py +0 -0
  162. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/snapshot/entities.py +0 -0
  163. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/snapshot/events.py +0 -0
  164. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/snapshot/list_folding.py +0 -0
  165. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/snapshot/models.py +0 -0
  166. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/snapshot/repository.py +0 -0
  167. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/snapshot/services.py +0 -0
  168. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/snapshot/value_objects.py +0 -0
  169. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/timeout/__init__.py +0 -0
  170. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/timeout/aggregates.py +0 -0
  171. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/timeout/entities.py +0 -0
  172. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/timeout/events.py +0 -0
  173. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/timeout/keyword_classifier.py +0 -0
  174. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/timeout/services.py +0 -0
  175. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/timeout/value_objects.py +0 -0
  176. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/token_accounting/__init__.py +0 -0
  177. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/token_accounting/aggregates.py +0 -0
  178. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/token_accounting/entities.py +0 -0
  179. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/token_accounting/events.py +0 -0
  180. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/token_accounting/repository.py +0 -0
  181. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/token_accounting/services.py +0 -0
  182. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/token_accounting/value_objects.py +0 -0
  183. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/tool_profile/__init__.py +0 -0
  184. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/tool_profile/adapters/__init__.py +0 -0
  185. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/tool_profile/adapters/fastmcp_adapter.py +0 -0
  186. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/tool_profile/aggregates.py +0 -0
  187. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/tool_profile/entities.py +0 -0
  188. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/tool_profile/events.py +0 -0
  189. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/tool_profile/repository.py +0 -0
  190. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/tool_profile/services.py +0 -0
  191. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/domains/tool_profile/value_objects.py +0 -0
  192. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/entry.py +0 -0
  193. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/frontend/__init__.py +0 -0
  194. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/frontend/api.py +0 -0
  195. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/frontend/apps.py +0 -0
  196. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/frontend/asgi.py +0 -0
  197. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/frontend/bridge.py +0 -0
  198. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/frontend/config.py +0 -0
  199. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/frontend/controller.py +0 -0
  200. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/frontend/devserver.py +0 -0
  201. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/frontend/django_app.py +0 -0
  202. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/frontend/static/frontend/app.js +0 -0
  203. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/frontend/static/frontend/base.css +0 -0
  204. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/frontend/templates/frontend/index.html +0 -0
  205. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/frontend/templates/frontend/layout.html +0 -0
  206. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/frontend/urls.py +0 -0
  207. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/frontend/views.py +0 -0
  208. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/models/__init__.py +0 -0
  209. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/models/browser_models.py +0 -0
  210. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/models/config_models.py +0 -0
  211. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/models/execution_models.py +0 -0
  212. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/models/library_models.py +0 -0
  213. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/models/session_models.py +0 -0
  214. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/onboarding/__init__.py +0 -0
  215. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/onboarding/adapters.py +0 -0
  216. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/onboarding/cli.py +0 -0
  217. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/onboarding/codecs.py +0 -0
  218. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/onboarding/diagnostics.py +0 -0
  219. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/onboarding/installer.py +0 -0
  220. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/onboarding/manifest.py +0 -0
  221. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/optimization/__init__.py +0 -0
  222. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/optimization/collector.py +0 -0
  223. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/optimization/compression_learner.py +0 -0
  224. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/optimization/folding_learner.py +0 -0
  225. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/optimization/instruction_hooks.py +0 -0
  226. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/optimization/instruction_learner.py +0 -0
  227. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/optimization/page_analyzer.py +0 -0
  228. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/optimization/pattern_store.py +0 -0
  229. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/optimization/ref_learner.py +0 -0
  230. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/optimization/timeout_learner.py +0 -0
  231. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/plugins/__init__.py +0 -0
  232. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/plugins/base.py +0 -0
  233. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/plugins/builtin/__init__.py +0 -0
  234. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/plugins/builtin/browser_plugin.py +0 -0
  235. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/plugins/builtin/definitions.py +0 -0
  236. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/plugins/builtin/requests_plugin.py +0 -0
  237. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/plugins/builtin/selenium_plugin.py +0 -0
  238. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/plugins/contracts.py +0 -0
  239. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/plugins/discovery.py +0 -0
  240. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/plugins/manager.py +0 -0
  241. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/server.py +0 -0
  242. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/utils/__init__.py +0 -0
  243. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/utils/argument_processor.py +0 -0
  244. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/utils/enhanced_response_serializer.py +0 -0
  245. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/utils/enhanced_serialization_integration.py +0 -0
  246. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/utils/hints.py +0 -0
  247. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/utils/libdoc_argument_parser.py +0 -0
  248. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/utils/library_checker.py +0 -0
  249. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/utils/library_detection.py +0 -0
  250. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/utils/library_detector.py +0 -0
  251. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/utils/library_loading_validator.py +0 -0
  252. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/utils/logging_setup.py +0 -0
  253. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/utils/requests_guidance.py +0 -0
  254. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/utils/response_serializer.py +0 -0
  255. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/utils/rf_libdoc_integration.py +0 -0
  256. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/utils/rf_native_type_converter.py +0 -0
  257. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/utils/rf_variables_compatibility.py +0 -0
  258. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/utils/sampling.py +0 -0
  259. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/utils/server_integration.py +0 -0
  260. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/utils/session_resolution.py +0 -0
  261. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/utils/token_efficient_output.py +0 -0
  262. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/utils/validation.py +0 -0
  263. {rf_mcp-0.34.0.dev2 → rf_mcp-0.34.0.dev4}/src/robotmcp/utils/visual_guidance.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: rf-mcp
3
- Version: 0.34.0.dev2
3
+ Version: 0.34.0.dev4
4
4
  Summary: Robot Framework MCP Server - Natural Language Test Automation Bridge
5
5
  Project-URL: Homepage, https://github.com/manykarim/rf-mcp
6
6
  Project-URL: Repository, https://github.com/manykarim/rf-mcp
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "rf-mcp"
7
- version = "0.34.0.dev2"
7
+ version = "0.34.0.dev4"
8
8
  description = "Robot Framework MCP Server - Natural Language Test Automation Bridge"
9
9
  authors = [{ name = "Many Kasiriha" }]
10
10
  license = { text = "Apache-2.0" }
@@ -40,12 +40,39 @@ ISOLATION_MARKER_ENV = "ROBOTMCP_PLATYNUI_ISOLATED_DISPLAY"
40
40
  ISOLATION_XPID_ENV = "ROBOTMCP_PLATYNUI_ISOLATED_XPID"
41
41
  ALLOW_ACTIVE_ENV = "ROBOTMCP_PLATYNUI_ALLOW_ACTIVE_DESKTOP"
42
42
  SAFETY_GUARD_ENV = "ROBOTMCP_PLATYNUI_SAFETY_GUARD" # "warn" => log, don't block
43
+ # Strict opt-IN to enforce isolation on Windows (change:
44
+ # fix-platynui-windows-runtime, F4). Windows has no headless-isolation model
45
+ # (no Xvfb/Xephyr equivalent), so the guard is permissive by default there;
46
+ # operators who insist on isolation set this to refuse instead.
47
+ REQUIRE_ISOLATED_ENV = "ROBOTMCP_PLATYNUI_REQUIRE_ISOLATED"
43
48
 
44
49
  _X_SERVER_BINARIES = ("xvfb", "xephyr", "xorg", "/x ", "/x\x00", "x11")
45
50
 
46
51
  ISOLATED = "isolated"
47
52
  ACTIVE = "active"
48
53
  UNKNOWN = "unknown"
54
+ # Windows has exactly one interactive desktop and no X11 isolated-display
55
+ # model; the X11/EWMH/marker probes never apply (change:
56
+ # fix-platynui-windows-runtime, F4).
57
+ WINDOWS = "windows"
58
+
59
+ # Windows-accurate isolation note — NEVER the Linux nested-X-server recipe.
60
+ _WINDOWS_ISOLATION_NOTE = (
61
+ "On Windows there is no headless nested-display isolation model. Desktop "
62
+ "automation drives the active session's desktop. To isolate it, run rf-mcp "
63
+ "in a dedicated Windows user session or over RDP to a separate session."
64
+ )
65
+
66
+
67
+ def _is_windows() -> bool:
68
+ """True on Windows (patchable via ``os.name`` in tests)."""
69
+ return os.name == "nt"
70
+
71
+
72
+ def require_isolated(environ: Optional[Dict[str, str]] = None) -> bool:
73
+ """Strict opt-in to enforce isolation on Windows (F4)."""
74
+ env = environ if environ is not None else os.environ
75
+ return env.get(REQUIRE_ISOLATED_ENV, "").strip().lower() in {"1", "true", "yes"}
49
76
 
50
77
 
51
78
  def _bound_display(environ: Dict[str, str]) -> Optional[str]:
@@ -201,6 +228,14 @@ def classify_bound_display_detailed(
201
228
  ``none`` (no display bound / probe inconclusive).
202
229
  """
203
230
  env = environ if environ is not None else os.environ
231
+ # Windows: no X11/EWMH/marker model applies — classify as `windows` before
232
+ # any probe (change: fix-platynui-windows-runtime, F4).
233
+ if _is_windows():
234
+ return {
235
+ "display": None,
236
+ "isolation": WINDOWS,
237
+ "isolation_source": "windows_console",
238
+ }
204
239
  display = _bound_display(env)
205
240
  if _has_isolation_marker(env, display):
206
241
  # Corroborate the marker against a recorded X-server PID (change:
@@ -286,6 +321,30 @@ def evaluate_safety(
286
321
  "classification": classification, "enforcing": True,
287
322
  "allowed": True, "bypassed": False, "reason": None,
288
323
  }
324
+ if classification == WINDOWS:
325
+ # F4: Windows has no isolated-display model — refusing every keyword
326
+ # (with an impossible Xephyr recipe) blocks all Windows automation.
327
+ # Allow by default with a one-time active-desktop warning; a strict
328
+ # opt-in enforces isolation for operators who want it. No Xephyr recipe.
329
+ if require_isolated(env):
330
+ return {
331
+ "classification": classification, "enforcing": True,
332
+ "allowed": False, "bypassed": False,
333
+ "reason": (
334
+ "ROBOTMCP_PLATYNUI_REQUIRE_ISOLATED is set and this is a "
335
+ "Windows active desktop. " + _WINDOWS_ISOLATION_NOTE
336
+ ),
337
+ "windows_note": _WINDOWS_ISOLATION_NOTE,
338
+ }
339
+ return {
340
+ "classification": classification, "enforcing": True,
341
+ "allowed": True, "bypassed": False,
342
+ "reason": (
343
+ "PlatynUI will drive the ACTIVE Windows desktop. "
344
+ + _WINDOWS_ISOLATION_NOTE
345
+ ),
346
+ "windows_note": _WINDOWS_ISOLATION_NOTE,
347
+ }
289
348
  reason = (
290
349
  f"bound display classified '{classification}': refusing desktop input "
291
350
  "to avoid leaking onto the user's active session. Run on an isolated "
@@ -535,6 +535,22 @@ class KeywordExecutor:
535
535
  logger.debug("desktop input-effect snapshot skipped: %s", exc)
536
536
  return None
537
537
 
538
+ @staticmethod
539
+ def _is_desktop_keyboard_keyword(session, keyword) -> bool:
540
+ """True when this is a PlatynUI keyboard keyword on a desktop session
541
+ (change: fix-platynui-windows-runtime, F16). Used to release held
542
+ modifiers when a keyboard keyword fails or is killed mid-chord."""
543
+ try:
544
+ is_desktop = getattr(session, "is_desktop_session", None)
545
+ if not (callable(is_desktop) and is_desktop() is True):
546
+ return False
547
+ norm = str(keyword or "").lower().rsplit(".", 1)[-1].strip()
548
+ return norm in (
549
+ "keyboard press", "keyboard type", "keyboard release",
550
+ )
551
+ except Exception: # pragma: no cover - defensive
552
+ return False
553
+
538
554
  def _platynui_safety_guard(self, session, keyword):
539
555
  """Active-desktop safety guard for PlatynUI interaction keywords
540
556
  (change: platynui-desktop-safety-isolation).
@@ -565,6 +581,18 @@ class KeywordExecutor:
565
581
  "PlatynUI safety guard: refused on %s desktop",
566
582
  outcome["classification"],
567
583
  )
584
+ elif outcome["classification"] == "windows":
585
+ # F4: one-time warning that Windows automation drives the active
586
+ # desktop (there is no isolated-display model on Windows).
587
+ if not getattr(session, "_platynui_windows_warned", False):
588
+ try:
589
+ session._platynui_windows_warned = True
590
+ except Exception:
591
+ pass
592
+ logger.warning(
593
+ "PlatynUI: driving the ACTIVE Windows desktop — %s",
594
+ outcome.get("reason") or "",
595
+ )
568
596
  return outcome
569
597
 
570
598
  def _platynui_focus_before_act(self, session, keyword, arguments):
@@ -1886,13 +1914,97 @@ class KeywordExecutor:
1886
1914
  # causing it to go to stderr instead of the MCP transport.
1887
1915
  # The lock also prevents concurrent Selenium WebDriver access.
1888
1916
  async with self._execution_lock:
1889
- return await self._execute_keyword_serialized(
1890
- session, keyword, arguments, browser_library_manager,
1891
- detail_level, library_prefix, assign_to, use_context, timeout_ms,
1892
- record=record,
1893
- pre_validate_timeout_ms=pre_validate_timeout_ms,
1917
+ # F16 (change: fix-platynui-windows-runtime): a desktop keyboard
1918
+ # keyword that raises or returns failure may have been killed
1919
+ # mid-chord, leaving a modifier physically held at the OS level
1920
+ # (the operator's keyboard wedged until reboot). Release all
1921
+ # modifiers on failure/exception. A SUCCESSFUL Keyboard Press
1922
+ # legitimately holds a key for a later Keyboard Release, so we do
1923
+ # NOT release on success. release_all_modifiers() is a no-op when
1924
+ # nothing is held.
1925
+ _kbd = self._is_desktop_keyboard_keyword(session, keyword)
1926
+ try:
1927
+ result = await self._execute_keyword_serialized(
1928
+ session, keyword, arguments, browser_library_manager,
1929
+ detail_level, library_prefix, assign_to, use_context,
1930
+ timeout_ms,
1931
+ record=record,
1932
+ pre_validate_timeout_ms=pre_validate_timeout_ms,
1933
+ )
1934
+ except BaseException:
1935
+ # An exception (incl. CancelledError from a killed run) is the
1936
+ # canonical "killed mid-chord" case. Release SYNCHRONOUSLY here
1937
+ # so it is guaranteed to run even while the task is being
1938
+ # cancelled (an awaited offload could be interrupted before it
1939
+ # dispatches). keyboard_release is a fast input dispatch, not a
1940
+ # tree query, so the brief on-loop cost is acceptable.
1941
+ if _kbd:
1942
+ self._release_desktop_modifiers()
1943
+ raise
1944
+ if (
1945
+ _kbd
1946
+ and isinstance(result, dict)
1947
+ and not result.get("success", True)
1948
+ # F2: a steering-confidence downgrade means the native keyword
1949
+ # actually SUCCEEDED (the key is held as intended) — releasing
1950
+ # would corrupt a deliberate Press/Release chord. Only release
1951
+ # on a genuine execution failure.
1952
+ and not self._is_steering_downgrade(result)
1953
+ ):
1954
+ # F4: offload the release off the event loop on the normal
1955
+ # failure-return path (no cancellation concern here).
1956
+ await asyncio.to_thread(self._release_desktop_modifiers)
1957
+ # F16 steering (wording only): point the agent at the atomic
1958
+ # Keyboard Type, which cannot leave a key held.
1959
+ try:
1960
+ hints = result.setdefault("hints", [])
1961
+ if isinstance(hints, list):
1962
+ hints.append({
1963
+ "type": "platynui_keyboard_release_safety",
1964
+ "message": (
1965
+ "Released all keyboard modifiers after this "
1966
+ "keyboard keyword failed. Prefer the atomic "
1967
+ "'Keyboard Type <Ctrl+A>' (self-contained "
1968
+ "press+release) over a bare 'Keyboard Press', "
1969
+ "which sends key-DOWN only and must be paired "
1970
+ "with 'Keyboard Release'."
1971
+ ),
1972
+ })
1973
+ except Exception: # pragma: no cover - defensive
1974
+ pass
1975
+ return result
1976
+
1977
+ @staticmethod
1978
+ def _is_steering_downgrade(result: dict) -> bool:
1979
+ """True when a desktop keyword's success was flipped to failure by the
1980
+ steering-confidence gate rather than a genuine execution failure
1981
+ (change: fix-platynui-windows-runtime, F16/F2).
1982
+
1983
+ A steering downgrade means the native keyword DID execute (its key is
1984
+ held as intended), so F16 must not release its modifiers. The gate only
1985
+ downgrades in the RF-success path, so this marker uniquely identifies
1986
+ the case."""
1987
+ try:
1988
+ from robotmcp.components.execution.desktop_execution_signals import (
1989
+ SC_CONTRADICTED,
1990
+ )
1991
+ except Exception: # pragma: no cover - defensive
1992
+ SC_CONTRADICTED = "contradicted"
1993
+ return bool(result.get("steering_confidence") == SC_CONTRADICTED)
1994
+
1995
+ @staticmethod
1996
+ def _release_desktop_modifiers() -> None:
1997
+ """Best-effort release of all held keyboard modifiers via the PlatynUI
1998
+ runtime (change: fix-platynui-windows-runtime, F16). Never raises."""
1999
+ try:
2000
+ from robotmcp.plugins.builtin.platynui_plugin import (
2001
+ release_all_modifiers,
1894
2002
  )
1895
2003
 
2004
+ release_all_modifiers()
2005
+ except Exception as exc: # pragma: no cover - defensive
2006
+ logger.debug("desktop modifier release failed: %s", exc)
2007
+
1896
2008
  async def _execute_keyword_serialized(
1897
2009
  self,
1898
2010
  session: ExecutionSession,
@@ -1924,6 +2036,19 @@ class KeywordExecutor:
1924
2036
  )
1925
2037
 
1926
2038
  ensure_x11_session_env()
2039
+ # F3 (change: fix-platynui-windows-runtime): a caller who
2040
+ # passes an explicit timeout_ms wants desktop query/wait
2041
+ # keywords to wait that long; stash it so the query-settings
2042
+ # default (applied in _execute_keyword_with_context) honors
2043
+ # it. This is PER-CALL — reset to None when absent so a
2044
+ # one-off long timeout never becomes a session-sticky
2045
+ # default that defeats the fast-fail behaviour on later
2046
+ # steps (the short default is restored automatically).
2047
+ session._platynui_query_timeout_ms = (
2048
+ timeout_ms
2049
+ if isinstance(timeout_ms, int) and timeout_ms > 0
2050
+ else None
2051
+ )
1927
2052
  # D2: if a recent desktop launch marked the tree stale,
1928
2053
  # refresh the cached accessibility tree before the FIRST
1929
2054
  # tree-resolving keyword so a newly-launched app resolves by
@@ -2029,8 +2154,18 @@ class KeywordExecutor:
2029
2154
  # the AUT window is visible/in-scope (change:
2030
2155
  # platynui-focused-execution). FocusError fails fast under
2031
2156
  # opt-in policy; otherwise warnings ride along on the step.
2032
- _focus_outcome = self._platynui_focus_before_act(
2033
- session, keyword, arguments
2157
+ # F14 (change: fix-platynui-windows-runtime): the focus
2158
+ # manager drives raw runtime.evaluate() (find/activate/
2159
+ # highlight the AUT window) with NO timeout knob — a broad
2160
+ # or busy-tree query blocks the event loop for tens of
2161
+ # seconds, wedging metadata calls. Offload to a worker
2162
+ # thread so the loop stays free; the global _execution_lock
2163
+ # already serializes desktop dispatch so no native
2164
+ # concurrency is introduced. FocusError still propagates
2165
+ # out of to_thread to the except below.
2166
+ _focus_outcome = await asyncio.to_thread(
2167
+ self._platynui_focus_before_act,
2168
+ session, keyword, arguments,
2034
2169
  )
2035
2170
  # Resolve a desktop Process launch/recovery executable to an
2036
2171
  # absolute path against the server PATH (maintainer-report
@@ -2052,8 +2187,10 @@ class KeywordExecutor:
2052
2187
  # so a success-with-no-effect can be flagged afterward
2053
2188
  # (change: desktop-input-and-runtime-diagnostics). Native /
2054
2189
  # non-reentrant; best-effort.
2055
- _input_effect_before = self._desktop_text_count_before(
2056
- keyword, arguments
2190
+ # F14: native CharacterCount query — also offload off the
2191
+ # loop (change: fix-platynui-windows-runtime).
2192
+ _input_effect_before = await asyncio.to_thread(
2193
+ self._desktop_text_count_before, keyword, arguments
2057
2194
  )
2058
2195
  except Exception as _focus_exc:
2059
2196
  from robotmcp.components.execution.platynui_focus import FocusError
@@ -2551,7 +2688,13 @@ class KeywordExecutor:
2551
2688
  # but the target's CharacterCount did not change.
2552
2689
  _after = None
2553
2690
  if _input_effect_before is not None:
2554
- _after = self._desktop_text_count_before(keyword, arguments)
2691
+ # F14 (change: fix-platynui-windows-runtime): the
2692
+ # AFTER CharacterCount probe is the same native
2693
+ # runtime.evaluate() as the offloaded BEFORE probe —
2694
+ # offload it off the loop too for symmetry.
2695
+ _after = await asyncio.to_thread(
2696
+ self._desktop_text_count_before, keyword, arguments
2697
+ )
2555
2698
  _eh = input_effect_hint(
2556
2699
  keyword=keyword,
2557
2700
  success=True,
@@ -3089,6 +3232,90 @@ class KeywordExecutor:
3089
3232
  return mapped
3090
3233
  return None
3091
3234
 
3235
+ # Short default desktop query timeout (F3). PlatynUI.BareMetal's
3236
+ # QuerySettings default is 30s, so an honest-miss Query/wait waits the full
3237
+ # 30s and stacks across retries (up to ~180s in the Windows eval). A short
3238
+ # default fails fast; a caller can still request a longer wait via
3239
+ # timeout_ms. Overridable via ROBOTMCP_PLATYNUI_QUERY_TIMEOUT_MS.
3240
+ _DEFAULT_PLATYNUI_QUERY_TIMEOUT_MS = 1500
3241
+
3242
+ def _ensure_desktop_query_timeout(self, session) -> None:
3243
+ """Give desktop PlatynUI query/wait keywords a short default timeout so
3244
+ an honest-miss fails fast (change: fix-platynui-windows-runtime, F3).
3245
+
3246
+ Mirrors ``Set Query Settings scope=SUITE`` by writing a short-timeout
3247
+ ``QuerySettings`` into ``${PLATYNUI_QUERY_SETTINGS}`` in the live RF
3248
+ context, once per session (re-applied only if the desired value
3249
+ changed). A caller-supplied ``timeout_ms`` (stashed on the session)
3250
+ overrides the short default. Best-effort; never raises — on any failure
3251
+ the library keeps its 30s default (no worse than before)."""
3252
+ try:
3253
+ checker = getattr(session, "is_desktop_session", None)
3254
+ if not (callable(checker) and checker() is True):
3255
+ return
3256
+ except Exception:
3257
+ return
3258
+ try:
3259
+ default_ms = int(
3260
+ os.environ.get(
3261
+ "ROBOTMCP_PLATYNUI_QUERY_TIMEOUT_MS",
3262
+ str(self._DEFAULT_PLATYNUI_QUERY_TIMEOUT_MS),
3263
+ )
3264
+ )
3265
+ except (ValueError, TypeError):
3266
+ default_ms = self._DEFAULT_PLATYNUI_QUERY_TIMEOUT_MS
3267
+ desired_ms = getattr(session, "_platynui_query_timeout_ms", None) or default_ms
3268
+ if desired_ms <= 0:
3269
+ return
3270
+ if getattr(session, "_platynui_query_timeout_applied_ms", None) == desired_ms:
3271
+ return
3272
+ try:
3273
+ from dataclasses import replace as _dc_replace
3274
+
3275
+ from robot.running.context import EXECUTION_CONTEXTS as _EC
3276
+ from PlatynUI.BareMetal import ( # library-sanctioned mechanism
3277
+ PLATYNUI_QUERY_SETTINGS,
3278
+ QuerySettings,
3279
+ )
3280
+
3281
+ ctx = _EC.current
3282
+ if ctx is None:
3283
+ return
3284
+ variables = ctx.variables
3285
+ name = f"${{{PLATYNUI_QUERY_SETTINGS}}}"
3286
+ # Preserve any non-timeout query settings already in scope
3287
+ # (retry_interval / ignore_exceptions) instead of resetting them to
3288
+ # class defaults — only the timeout is ours to change.
3289
+ base = None
3290
+ try:
3291
+ if PLATYNUI_QUERY_SETTINGS in variables:
3292
+ existing = variables[name]
3293
+ if isinstance(existing, QuerySettings):
3294
+ base = existing
3295
+ except Exception:
3296
+ base = None
3297
+ timeout_s = desired_ms / 1000.0
3298
+ settings = (
3299
+ _dc_replace(base, timeout=timeout_s)
3300
+ if base is not None
3301
+ else QuerySettings(timeout=timeout_s)
3302
+ )
3303
+ # set_suite(children=True): a bare write into the SUITE store is
3304
+ # invisible because rf-mcp already holds a live TEST scope copied
3305
+ # from suite BEFORE the write, and PlatynUI resolves the variable
3306
+ # via variables.current (the test scope). set_suite(children=True)
3307
+ # both persists at SUITE (surviving per-step test-scope resets) AND
3308
+ # propagates down to the current/test scope so the library sees it
3309
+ # immediately (mirrors Set Query Settings scope=SUITE).
3310
+ variables.set_suite(name, settings, children=True)
3311
+ session._platynui_query_timeout_applied_ms = desired_ms
3312
+ logger.debug(
3313
+ "Applied desktop query timeout %sms via ${PLATYNUI_QUERY_SETTINGS}",
3314
+ desired_ms,
3315
+ )
3316
+ except Exception as exc: # pragma: no cover - env/internals dependent
3317
+ logger.debug("desktop query-settings apply skipped: %s", exc)
3318
+
3092
3319
  def _inject_timeout_into_arguments(
3093
3320
  self,
3094
3321
  keyword: str,
@@ -3675,6 +3902,10 @@ class KeywordExecutor:
3675
3902
  except Exception:
3676
3903
  pass
3677
3904
 
3905
+ # F3: give desktop query/wait keywords a short fast-fail timeout
3906
+ # now that the RF context exists (no-op for non-desktop sessions).
3907
+ self._ensure_desktop_query_timeout(session)
3908
+
3678
3909
  # Execute keyword using RF native context with session variables
3679
3910
  result = await asyncio.to_thread(
3680
3911
  self.rf_native_context.execute_keyword_with_context,
@@ -110,6 +110,39 @@ class GeneratedTestSuite:
110
110
  column_headers: Optional[List[str]] = None
111
111
 
112
112
 
113
+ # change: fix-suite-arg-escape-roundtrip. RF recognizes only \n \r \t (+ \xHH \uHHHH
114
+ # \UHHHHHHHH) as escapes and drops the backslash for anything else. Preserve those and the
115
+ # syntax escapes rf-mcp/agents rely on; double every other literal backslash so RF's parser
116
+ # recovers the value. Idempotent: an already-doubled \\ is kept as a pair.
117
+ _RF_ESCAPE_PRESERVE_NEXT = frozenset("nrtxuU=#$@&%{} ")
118
+
119
+
120
+ def _escape_aware_backslashes(s: str) -> str:
121
+ """Double literal backslashes that RF would drop, preserving real/intended escapes."""
122
+ if "\\" not in s:
123
+ return s
124
+ out: List[str] = []
125
+ i, n = 0, len(s)
126
+ while i < n:
127
+ c = s[i]
128
+ if c == "\\":
129
+ nxt = s[i + 1] if i + 1 < n else ""
130
+ if nxt == "\\":
131
+ out.append("\\\\") # keep an existing \\ pair verbatim (idempotent)
132
+ i += 2
133
+ continue
134
+ if nxt in _RF_ESCAPE_PRESERVE_NEXT:
135
+ out.append("\\") # recognized/intended escape — do not double
136
+ i += 1
137
+ continue
138
+ out.append("\\\\") # unrecognized escape / trailing lone \ — double it
139
+ i += 1
140
+ continue
141
+ out.append(c)
142
+ i += 1
143
+ return "".join(out)
144
+
145
+
113
146
  class TestBuilder:
114
147
  """Builds Robot Framework test suites from successful execution steps."""
115
148
 
@@ -4098,6 +4131,24 @@ class TestBuilder:
4098
4131
  # Fix malformed evaluation namespace expressions first
4099
4132
  arg = self._fix_malformed_evaluation_namespace(arg)
4100
4133
 
4134
+ # Platform-independent path form (change: fix-suite-path-escaping): a Windows
4135
+ # DRIVE-LETTER path is rewritten to forward slashes — valid for Start Process/
4136
+ # subprocess and the OperatingSystem keywords on BOTH Windows and POSIX, and
4137
+ # RF-escape-safe. Without this, RF's escape processing corrupts a literal
4138
+ # backslash path (C:\WINDOWS -> C:WINDOWS; C:\name -> embedded newline).
4139
+ # Only the unambiguous drive-letter shape is touched; flags (/w), URLs
4140
+ # (https://…) and variable references do NOT match and are left unchanged.
4141
+ if re.match(r"^[A-Za-z]:[\\/]", arg):
4142
+ arg = arg.replace("\\", "/")
4143
+
4144
+ # Escape-aware backslash round-trip (change: fix-suite-arg-escape-roundtrip). RF
4145
+ # drops the backslash for every escape it does not recognize (\d -> d, \W -> W), so
4146
+ # a literal backslash (regex, relative Windows path) is corrupted on parse. Double
4147
+ # a backslash ONLY where RF would drop it; PRESERVE RF's real escapes (\n \r \t,
4148
+ # \x \u \U), an already-doubled \\, and the intended syntax escapes (\= \# \$ \{ \}
4149
+ # \@ \& \% \<space>). Idempotent, and leaves ${..} references (no backslash) intact.
4150
+ arg = _escape_aware_backslashes(arg)
4151
+
4101
4152
  # Escape literal newlines/tabs so they don't break the .robot line structure.
4102
4153
  # RF uses \n / \t as escape sequences, so we emit \\n / \\t in the text
4103
4154
  # which RF interprets back as the literal characters at runtime.
@@ -162,6 +162,17 @@ _RUNTIME_STATE = "new"
162
162
  # Last bind/connect failure text (for classification). None once open.
163
163
  _RUNTIME_LAST_ERROR: Optional[str] = None
164
164
 
165
+ # Stuck-key safety net (change: fix-platynui-windows-runtime, F16). ``Keyboard
166
+ # Press`` sends key-DOWN only, and runs that time out / are killed mid-chord can
167
+ # leave a modifier physically held at the OS level — the operator's keyboard is
168
+ # then wedged until reboot. Releasing keys that are not held is a proven no-op,
169
+ # so a broad release sequence is always safe. Left+right variants + AltGr + Win
170
+ # cover every modifier the native keyboard recognizes.
171
+ _RELEASE_ALL_SEQUENCE = "<LCtrl+RCtrl+LAlt+RAlt+AltGr+LShift+RShift+LWin+RWin>"
172
+ # atexit/SIGTERM release handlers are registered exactly once, on first runtime
173
+ # bind (guarded under _RUNTIME_LOCK), so a killed process still releases keys.
174
+ _RELEASE_HANDLERS_REGISTERED = False
175
+
165
176
 
166
177
  def runtime_unavailable_reason() -> Optional[str]:
167
178
  """Classify why the PlatynUI runtime is unavailable, or None when it is
@@ -225,6 +236,9 @@ def get_runtime():
225
236
  _RUNTIME = pn.Runtime()
226
237
  _RUNTIME_STATE = "open"
227
238
  _RUNTIME_LAST_ERROR = None
239
+ # First successful bind: arm the stuck-key safety net so a killed
240
+ # process still releases held modifiers (F16). Under _RUNTIME_LOCK.
241
+ _register_release_handlers_once()
228
242
  except Exception as exc: # pragma: no cover - env dependent
229
243
  logger.debug("PlatynUI runtime broker bind failed: %s", exc)
230
244
  _RUNTIME = None
@@ -235,6 +249,72 @@ def get_runtime():
235
249
  return _RUNTIME
236
250
 
237
251
 
252
+ def release_all_modifiers() -> bool:
253
+ """Best-effort release of every keyboard modifier (change:
254
+ fix-platynui-windows-runtime, F16).
255
+
256
+ Releases Ctrl/Alt/AltGr/Shift/Win (left+right) against the ALREADY-OPEN
257
+ native runtime. Releasing a key that is not held is a no-op, so this is
258
+ always safe to call from teardown, keyword-failure ``finally`` paths, and
259
+ process-exit handlers. Never raises; never starts the runtime just to
260
+ release (teardown must not spin up the native broker). Returns True when a
261
+ release was dispatched.
262
+ """
263
+ if _RUNTIME is None or _RUNTIME_STATE != "open":
264
+ return False
265
+ try:
266
+ _RUNTIME.keyboard_release(_RELEASE_ALL_SEQUENCE)
267
+ return True
268
+ except Exception as exc: # pragma: no cover - env dependent
269
+ logger.debug("release_all_modifiers failed: %s", exc)
270
+ return False
271
+
272
+
273
+ def _register_release_handlers_once() -> None:
274
+ """Arm atexit + SIGTERM handlers that release held modifiers on process
275
+ exit (F16). Called once from ``get_runtime`` under ``_RUNTIME_LOCK`` after
276
+ the first successful bind — the safety net for a run killed mid-chord (e.g.
277
+ a ``claude -p`` process terminated before ``on_session_end`` runs). Never
278
+ raises."""
279
+ global _RELEASE_HANDLERS_REGISTERED
280
+ if _RELEASE_HANDLERS_REGISTERED:
281
+ return
282
+ _RELEASE_HANDLERS_REGISTERED = True
283
+ try:
284
+ import atexit
285
+
286
+ atexit.register(release_all_modifiers)
287
+ except Exception as exc: # pragma: no cover - defensive
288
+ logger.debug("atexit release registration failed: %s", exc)
289
+ try:
290
+ import signal
291
+
292
+ # Chain to any prior SIGTERM handler so we don't swallow shutdown.
293
+ _prior = signal.getsignal(signal.SIGTERM)
294
+
295
+ def _release_on_sigterm(signum, frame): # pragma: no cover - signal path
296
+ release_all_modifiers()
297
+ if callable(_prior) and _prior not in (
298
+ signal.SIG_DFL,
299
+ signal.SIG_IGN,
300
+ ):
301
+ _prior(signum, frame)
302
+ elif _prior == signal.SIG_IGN:
303
+ # Prior handler explicitly ignored SIGTERM — preserve that.
304
+ return
305
+ else:
306
+ # SIG_DFL, None, or unknown: restore the default and re-raise so
307
+ # the process still terminates on SIGTERM (never leave it
308
+ # un-killable by swallowing the signal).
309
+ signal.signal(signal.SIGTERM, signal.SIG_DFL)
310
+ os.kill(os.getpid(), signum)
311
+
312
+ signal.signal(signal.SIGTERM, _release_on_sigterm)
313
+ except (ValueError, OSError, AttributeError) as exc:
314
+ # signal.signal only works on the main thread; best-effort otherwise.
315
+ logger.debug("SIGTERM release registration skipped: %s", exc)
316
+
317
+
238
318
  def runtime_state() -> str:
239
319
  return _RUNTIME_STATE
240
320
 
@@ -555,9 +635,33 @@ class PlatynUILibraryPlugin(StaticLibraryPlugin):
555
635
  libraries.append(preference)
556
636
  if any("platynui" in str(lib).lower() for lib in libraries):
557
637
  ensure_x11_session_env()
638
+ # Arm the stuck-key release handlers HERE, on the main thread
639
+ # (session creation runs on the event loop). The first runtime
640
+ # bind often happens inside the F14 to_thread-offloaded focus
641
+ # call on a WORKER thread, where signal.signal() raises — so
642
+ # arming only at bind time would silently skip the SIGTERM net
643
+ # (and default SIGTERM also skips atexit), defeating the F16
644
+ # process-kill safety net. Registration is idempotent and the
645
+ # release itself no-ops until the runtime is open. F16.
646
+ _register_release_handlers_once()
647
+ # Defensively clear any modifier a PRIOR crashed run left held
648
+ # (no-op unless the runtime is already open) — F16.
649
+ release_all_modifiers()
558
650
  except Exception as exc: # pragma: no cover - defensive
559
651
  logger.debug("PlatynUI session-start hook failed: %s", exc)
560
652
 
653
+ def on_session_end(self, session: "ExecutionSession") -> None:
654
+ """Release any held keyboard modifiers when a PlatynUI desktop session
655
+ closes (change: fix-platynui-windows-runtime, F16).
656
+
657
+ ``session_manager.on_session_end`` invokes this for every plugin; the
658
+ release is a no-op when the runtime is not open or nothing is held, so
659
+ it is safe for non-desktop sessions too. Never raises."""
660
+ try:
661
+ release_all_modifiers()
662
+ except Exception as exc: # pragma: no cover - defensive
663
+ logger.debug("PlatynUI session-end release failed: %s", exc)
664
+
561
665
  def before_keyword_execution(
562
666
  self,
563
667
  session: "ExecutionSession",
File without changes