vmware-aiops 2.3.2__tar.gz → 2.4.1__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 (208) hide show
  1. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/.claude-plugin/plugin.json +1 -1
  2. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/.mcp.json +1 -1
  3. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/PKG-INFO +2 -1
  4. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/README-CN.md +1 -0
  5. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/README.md +1 -0
  6. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/RELEASE_NOTES.md +41 -0
  7. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/openclaw.plugin.json +1 -1
  8. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/package.json +1 -1
  9. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/pyproject.toml +1 -1
  10. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/server.json +2 -2
  11. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/skills/vmware-aiops/SKILL.md +2 -2
  12. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/skills/vmware-aiops/references/setup-guide.md +6 -5
  13. vmware_aiops-2.4.1/tests/eval/regression/test_browse_ds_path_join.py +79 -0
  14. vmware_aiops-2.4.1/tests/eval/regression/test_browse_sees_every_file.py +70 -0
  15. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_cli_writes_guarded.py +3 -1
  16. vmware_aiops-2.4.1/tests/eval/regression/test_deploy_failures_are_failures.py +205 -0
  17. vmware_aiops-2.4.1/tests/eval/regression/test_error_hints_fit_the_surface.py +85 -0
  18. vmware_aiops-2.4.1/tests/eval/regression/test_image_registry_per_target.py +64 -0
  19. vmware_aiops-2.4.1/tests/eval/regression/test_linked_clone_failure_on_the_wire.py +71 -0
  20. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_list_envelope_conformance.py +1 -1
  21. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_snapshot_async.py +6 -1
  22. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_v1_5_25_blockers.py +8 -3
  23. vmware_aiops-2.4.1/tests/eval/regression/test_write_round_low_findings.py +104 -0
  24. vmware_aiops-2.4.1/tests/test_composite_tools_obey_policy.py +218 -0
  25. vmware_aiops-2.4.1/tests/test_plan_steps_obey_policy.py +299 -0
  26. vmware_aiops-2.4.1/tests/test_policy_roles.py +117 -0
  27. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/uv.lock +8 -8
  28. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/__init__.py +1 -1
  29. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/cli/deploy.py +26 -2
  30. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/cli/vm.py +4 -0
  31. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/config.py +7 -2
  32. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/mcp_server/_shared.py +42 -1
  33. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/mcp_server/tools/datastore.py +13 -3
  34. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/mcp_server/tools/deploy.py +24 -1
  35. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/mcp_server/tools/guest.py +5 -0
  36. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/mcp_server/tools/plan.py +62 -10
  37. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/mcp_server/tools/ttl.py +3 -0
  38. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/mcp_server/tools/vm.py +3 -2
  39. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/cluster_mgmt.py +4 -1
  40. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/datastore_browser.py +89 -18
  41. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/inventory.py +4 -3
  42. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/ova_deploy.py +44 -11
  43. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/plan_gate.py +73 -7
  44. vmware_aiops-2.4.1/vmware_aiops/ops/step_policy.py +154 -0
  45. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/vm_deploy.py +86 -23
  46. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/vm_gate.py +9 -4
  47. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/vm_lifecycle.py +78 -36
  48. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/policy_environment.py +3 -0
  49. vmware_aiops-2.4.1/vmware_aiops/policy_roles.py +138 -0
  50. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/.clawhubignore +0 -0
  51. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/.env.example +0 -0
  52. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  53. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  54. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  55. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  56. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/.github/dependabot.yml +0 -0
  57. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/.gitignore +0 -0
  58. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/Dockerfile +0 -0
  59. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/LICENSE +0 -0
  60. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/SECURITY.md +0 -0
  61. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/config.example.yaml +0 -0
  62. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/docker-compose.yml +0 -0
  63. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/docs/integrations/continue.md +0 -0
  64. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/docs/integrations/cursor.md +0 -0
  65. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/docs/integrations/goose.md +0 -0
  66. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/docs/integrations/localcowork.md +0 -0
  67. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/docs/integrations/mcp-agent.md +0 -0
  68. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/docs/integrations/vscode-copilot.md +0 -0
  69. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/docs/plans/2026-03-09-plan-apply-design.md +0 -0
  70. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/examples/deploy.yaml +0 -0
  71. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/examples/mcp-configs/README.md +0 -0
  72. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/examples/mcp-configs/claude-code.json +0 -0
  73. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/examples/mcp-configs/continue.yaml +0 -0
  74. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/examples/mcp-configs/cursor.json +0 -0
  75. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/examples/mcp-configs/goose.json +0 -0
  76. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/examples/mcp-configs/localcowork.json +0 -0
  77. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/examples/mcp-configs/mcp-agent.yaml +0 -0
  78. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/examples/mcp-configs/vscode-copilot.json +0 -0
  79. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/examples/ollama-local-setup.md +0 -0
  80. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/skills/vmware-aiops/evals/evals.json +0 -0
  81. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/skills/vmware-aiops/references/agent-guardrails.md +0 -0
  82. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/skills/vmware-aiops/references/capabilities.md +0 -0
  83. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/skills/vmware-aiops/references/cli-reference.md +0 -0
  84. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/skills/vmware-aiops/references/investigation-protocol.md +0 -0
  85. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/smithery.yaml +0 -0
  86. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/__init__.py +0 -0
  87. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/conftest.py +0 -0
  88. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/__init__.py +0 -0
  89. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/capability/__init__.py +0 -0
  90. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/capability/_family.py +0 -0
  91. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/capability/_scores.json +0 -0
  92. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/capability/_scoring.py +0 -0
  93. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/capability/_skill.py +0 -0
  94. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/capability/conftest.py +0 -0
  95. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/capability/test_entity_reachability.py +0 -0
  96. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/capability/test_error_actionability.py +0 -0
  97. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/capability/test_tool_description_quality.py +0 -0
  98. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/capability/test_tool_manifest_budget.py +0 -0
  99. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/__init__.py +0 -0
  100. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/_diag_fakes.py +0 -0
  101. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/_pc_fakes.py +0 -0
  102. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_aiops_specific.py +0 -0
  103. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_alarm_health_scale_n1.py +0 -0
  104. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_atexit_handlers_do_not_accumulate.py +0 -0
  105. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_capability_grader.py +0 -0
  106. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_cli_destructive_guards.py +0 -0
  107. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_cluster_health_summary_delegation.py +0 -0
  108. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_code_review_fixes.py +0 -0
  109. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_config_path_resolution_is_shared.py +0 -0
  110. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_daemon_calls_are_audited.py +0 -0
  111. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_daemon_host_log_cycles.py +0 -0
  112. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_deploy_correctness.py +0 -0
  113. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_doctor_authenticates_every_target.py +0 -0
  114. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_documented_gates_match_the_registry.py +0 -0
  115. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_enum_params_match_implementation.py +0 -0
  116. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_env_secret_obfuscation.py +0 -0
  117. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_event_sets_match_real_class_names.py +0 -0
  118. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_events_are_read_newest_first.py +0 -0
  119. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_guest_download_writes_the_local_disk.py +0 -0
  120. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_guest_username_is_explicit.py +0 -0
  121. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_host_log_scan_reads_real_logs.py +0 -0
  122. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_host_paths_protected.py +0 -0
  123. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_instructions_name_targets.py +0 -0
  124. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_inventory_scale_issue31.py +0 -0
  125. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_list_plans_is_non_destructive.py +0 -0
  126. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_login_without_vsphere_role.py +0 -0
  127. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_mcp_frame_marks_failures.py +0 -0
  128. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_ova_network_mapping.py +0 -0
  129. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_release_blockers.py +0 -0
  130. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_result_envelope.py +0 -0
  131. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_returned_failure_is_audited.py +0 -0
  132. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_session_probe_eviction.py +0 -0
  133. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_set_ttl_safety.py +0 -0
  134. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_text_io_declares_encoding.py +0 -0
  135. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_tool_annotations_match_behaviour.py +0 -0
  136. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_unreachable_host_vmks.py +0 -0
  137. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/eval/regression/test_vim_attribute_conformance.py +0 -0
  138. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_access_info.py +0 -0
  139. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_cluster_mgmt_sanitize.py +0 -0
  140. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_cluster_ttl_plan_gate.py +0 -0
  141. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_concurrent_local_state.py +0 -0
  142. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_config.py +0 -0
  143. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_drs_rules.py +0 -0
  144. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_gate_review_fixes.py +0 -0
  145. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_guest_tools_gate.py +0 -0
  146. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_host_network_mgmt.py +0 -0
  147. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_init_and_onboarding.py +0 -0
  148. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_network_mgmt.py +0 -0
  149. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_no_destructive_ops.py +0 -0
  150. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_plan_gate_narrow_fixes.py +0 -0
  151. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_plan_rollback_final_fixes.py +0 -0
  152. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_reset_alarm_gate.py +0 -0
  153. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_results_name_their_target.py +0 -0
  154. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_safe_error_passthrough.py +0 -0
  155. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_session_pool.py +0 -0
  156. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_stop_signal_runs_atexit.py +0 -0
  157. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_vm_delete_blast_radius.py +0 -0
  158. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/tests/test_vm_tools_gate.py +0 -0
  159. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/_fsutil.py +0 -0
  160. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/access_requirements.py +0 -0
  161. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/cli/__init__.py +0 -0
  162. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/cli/_common.py +0 -0
  163. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/cli/_root.py +0 -0
  164. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/cli/access.py +0 -0
  165. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/cli/alarm.py +0 -0
  166. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/cli/cluster.py +0 -0
  167. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/cli/doctor.py +0 -0
  168. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/cli/hub.py +0 -0
  169. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/cli/investigate.py +0 -0
  170. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/cli/mcp_config.py +0 -0
  171. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/cli/plan.py +0 -0
  172. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/cli/scan.py +0 -0
  173. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/cli/summary.py +0 -0
  174. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/connection.py +0 -0
  175. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/doctor.py +0 -0
  176. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/init_wizard.py +0 -0
  177. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/mcp_server/__init__.py +0 -0
  178. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/mcp_server/__main__.py +0 -0
  179. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/mcp_server/server.py +0 -0
  180. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/mcp_server/tools/__init__.py +0 -0
  181. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/mcp_server/tools/access.py +0 -0
  182. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/mcp_server/tools/alarm.py +0 -0
  183. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/mcp_server/tools/cluster.py +0 -0
  184. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/mcp_server/tools/network.py +0 -0
  185. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/mcp_server/tools/summary.py +0 -0
  186. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/notify/__init__.py +0 -0
  187. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/notify/audit.py +0 -0
  188. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/notify/logger.py +0 -0
  189. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/notify/webhook.py +0 -0
  190. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/__init__.py +0 -0
  191. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/_paging.py +0 -0
  192. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/alarm_mgmt.py +0 -0
  193. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/cluster_gate.py +0 -0
  194. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/gate.py +0 -0
  195. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/guest_gate.py +0 -0
  196. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/guest_ops.py +0 -0
  197. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/health.py +0 -0
  198. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/host_network_mgmt.py +0 -0
  199. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/iscsi_config.py +0 -0
  200. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/network_mgmt.py +0 -0
  201. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/plan_executor.py +0 -0
  202. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/planner.py +0 -0
  203. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/ttl.py +0 -0
  204. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/ops/vm_delete_gate.py +0 -0
  205. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/scanner/__init__.py +0 -0
  206. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/scanner/alarm_scanner.py +0 -0
  207. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/scanner/log_scanner.py +0 -0
  208. {vmware_aiops-2.3.2 → vmware_aiops-2.4.1}/vmware_aiops/scanner/scheduler.py +0 -0
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://anthropic.com/claude-code/plugin.schema.json",
3
3
  "name": "vmware-aiops",
4
4
  "displayName": "VMware AIops",
5
- "version": "2.3.2",
5
+ "version": "2.4.1",
6
6
  "description": "AI-powered VMware vCenter/ESXi VM lifecycle and deployment with 61 MCP tools.",
7
7
  "author": {
8
8
  "name": "Wei Zhou",
@@ -5,7 +5,7 @@
5
5
  "command": "uvx",
6
6
  "args": [
7
7
  "--from",
8
- "vmware-aiops==2.3.2",
8
+ "vmware-aiops==2.4.1",
9
9
  "vmware-aiops-mcp"
10
10
  ]
11
11
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: vmware-aiops
3
- Version: 2.3.2
3
+ Version: 2.4.1
4
4
  Summary: VMware vCenter/ESXi AI-powered VM lifecycle and deployment tool
5
5
  Project-URL: Homepage, https://github.com/vmware-skills/VMware-AIops
6
6
  Project-URL: Repository, https://github.com/vmware-skills/VMware-AIops
@@ -1204,6 +1204,7 @@ claude mcp add --transport http vmware-aiops https://mcp.example.com:8443/mcp \
1204
1204
  (default 2) sessions per target; each session is a login on the target, and a call waits at most 300 s for one.
1205
1205
  - **Everyone who can reach the endpoint acts with this server's account.** Tokens identify callers; they grant
1206
1206
  nothing on the platform. Run one endpoint per privilege level.
1207
+ - **Several people or roles on one endpoint:** run `vmware-audit roles enable` once, then give each token a role (`vmware-audit token create NAME --role reader`, or a role in `roles.yaml` naming tools and targets; vmware-policy ≥ 1.24.0). A role limits what the token may call on this endpoint; it cannot grant more than the account has. See vmware-policy's README, "Roles for network callers".
1207
1208
 
1208
1209
  **Offer only read tools, plus the writes you choose:**
1209
1210
 
@@ -843,6 +843,7 @@ claude mcp add --transport http vmware-aiops https://mcp.example.com:8443/mcp \
843
843
  个会话;每个会话都是在该 target 上的一次登录,一个调用最多等待 300 秒拿到会话。
844
844
  - **能连到这个端点的人,都以该 server 配置的账号操作。** token 只用来识别调用者,不在平台上授予任何权限。
845
845
  每个权限级别单独起一个端点。
846
+ - **一个端点给多人或多种角色用:** 先运行一次 `vmware-audit roles enable`,再给每个 token 指定角色(`vmware-audit token create 名字 --role reader`,或在 `roles.yaml` 里定义可用的工具和 target;需 vmware-policy ≥ 1.24.0)。角色只能限定这个 token 在本端点上能调用什么,不能超出账号本身的权限。详见 vmware-policy README 的「网络调用者的角色」一节。
846
847
 
847
848
  **只提供读工具,加上你选定的写工具:**
848
849
 
@@ -1180,6 +1180,7 @@ claude mcp add --transport http vmware-aiops https://mcp.example.com:8443/mcp \
1180
1180
  (default 2) sessions per target; each session is a login on the target, and a call waits at most 300 s for one.
1181
1181
  - **Everyone who can reach the endpoint acts with this server's account.** Tokens identify callers; they grant
1182
1182
  nothing on the platform. Run one endpoint per privilege level.
1183
+ - **Several people or roles on one endpoint:** run `vmware-audit roles enable` once, then give each token a role (`vmware-audit token create NAME --role reader`, or a role in `roles.yaml` naming tools and targets; vmware-policy ≥ 1.24.0). A role limits what the token may call on this endpoint; it cannot grant more than the account has. See vmware-policy's README, "Roles for network callers".
1183
1184
 
1184
1185
  **Offer only read tools, plus the writes you choose:**
1185
1186
 
@@ -1,3 +1,44 @@
1
+ ## v2.4.1 — fixes from the 2026-10-17 real-hardware read and write rounds
2
+
3
+ * **Tools that run other tools' operations obey `rules.yaml` for those tools**, on the MCP and CLI surfaces.
4
+ `restore_vm_clean_slate`, the batch tools, `provision_vm_guest`, and the deploy tools' `power_on` / snapshot /
5
+ CPU-memory follow-ups are refused, with a `denied` audit row, when the policy denies the tool that does that part
6
+ (a deny on `revert_vm_snapshot` used to leave clean slate working; a deny on `power_on_vm` did not stop
7
+ `batch_clone_vms(power_on=True)`). A follow-up is checked only when the call asks for it. `run_vm_guest_command_with_output` is
8
+ checked as `run_vm_guest_command` (and `download_vm_guest_file`) too.
9
+ * **Behaviour change for custom network roles:** a role check reads no arguments, so a role that allows
10
+ `deploy_vm_from_template`, `deploy_vm_from_ova` or `deploy_linked_clone` must now also allow `power_on_vm`,
11
+ `create_vm_snapshot` and `reconfigure_vm` (and one allowing `run_vm_guest_command_with_output` must allow
12
+ `run_vm_guest_command` and `download_vm_guest_file`), or every call is refused. The built-in `reader` and `admin`
13
+ roles are unaffected.
14
+ * **Plan steps obey `rules.yaml`.** Each step of `apply_vm_plan` / `rollback_vm_plan` is authorized by vmware-policy
15
+ under the tool that does the same thing (a `delete_vm` step as `delete_vm`, `power_off` as `power_off_vm`, the
16
+ rollback that deletes a VM the plan created as `delete_vm`), at that tool's risk level — in the preview and again
17
+ just before the step runs, with a `denied` audit row. A deny on `delete_vm` used to stop the tool but not a plan step.
18
+ * **Failures are failures.** Deploy, clone, linked clone, template conversion, ISO attach, `create_vm`, `migrate_vm`,
19
+ `reconfigure_vm` and `configure_cluster` with nothing to change returned their refusals as status strings, which
20
+ the MCP frame and the audit recorded as success; they now raise. A plan that stops part-way, a rollback that leaves
21
+ a step not undone, and a plan that fails validation now carry the family `error` key.
22
+ * **`deploy_vm_from_template` works on a vCenter with standalone hosts**: the clone gets the template's resource
23
+ pool (it failed with `spec.location.pool`), and the tool now takes `target_host`.
24
+ * **`scan_all_datastore_images` keeps each target's images**: the cache was one list replaced by every scan, so
25
+ scanning `datastore1` on an ESXi host erased what a vCenter scan had found. Entries now carry `target`; a corrupt
26
+ cache file is rebuilt by the next scan and explained by the error instead of a bare JSON error.
27
+ * **`browse_datastore` sees every file** (`.ova`, `.ovf`, `.vmx`, `.log` were filtered out), `ds_path` is well formed
28
+ for ESXi and vCenter folder paths, and a leading `/` is refused with the corrected path, as vmware-storage does.
29
+ * Error text names MCP tools and parameters (CLI form in brackets); doctor is suggested only for connectivity errors;
30
+ a CPU-feature vMotion refusal leads with the remedy; `get_vm_task_status` no longer says an unknown id means the
31
+ task finished; powering off a VM that is already off is a plain `noop`.
32
+
33
+ ## v2.4.0 — roles for network callers (HLD §18)
34
+
35
+ * Declares the role metadata vmware-policy ≥ 1.24.0 needs to apply **roles** to this server over HTTPS
36
+ (HLD §18): how a missing target resolves, the guest and host-file tools that escape a role, and what each composite tool can run — `apply_vm_plan` and `rollback_vm_plan` reach every tool a plan step maps to plus `delete_vm` (rollback), `set_vm_ttl` reaches `delete_vm`, the batch tools reach the deploy and clone tools and the follow-up steps their arguments ask for (`reconfigure_vm`, `create_vm_snapshot`, `power_on_vm`, and `attach_iso_to_vm` for a deployment spec); a test derives these from what the batch code calls. A role may allow a composite only if it allows all of them. A test fails when a new plan action is not mapped to a tool. With an older vmware-policy nothing changes. Once an operator runs
37
+ `vmware-audit roles enable`, a token's role decides which tools and targets it may use here; a refused call is
38
+ audited as `denied`. See vmware-policy's README, "Roles for network callers".
39
+ * A target-limited role is checked against the same config the connection uses: the copy the server's connection manager loaded on its first connection. Edit `config.yaml` and restart for both to see the change.
40
+ * README and setup guide: one endpoint for several people or roles (run `vmware-audit roles enable` first).
41
+
1
42
  ## v2.3.2 — tools may not touch this server's own control files
2
43
 
3
44
  * **Security fix (HLD §18 step 0).** guest file download/upload, OVA and deployment-spec paths could name this server's own files, so over HTTPS a token holder could
@@ -11,7 +11,7 @@
11
11
  "command": "uvx",
12
12
  "args": [
13
13
  "--from",
14
- "vmware-aiops==2.3.2",
14
+ "vmware-aiops==2.4.1",
15
15
  "vmware-aiops-mcp"
16
16
  ]
17
17
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zw008/vmware-aiops",
3
- "version": "2.3.2",
3
+ "version": "2.4.1",
4
4
  "description": "AI-powered VMware vCenter/ESXi VM lifecycle and deployment with 61 MCP tools.",
5
5
  "private": true,
6
6
  "license": "MIT",
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "vmware-aiops"
7
- version = "2.3.2"
7
+ version = "2.4.1"
8
8
  description = "VMware vCenter/ESXi AI-powered VM lifecycle and deployment tool"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -7,12 +7,12 @@
7
7
  "url": "https://github.com/vmware-skills/VMware-AIops",
8
8
  "source": "github"
9
9
  },
10
- "version": "2.3.2",
10
+ "version": "2.4.1",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "pypi",
14
14
  "identifier": "vmware-aiops",
15
- "version": "2.3.2",
15
+ "version": "2.4.1",
16
16
  "runtimeHint": "uvx",
17
17
  "packageArguments": [
18
18
  {
@@ -63,7 +63,7 @@ Read before connecting an agent. Per-tool inventory: `references/capabilities.md
63
63
  ## Quick Install
64
64
 
65
65
  ```bash
66
- uv tool install vmware-aiops==2.3.2
66
+ uv tool install vmware-aiops==2.4.1
67
67
  vmware-aiops doctor
68
68
  vmware-aiops hub status # see which family members are installed
69
69
  ```
@@ -303,7 +303,7 @@ The account has no vSphere role. Assign one (Read-Only or higher) on the vCenter
303
303
  ## Setup
304
304
 
305
305
  ```bash
306
- uv tool install vmware-aiops==2.3.2
306
+ uv tool install vmware-aiops==2.4.1
307
307
  mkdir -p ~/.vmware-aiops
308
308
  vmware-aiops init # generates config.yaml and .env templates
309
309
  chmod 600 ~/.vmware-aiops/.env
@@ -6,13 +6,13 @@ All install methods fetch from the same source: [github.com/vmware-skills/VMware
6
6
 
7
7
  ```bash
8
8
  # Via PyPI (recommended for version pinning)
9
- uv tool install vmware-aiops==2.3.2
9
+ uv tool install vmware-aiops==2.4.1
10
10
 
11
11
  # Via Skills.sh (fetches from GitHub)
12
- npx skills add vmware-skills/VMware-AIops#v2.3.2
12
+ npx skills add vmware-skills/VMware-AIops#v2.4.1
13
13
 
14
14
  # Via ClawHub (fetches from ClawHub registry snapshot of GitHub)
15
- clawhub install @zw008/vmware-aiops --version 2.3.2
15
+ clawhub install @zw008/vmware-aiops --version 2.4.1
16
16
  ```
17
17
 
18
18
  ### Claude Code
@@ -35,7 +35,7 @@ claude mcp add vmware-aiops -- vmware-aiops mcp
35
35
 
36
36
  ```bash
37
37
  # 1. Install from PyPI (source: github.com/vmware-skills/VMware-AIops)
38
- uv tool install vmware-aiops==2.3.2
38
+ uv tool install vmware-aiops==2.4.1
39
39
 
40
40
  # 2. Verify installation source
41
41
  vmware-aiops --version # confirms installed version
@@ -73,7 +73,7 @@ The `vmware-aiops` package installs a Python CLI binary and its dependencies (py
73
73
  ## Development Install
74
74
 
75
75
  ```bash
76
- git clone --branch v2.3.2 https://github.com/vmware-skills/VMware-AIops.git
76
+ git clone --branch v2.4.1 https://github.com/vmware-skills/VMware-AIops.git
77
77
  cd VMware-AIops
78
78
  uv venv && source .venv/bin/activate
79
79
  # --no-sources: pyproject's [tool.uv.sources] points vmware-monitor at a sibling
@@ -245,4 +245,5 @@ vmware-aiops mcp --transport https --host 0.0.0.0 --port 8443 \
245
245
  - **Everyone who can reach the endpoint acts with this server's account.** Tokens identify callers; they grant
246
246
  nothing on the platform. Run one endpoint per privilege level (for example a read-only account behind
247
247
  `--expose read`, and a separate endpoint for operators).
248
+ - **Several people or roles on one endpoint:** run `vmware-audit roles enable` once, then give each token a role (`vmware-audit token create NAME --role reader`, or a role in `roles.yaml` naming tools and targets; vmware-policy ≥ 1.24.0). A role limits what the token may call on this endpoint; it cannot grant more than the account has. See vmware-policy's README, "Roles for network callers".
248
249
 
@@ -0,0 +1,79 @@
1
+ """browse_datastore must return a ds_path that deploy/attach tools can use.
2
+
3
+ Regression source: real ESXi 8 host (home-esxi, 2026-10-17). The browse task
4
+ reports each folder's ``folderPath`` with no trailing separator —
5
+ ``"[datastore1]"`` for the root, ``"[datastore1] backup"`` for a folder —
6
+ and the old code glued the file name straight on, producing
7
+ ``"[datastore1] backupVMware-VCSA.iso"`` and ``"[datastore1]Hermers - TT"``.
8
+ Agents pass that string into deploy_vm_from_ova / attach_iso_to_vm, so a
9
+ malformed path breaks the very flow the browse exists for. The earlier tests
10
+ mocked ``"[ds1] images/"`` (trailing slash), the one shape that hid the bug.
11
+ """
12
+ from __future__ import annotations
13
+
14
+ from types import SimpleNamespace
15
+
16
+ import pytest
17
+ from pyVmomi import vim
18
+
19
+ from vmware_aiops.ops import datastore_browser as dsb
20
+
21
+
22
+ @pytest.mark.unit
23
+ @pytest.mark.parametrize(
24
+ ("folder", "name", "expected"),
25
+ [
26
+ # Root, as ESXi reports it: no space after the bracket.
27
+ ("[datastore1]", "Hermers - TT", "[datastore1] Hermers - TT"),
28
+ ("[datastore1]", "x.iso", "[datastore1] x.iso"),
29
+ # Root, as vCenter reports it: trailing space.
30
+ ("[datastore1] ", "x.iso", "[datastore1] x.iso"),
31
+ # Folder without a trailing slash (ESXi).
32
+ ("[datastore1] backup", "x.iso", "[datastore1] backup/x.iso"),
33
+ (
34
+ "[datastore1] Hermers - TT",
35
+ "Hermers - TT.vmdk",
36
+ "[datastore1] Hermers - TT/Hermers - TT.vmdk",
37
+ ),
38
+ ("[datastore1] a/b", "x.iso", "[datastore1] a/b/x.iso"),
39
+ # Folder with a trailing slash (vCenter) — must not double it.
40
+ ("[datastore1] backup/", "x.iso", "[datastore1] backup/x.iso"),
41
+ ],
42
+ )
43
+ def test_join_ds_path(folder, name, expected):
44
+ assert dsb._join_ds_path(folder, name) == expected
45
+
46
+
47
+ @pytest.mark.unit
48
+ def test_browse_root_returns_well_formed_paths_for_every_folder(monkeypatch):
49
+ """Root browse is recursive: subfolder rows come back too, each well formed."""
50
+ def _file(path):
51
+ return SimpleNamespace(path=path, fileSize=1024 * 1024, modification="2026-10-17")
52
+
53
+ results = [
54
+ SimpleNamespace(folderPath="[datastore1]", file=[_file("backup")]),
55
+ SimpleNamespace(folderPath="[datastore1] backup", file=[_file("x.iso")]),
56
+ ]
57
+ seen = {}
58
+
59
+ def fake_search(datastorePath, searchSpec): # noqa: N803 — pyVmomi API names
60
+ seen["path"] = datastorePath
61
+ return SimpleNamespace(
62
+ info=SimpleNamespace(state=vim.TaskInfo.State.success, result=results, error=None)
63
+ )
64
+
65
+ monkeypatch.setattr(
66
+ dsb,
67
+ "find_datastore_by_name",
68
+ lambda si, name: SimpleNamespace(
69
+ browser=SimpleNamespace(SearchDatastoreSubFolders_Task=fake_search)
70
+ ),
71
+ )
72
+
73
+ rows = dsb.browse_datastore(object(), "datastore1")["items"]
74
+
75
+ assert seen["path"] == "[datastore1]"
76
+ assert {r["name"]: r["ds_path"] for r in rows} == {
77
+ "backup": "[datastore1] backup",
78
+ "x.iso": "[datastore1] backup/x.iso",
79
+ }
@@ -0,0 +1,70 @@
1
+ """browse_datastore must see every file, and treat a leading '/' one way.
2
+
3
+ Real hardware, 2026-10-17: vmware-aiops' search spec listed only IsoImageQuery,
4
+ VmDiskQuery and FolderQuery under a comment saying "include all file types".
5
+ A DatastoreBrowser query is a filter, not a hint -- with those three, .ova /
6
+ .ovf / .vmx / .log files are never returned. On the lab vCenter it found 0 of
7
+ the 2 OVAs on 'datastore1 (1)' that vmware-storage (which also passes the
8
+ generic Query) found, so scan_all_datastore_images could not do the one job its
9
+ description gives it: finding an OVA for deploy_vm_from_ova.
10
+
11
+ The twin also drifted on a leading slash: vmware-aiops passed '/vcsa8' through,
12
+ vmware-storage refused it as "absolute" (its 1.5.35 hardening). Both now refuse
13
+ it, and the error names the corrected path.
14
+ """
15
+ from __future__ import annotations
16
+
17
+ from types import SimpleNamespace
18
+
19
+ import pytest
20
+ from pyVmomi import vim
21
+
22
+ from vmware_aiops.ops import datastore_browser as dsb
23
+
24
+
25
+ def _browse(monkeypatch, path=""):
26
+ seen: dict = {}
27
+
28
+ def fake_search(datastorePath, searchSpec): # noqa: N803 -- pyVmomi API names
29
+ seen["path"] = datastorePath
30
+ seen["spec"] = searchSpec
31
+ return SimpleNamespace(
32
+ info=SimpleNamespace(state=vim.TaskInfo.State.success, result=[], error=None)
33
+ )
34
+
35
+ monkeypatch.setattr(
36
+ dsb,
37
+ "find_datastore_by_name",
38
+ lambda si, name: SimpleNamespace(
39
+ browser=SimpleNamespace(SearchDatastoreSubFolders_Task=fake_search)
40
+ ),
41
+ )
42
+ dsb.browse_datastore(object(), "datastore1", path=path, pattern="*.ova")
43
+ return seen
44
+
45
+
46
+ @pytest.mark.unit
47
+ def test_search_spec_includes_the_generic_file_query(monkeypatch) -> None:
48
+ spec = _browse(monkeypatch)["spec"]
49
+ # Exact type: every specific query subclasses Query, so isinstance would pass
50
+ # for the three that hid the OVAs.
51
+ assert any(type(q) is vim.host.DatastoreBrowser.Query for q in spec.query)
52
+
53
+
54
+ @pytest.mark.unit
55
+ def test_relative_path_is_joined_under_the_datastore(monkeypatch) -> None:
56
+ assert _browse(monkeypatch, "vcsa8")["path"] == "[datastore1] vcsa8"
57
+
58
+
59
+ @pytest.mark.unit
60
+ @pytest.mark.parametrize("path", ["/vcsa8", "//vcsa8"])
61
+ def test_leading_slash_is_refused_with_the_fix(monkeypatch, path) -> None:
62
+ with pytest.raises(ValueError, match="path='vcsa8'"):
63
+ _browse(monkeypatch, path)
64
+
65
+
66
+ @pytest.mark.unit
67
+ @pytest.mark.parametrize("path", ["../other", "a/../../b", "x\x00y", "\\\\server\\share", "/"])
68
+ def test_traversal_and_odd_paths_are_refused(monkeypatch, path) -> None:
69
+ with pytest.raises(ValueError, match="datastore-relative"):
70
+ _browse(monkeypatch, path)
@@ -51,7 +51,9 @@ def _ops_refs(tree: ast.AST) -> tuple[dict[str, str], set[str]]:
51
51
  for n in ast.walk(tree):
52
52
  if isinstance(n, ast.ImportFrom) and n.module:
53
53
  parts = n.module.split(".")
54
- if "ops" in parts:
54
+ # step_policy authorizes the operation; it is not the operation, and
55
+ # every composite command calls it (2026-10-17).
56
+ if "ops" in parts and parts[-1] != "step_policy":
55
57
  if parts[-1] == "ops":
56
58
  mods.update(a.asname or a.name for a in n.names)
57
59
  else:
@@ -0,0 +1,205 @@
1
+ """A deploy that did not happen must not be reported, or audited, as success.
2
+
3
+ Real hardware, 2026-10-17: ``deploy_vm_from_ova`` with a datastore name that
4
+ does not exist returned the string ``"Datastore 'x' not found."``. The ops layer
5
+ *returned* it instead of raising, so ``@tool_errors`` never saw an exception:
6
+ the MCP frame said success, the audit row said ``status=ok``, and the plan
7
+ executor and batch deploy counted the step as done. The same shape ran through
8
+ every early exit in ``vm_deploy`` / ``ova_deploy``.
9
+
10
+ These tests assert the audit status and the raised type -- the returned text
11
+ was never the problem.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from unittest.mock import MagicMock
17
+
18
+ import pytest
19
+ from pyVmomi import vim
20
+ from vmware_policy.budget import reset_budget
21
+ from vmware_policy.policy import reset_policy_engine
22
+ from vmware_policy.undo import reset_undo_store
23
+
24
+ import vmware_aiops.mcp_server.tools.deploy as deploy_tools
25
+ from vmware_aiops.ops import ova_deploy, vm_deploy
26
+
27
+
28
+ @pytest.fixture(autouse=True)
29
+ def harness(tmp_path, monkeypatch):
30
+ monkeypatch.setenv("OPS_HOME", str(tmp_path))
31
+ reset_policy_engine()
32
+ reset_budget()
33
+ reset_undo_store()
34
+ yield
35
+ reset_policy_engine()
36
+ reset_budget()
37
+ reset_undo_store()
38
+
39
+
40
+ @pytest.fixture
41
+ def audit_rows(monkeypatch):
42
+ rows: list[dict] = []
43
+
44
+ class _Recorder:
45
+ def log(self, **kw):
46
+ rows.append(kw)
47
+
48
+ monkeypatch.setattr("vmware_policy.guard.get_engine", lambda: _Recorder())
49
+ return rows
50
+
51
+
52
+ @pytest.mark.unit
53
+ def test_ova_deploy_to_a_missing_datastore_raises(monkeypatch) -> None:
54
+ monkeypatch.setattr(ova_deploy, "find_datastore_by_name", lambda si, n: None)
55
+ with pytest.raises(vm_deploy.DeployError, match="list_all_datastores"):
56
+ vm_deploy.deploy_ova(MagicMock(), "/x.ova", "vm1", datastore_name="nope")
57
+
58
+
59
+ @pytest.mark.unit
60
+ def test_template_deploy_of_a_missing_template_raises(monkeypatch) -> None:
61
+ monkeypatch.setattr(vm_deploy, "find_vm_by_name", lambda si, n: None)
62
+ with pytest.raises(vm_deploy.DeployError, match="not found"):
63
+ vm_deploy.deploy_from_template(MagicMock(), "tmpl", "vm1")
64
+
65
+
66
+ @pytest.mark.unit
67
+ def test_template_deploy_to_a_missing_datastore_raises(monkeypatch) -> None:
68
+ tmpl = MagicMock()
69
+ tmpl.config.template = True
70
+ tmpl.runtime.host.parent.resourcePool = vim.ResourcePool("resgroup-1", None)
71
+ monkeypatch.setattr(vm_deploy, "find_vm_by_name", lambda si, n: tmpl)
72
+ monkeypatch.setattr(vm_deploy, "find_datastore_by_name", lambda si, n: None)
73
+ with pytest.raises(vm_deploy.DeployError, match="list_all_datastores"):
74
+ vm_deploy.deploy_from_template(MagicMock(), "tmpl", "vm1", datastore_name="nope")
75
+
76
+
77
+ @pytest.mark.unit
78
+ def test_linked_clone_of_a_missing_source_raises(monkeypatch) -> None:
79
+ monkeypatch.setattr(vm_deploy, "find_vm_by_name", lambda si, n: None)
80
+ with pytest.raises(vm_deploy.DeployError):
81
+ vm_deploy.linked_clone(MagicMock(), "src", "vm1", snapshot_name="s")
82
+
83
+
84
+ @pytest.mark.unit
85
+ def test_ova_tool_failure_is_audited_as_error(audit_rows, monkeypatch) -> None:
86
+ monkeypatch.setattr(deploy_tools, "_get_connection", lambda target=None: MagicMock())
87
+ monkeypatch.setattr(ova_deploy, "find_datastore_by_name", lambda si, n: None)
88
+
89
+ returned = deploy_tools.deploy_vm_from_ova("/x.ova", "vm1", "nope", target="t")
90
+
91
+ assert returned.startswith("Error:")
92
+ assert "nope" in returned
93
+ assert audit_rows and audit_rows[0]["status"] == "error"
94
+
95
+
96
+ @pytest.mark.unit
97
+ def test_template_tool_failure_is_audited_as_error(audit_rows, monkeypatch) -> None:
98
+ monkeypatch.setattr(deploy_tools, "_get_connection", lambda target=None: MagicMock())
99
+ monkeypatch.setattr(vm_deploy, "find_vm_by_name", lambda si, n: None)
100
+
101
+ returned = deploy_tools.deploy_vm_from_template("tmpl", "vm1", target="t")
102
+
103
+ assert returned.startswith("Error:")
104
+ assert audit_rows and audit_rows[0]["status"] == "error"
105
+
106
+
107
+ @pytest.mark.unit
108
+ def test_batch_deploy_marks_a_failed_vm_as_error(monkeypatch) -> None:
109
+ """A failed template deploy used to land in 'messages' with status ok."""
110
+ monkeypatch.setattr(vm_deploy, "find_vm_by_name", lambda si, n: None)
111
+ spec = {"template": "tmpl", "vms": [{"name": "vm1"}]}
112
+ monkeypatch.setattr(vm_deploy, "load_deploy_spec", lambda p: spec)
113
+
114
+ results = vm_deploy.batch_deploy(MagicMock(), "spec.yaml")
115
+
116
+ assert results[0]["status"] == "error"
117
+
118
+
119
+ @pytest.mark.unit
120
+ def test_create_vm_on_a_missing_datastore_raises(monkeypatch) -> None:
121
+ """The same shape one module over: create_vm returned this string too."""
122
+ from vmware_aiops.ops import inventory, vm_lifecycle
123
+
124
+ monkeypatch.setattr(vm_lifecycle, "resolve_datacenter", lambda si, *a, **k: MagicMock())
125
+ monkeypatch.setattr(vm_lifecycle, "find_compute_resource", lambda *a, **k: MagicMock())
126
+ monkeypatch.setattr(vm_lifecycle, "find_datastore_by_name", lambda si, n: None)
127
+ with pytest.raises(inventory.InventoryError, match="list_all_datastores"):
128
+ vm_lifecycle.create_vm(MagicMock(), "vm1", datastore_name="nope")
129
+
130
+
131
+ # ── 2026-10-17 write tests on home-vcenter (second batch) ────────────────────
132
+
133
+
134
+ @pytest.mark.unit
135
+ @pytest.mark.parametrize(
136
+ "kwargs, says",
137
+ [
138
+ ({"target_host": "esx-gone"}, "list_esxi_hosts"),
139
+ ({"target_datastore": "ds-gone"}, "list_all_datastores"),
140
+ ],
141
+ )
142
+ def test_clone_to_a_missing_host_or_datastore_raises(monkeypatch, kwargs, says) -> None:
143
+ """Its hints also named CLI commands that do not exist
144
+ ('vmware-aiops cluster list-hosts', 'vmware-aiops datastore list')."""
145
+ from vmware_aiops.ops import inventory, vm_lifecycle
146
+
147
+ monkeypatch.setattr(vm_lifecycle, "_require_vm", lambda si, n: MagicMock())
148
+ monkeypatch.setattr(vm_lifecycle, "find_host_by_name", lambda si, n: None)
149
+ monkeypatch.setattr(vm_lifecycle, "find_datastore_by_name", lambda si, n: None)
150
+ with pytest.raises(inventory.InventoryError, match=says) as info:
151
+ vm_lifecycle.clone_vm(MagicMock(), "src", "new", **kwargs)
152
+ assert "list-hosts" not in str(info.value) and "datastore list" not in str(info.value)
153
+
154
+
155
+ @pytest.mark.unit
156
+ def test_reconfigure_with_nothing_to_change_raises(monkeypatch) -> None:
157
+ from vmware_aiops.ops import vm_lifecycle
158
+
159
+ monkeypatch.setattr(vm_lifecycle, "_require_vm", lambda si, n: MagicMock())
160
+ with pytest.raises(ValueError, match="memory_mb"):
161
+ vm_lifecycle.reconfigure_vm(MagicMock(), "vm1")
162
+
163
+
164
+ def _template_on_standalone_host(monkeypatch):
165
+ pool = vim.ResourcePool("resgroup-standalone", None)
166
+ tmpl = MagicMock(name="template")
167
+ tmpl.config.template = True
168
+ tmpl.runtime.host.parent.resourcePool = pool
169
+ seen = {}
170
+
171
+ def clone(folder, name, spec):
172
+ seen["spec"] = spec
173
+ return MagicMock()
174
+
175
+ tmpl.Clone = clone
176
+ monkeypatch.setattr(vm_deploy, "find_vm_by_name", lambda si, n: tmpl)
177
+ monkeypatch.setattr(vm_deploy, "_wait_for_task", lambda *a, **k: None)
178
+ return pool, seen
179
+
180
+
181
+ @pytest.mark.unit
182
+ def test_template_deploy_without_a_host_names_the_templates_pool(monkeypatch) -> None:
183
+ """vCenter with standalone hosts: 'A specified parameter was not correct:
184
+ spec.location.pool' on every call, because the pool was only set when
185
+ target_host was passed and the MCP tool did not expose target_host."""
186
+ pool, seen = _template_on_standalone_host(monkeypatch)
187
+
188
+ vm_deploy.deploy_from_template(MagicMock(), "tmpl", "vm1")
189
+
190
+ assert seen["spec"].location.pool is pool
191
+
192
+
193
+ @pytest.mark.unit
194
+ def test_template_tool_exposes_target_host(monkeypatch) -> None:
195
+ import inspect
196
+
197
+ assert "target_host" in inspect.signature(deploy_tools.deploy_vm_from_template).parameters
198
+
199
+
200
+ @pytest.mark.unit
201
+ def test_configure_cluster_with_nothing_to_change_raises() -> None:
202
+ from vmware_aiops.ops import cluster_mgmt
203
+
204
+ with pytest.raises(ValueError, match="drs_behavior"):
205
+ cluster_mgmt.configure_cluster(MagicMock(), "c1")
@@ -0,0 +1,85 @@
1
+ """Error hints must fit the error, and the surface that shows them.
2
+
3
+ Real hardware, 2026-10-17: every MCP error carried "Run 'vmware-aiops doctor'
4
+ to verify connectivity and credentials." -- also for a name that does not
5
+ resolve, where connectivity is fine and the message already says what to change.
6
+ And messages an agent reads said "Pass --target" / "Poll with: vm task-status
7
+ ... --target", a CLI flag and a CLI command; the MCP names are the ``target``
8
+ parameter and ``get_vm_task_status``.
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import pytest
14
+
15
+ from vmware_aiops.config import AppConfig
16
+ from vmware_aiops.mcp_server import _shared
17
+ from vmware_aiops.ops.ova_deploy import DeployError
18
+ from vmware_aiops.ops.vm_lifecycle import TaskStillRunning, VMNotFoundError
19
+
20
+
21
+ def _unknown_target():
22
+ from vmware_aiops.config import UnknownTargetError
23
+
24
+ return UnknownTargetError("Target 'x' not found.")
25
+
26
+
27
+ @pytest.mark.unit
28
+ @pytest.mark.parametrize(
29
+ "exc", [ValueError("bad"), _unknown_target(), VMNotFoundError("x"), DeployError("x")]
30
+ )
31
+ def test_an_authored_request_error_does_not_send_the_agent_to_doctor(exc) -> None:
32
+ assert "doctor" not in _shared._hint_for(exc)
33
+
34
+
35
+ @pytest.mark.unit
36
+ @pytest.mark.parametrize("exc", [ConnectionError("x"), TimeoutError("x"), RuntimeError("x")])
37
+ def test_connectivity_and_unplanned_errors_keep_the_doctor_hint(exc) -> None:
38
+ assert _shared._hint_for(exc) == _shared._DOCTOR_HINT
39
+
40
+
41
+ @pytest.mark.unit
42
+ def test_unknown_target_names_the_mcp_parameter() -> None:
43
+ with pytest.raises(KeyError) as info:
44
+ AppConfig(targets=()).get_target("nope")
45
+ assert "target=" in str(info.value)
46
+
47
+
48
+ @pytest.mark.unit
49
+ def test_still_running_names_the_mcp_poll_tool() -> None:
50
+ assert "get_vm_task_status" in str(TaskStillRunning("task-1", 60))
51
+
52
+
53
+ @pytest.mark.unit
54
+ @pytest.mark.parametrize("make", [
55
+ lambda: __import__("vmware_aiops.ops.vm_lifecycle", fromlist=["x"]).TaskFailedError(
56
+ "Task failed: ... | fault=vim.fault.MemoryHotPlugNotSupported"),
57
+ lambda: __import__("vmware_aiops.ops.guest_ops", fromlist=["x"]).GuestOpsError(
58
+ "VMware Tools is not running in the guest"),
59
+ lambda: __import__("vmware_aiops.ops.gate", fromlist=["x"]).GateRefusedError(
60
+ "refused: VMware Tools is not running"),
61
+ ])
62
+ def test_task_guest_and_gate_refusals_do_not_send_the_agent_to_doctor(make) -> None:
63
+ """Write round 2026-10-17: MemoryHotPlugNotSupported and Tools-not-running
64
+ errors said 'Run doctor to verify connectivity and credentials'."""
65
+ assert "doctor" not in _shared._hint_for(make())
66
+
67
+
68
+
69
+ @pytest.mark.unit
70
+ @pytest.mark.parametrize("exc", [
71
+ __import__("json").JSONDecodeError("Expecting value", "<html>", 0),
72
+ KeyError("summary.capacity"),
73
+ UnicodeDecodeError("utf-8", b"\xff", 0, 1, "invalid start byte"),
74
+ ])
75
+ def test_parse_errors_and_bare_key_errors_keep_the_doctor_hint(exc) -> None:
76
+ """Independent review 2026-10-17: a reverse proxy answering HTML raises
77
+ JSONDecodeError (a ValueError), and a programming slip raises a bare KeyError;
78
+ neither message says what to change, so they must not be told it does."""
79
+ assert _shared._hint_for(exc) == _shared._DOCTOR_HINT
80
+
81
+
82
+ @pytest.mark.unit
83
+ def test_an_unknown_target_is_still_a_key_error() -> None:
84
+ """Callers catch KeyError from get_target; the subclass must not break them."""
85
+ assert issubclass(type(_unknown_target()), KeyError)