vmware-aiops 2.3.0__tar.gz → 2.3.2__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 (196) hide show
  1. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/.claude-plugin/plugin.json +1 -1
  2. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/.mcp.json +1 -1
  3. vmware_aiops-2.3.0/README.md → vmware_aiops-2.3.2/PKG-INFO +36 -0
  4. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/README-CN.md +10 -0
  5. vmware_aiops-2.3.0/PKG-INFO → vmware_aiops-2.3.2/README.md +12 -24
  6. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/RELEASE_NOTES.md +44 -0
  7. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/openclaw.plugin.json +1 -1
  8. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/package.json +1 -1
  9. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/pyproject.toml +3 -3
  10. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/server.json +2 -2
  11. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/skills/vmware-aiops/SKILL.md +9 -7
  12. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/skills/vmware-aiops/references/setup-guide.md +15 -5
  13. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_atexit_handlers_do_not_accumulate.py +8 -3
  14. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_deploy_correctness.py +5 -0
  15. vmware_aiops-2.3.2/tests/eval/regression/test_host_paths_protected.py +239 -0
  16. vmware_aiops-2.3.2/tests/eval/regression/test_login_without_vsphere_role.py +225 -0
  17. vmware_aiops-2.3.2/tests/eval/regression/test_ova_network_mapping.py +107 -0
  18. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_guest_tools_gate.py +3 -1
  19. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/uv.lock +7 -7
  20. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/__init__.py +1 -1
  21. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/cli/_common.py +19 -2
  22. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/cli/deploy.py +4 -1
  23. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/connection.py +32 -0
  24. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/mcp_server/_shared.py +25 -4
  25. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/mcp_server/tools/deploy.py +3 -1
  26. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/mcp_server/tools/summary.py +3 -1
  27. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/guest_gate.py +5 -0
  28. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/guest_ops.py +65 -4
  29. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/ova_deploy.py +54 -2
  30. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/planner.py +26 -5
  31. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/vm_deploy.py +3 -0
  32. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/.clawhubignore +0 -0
  33. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/.env.example +0 -0
  34. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  35. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/.github/ISSUE_TEMPLATE/config.yml +0 -0
  36. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
  37. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  38. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/.github/dependabot.yml +0 -0
  39. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/.gitignore +0 -0
  40. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/Dockerfile +0 -0
  41. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/LICENSE +0 -0
  42. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/SECURITY.md +0 -0
  43. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/config.example.yaml +0 -0
  44. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/docker-compose.yml +0 -0
  45. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/docs/integrations/continue.md +0 -0
  46. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/docs/integrations/cursor.md +0 -0
  47. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/docs/integrations/goose.md +0 -0
  48. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/docs/integrations/localcowork.md +0 -0
  49. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/docs/integrations/mcp-agent.md +0 -0
  50. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/docs/integrations/vscode-copilot.md +0 -0
  51. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/docs/plans/2026-03-09-plan-apply-design.md +0 -0
  52. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/examples/deploy.yaml +0 -0
  53. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/examples/mcp-configs/README.md +0 -0
  54. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/examples/mcp-configs/claude-code.json +0 -0
  55. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/examples/mcp-configs/continue.yaml +0 -0
  56. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/examples/mcp-configs/cursor.json +0 -0
  57. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/examples/mcp-configs/goose.json +0 -0
  58. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/examples/mcp-configs/localcowork.json +0 -0
  59. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/examples/mcp-configs/mcp-agent.yaml +0 -0
  60. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/examples/mcp-configs/vscode-copilot.json +0 -0
  61. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/examples/ollama-local-setup.md +0 -0
  62. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/skills/vmware-aiops/evals/evals.json +0 -0
  63. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/skills/vmware-aiops/references/agent-guardrails.md +0 -0
  64. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/skills/vmware-aiops/references/capabilities.md +0 -0
  65. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/skills/vmware-aiops/references/cli-reference.md +0 -0
  66. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/skills/vmware-aiops/references/investigation-protocol.md +0 -0
  67. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/smithery.yaml +0 -0
  68. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/__init__.py +0 -0
  69. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/conftest.py +0 -0
  70. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/__init__.py +0 -0
  71. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/capability/__init__.py +0 -0
  72. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/capability/_family.py +0 -0
  73. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/capability/_scores.json +0 -0
  74. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/capability/_scoring.py +0 -0
  75. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/capability/_skill.py +0 -0
  76. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/capability/conftest.py +0 -0
  77. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/capability/test_entity_reachability.py +0 -0
  78. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/capability/test_error_actionability.py +0 -0
  79. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/capability/test_tool_description_quality.py +0 -0
  80. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/capability/test_tool_manifest_budget.py +0 -0
  81. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/__init__.py +0 -0
  82. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/_diag_fakes.py +0 -0
  83. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/_pc_fakes.py +0 -0
  84. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_aiops_specific.py +0 -0
  85. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_alarm_health_scale_n1.py +0 -0
  86. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_capability_grader.py +0 -0
  87. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_cli_destructive_guards.py +0 -0
  88. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_cli_writes_guarded.py +0 -0
  89. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_cluster_health_summary_delegation.py +0 -0
  90. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_code_review_fixes.py +0 -0
  91. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_config_path_resolution_is_shared.py +0 -0
  92. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_daemon_calls_are_audited.py +0 -0
  93. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_daemon_host_log_cycles.py +0 -0
  94. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_doctor_authenticates_every_target.py +0 -0
  95. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_documented_gates_match_the_registry.py +0 -0
  96. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_enum_params_match_implementation.py +0 -0
  97. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_env_secret_obfuscation.py +0 -0
  98. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_event_sets_match_real_class_names.py +0 -0
  99. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_events_are_read_newest_first.py +0 -0
  100. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_guest_download_writes_the_local_disk.py +0 -0
  101. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_guest_username_is_explicit.py +0 -0
  102. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_host_log_scan_reads_real_logs.py +0 -0
  103. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_instructions_name_targets.py +0 -0
  104. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_inventory_scale_issue31.py +0 -0
  105. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_list_envelope_conformance.py +0 -0
  106. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_list_plans_is_non_destructive.py +0 -0
  107. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_mcp_frame_marks_failures.py +0 -0
  108. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_release_blockers.py +0 -0
  109. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_result_envelope.py +0 -0
  110. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_returned_failure_is_audited.py +0 -0
  111. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_session_probe_eviction.py +0 -0
  112. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_set_ttl_safety.py +0 -0
  113. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_snapshot_async.py +0 -0
  114. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_text_io_declares_encoding.py +0 -0
  115. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_tool_annotations_match_behaviour.py +0 -0
  116. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_unreachable_host_vmks.py +0 -0
  117. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_v1_5_25_blockers.py +0 -0
  118. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_vim_attribute_conformance.py +0 -0
  119. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_access_info.py +0 -0
  120. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_cluster_mgmt_sanitize.py +0 -0
  121. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_cluster_ttl_plan_gate.py +0 -0
  122. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_concurrent_local_state.py +0 -0
  123. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_config.py +0 -0
  124. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_drs_rules.py +0 -0
  125. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_gate_review_fixes.py +0 -0
  126. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_host_network_mgmt.py +0 -0
  127. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_init_and_onboarding.py +0 -0
  128. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_network_mgmt.py +0 -0
  129. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_no_destructive_ops.py +0 -0
  130. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_plan_gate_narrow_fixes.py +0 -0
  131. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_plan_rollback_final_fixes.py +0 -0
  132. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_reset_alarm_gate.py +0 -0
  133. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_results_name_their_target.py +0 -0
  134. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_safe_error_passthrough.py +0 -0
  135. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_session_pool.py +0 -0
  136. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_stop_signal_runs_atexit.py +0 -0
  137. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_vm_delete_blast_radius.py +0 -0
  138. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_vm_tools_gate.py +0 -0
  139. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/_fsutil.py +0 -0
  140. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/access_requirements.py +0 -0
  141. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/cli/__init__.py +0 -0
  142. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/cli/_root.py +0 -0
  143. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/cli/access.py +0 -0
  144. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/cli/alarm.py +0 -0
  145. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/cli/cluster.py +0 -0
  146. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/cli/doctor.py +0 -0
  147. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/cli/hub.py +0 -0
  148. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/cli/investigate.py +0 -0
  149. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/cli/mcp_config.py +0 -0
  150. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/cli/plan.py +0 -0
  151. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/cli/scan.py +0 -0
  152. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/cli/summary.py +0 -0
  153. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/cli/vm.py +0 -0
  154. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/config.py +0 -0
  155. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/doctor.py +0 -0
  156. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/init_wizard.py +0 -0
  157. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/mcp_server/__init__.py +0 -0
  158. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/mcp_server/__main__.py +0 -0
  159. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/mcp_server/server.py +0 -0
  160. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/mcp_server/tools/__init__.py +0 -0
  161. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/mcp_server/tools/access.py +0 -0
  162. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/mcp_server/tools/alarm.py +0 -0
  163. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/mcp_server/tools/cluster.py +0 -0
  164. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/mcp_server/tools/datastore.py +0 -0
  165. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/mcp_server/tools/guest.py +0 -0
  166. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/mcp_server/tools/network.py +0 -0
  167. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/mcp_server/tools/plan.py +0 -0
  168. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/mcp_server/tools/ttl.py +0 -0
  169. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/mcp_server/tools/vm.py +0 -0
  170. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/notify/__init__.py +0 -0
  171. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/notify/audit.py +0 -0
  172. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/notify/logger.py +0 -0
  173. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/notify/webhook.py +0 -0
  174. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/__init__.py +0 -0
  175. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/_paging.py +0 -0
  176. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/alarm_mgmt.py +0 -0
  177. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/cluster_gate.py +0 -0
  178. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/cluster_mgmt.py +0 -0
  179. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/datastore_browser.py +0 -0
  180. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/gate.py +0 -0
  181. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/health.py +0 -0
  182. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/host_network_mgmt.py +0 -0
  183. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/inventory.py +0 -0
  184. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/iscsi_config.py +0 -0
  185. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/network_mgmt.py +0 -0
  186. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/plan_executor.py +0 -0
  187. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/plan_gate.py +0 -0
  188. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/ttl.py +0 -0
  189. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/vm_delete_gate.py +0 -0
  190. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/vm_gate.py +0 -0
  191. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/vm_lifecycle.py +0 -0
  192. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/policy_environment.py +0 -0
  193. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/scanner/__init__.py +0 -0
  194. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/scanner/alarm_scanner.py +0 -0
  195. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/scanner/log_scanner.py +0 -0
  196. {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/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.0",
5
+ "version": "2.3.2",
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.0",
8
+ "vmware-aiops==2.3.2",
9
9
  "vmware-aiops-mcp"
10
10
  ]
11
11
  }
@@ -1,3 +1,27 @@
1
+ Metadata-Version: 2.5
2
+ Name: vmware-aiops
3
+ Version: 2.3.2
4
+ Summary: VMware vCenter/ESXi AI-powered VM lifecycle and deployment tool
5
+ Project-URL: Homepage, https://github.com/vmware-skills/VMware-AIops
6
+ Project-URL: Repository, https://github.com/vmware-skills/VMware-AIops
7
+ Project-URL: Issues, https://github.com/vmware-skills/VMware-AIops/issues
8
+ Project-URL: Changelog, https://github.com/vmware-skills/VMware-AIops/blob/main/RELEASE_NOTES.md
9
+ Author-email: Wei Zhou <wei-wz.zhou@broadcom.com>
10
+ License-Expression: MIT
11
+ License-File: LICENSE
12
+ Requires-Python: >=3.10
13
+ Requires-Dist: apscheduler<4.0,>=3.10
14
+ Requires-Dist: httpx<1.0,>=0.27
15
+ Requires-Dist: mcp[cli]<2.0,>=1.28
16
+ Requires-Dist: pyaml<27.0,>=24.0
17
+ Requires-Dist: python-dotenv<2.0,>=1.0
18
+ Requires-Dist: pyvmomi<10.0,>=8.0.3.0
19
+ Requires-Dist: rich<16.0,>=13.0
20
+ Requires-Dist: typer<1.0,>=0.12
21
+ Requires-Dist: vmware-monitor<3.0,>=2.3.1
22
+ Requires-Dist: vmware-policy<2.0,>=1.23.1
23
+ Description-Content-Type: text/markdown
24
+
1
25
  <!-- mcp-name: io.github.vmware-skills/vmware-aiops -->
2
26
  # VMware AIops
3
27
 
@@ -436,6 +460,9 @@ Run `vmware-aiops plan list` to see failed plan status. Ask user if they want to
436
460
  1. Verify target is reachable: `vmware-aiops doctor`
437
461
  2. For self-signed certs: set `verify_ssl: false` in config.yaml (lab environments only)
438
462
 
463
+ ### `doctor` reports "no vSphere role", or every command fails with "session is not authenticated"
464
+ The account logs in but has no vSphere role, so vCenter gives it no session (AIops#47). Assign it a role (Read-Only or higher) on the vCenter root folder: Administration > Access Control > Global Permissions. Changing the password will not help. `vmware-aiops doctor` reports this case by name, and `attention` counts such a target as unread (overall at least WARN).
465
+
439
466
  ---
440
467
 
441
468
  ## Supported AI Platforms
@@ -1164,6 +1191,15 @@ claude mcp add --transport http vmware-aiops https://mcp.example.com:8443/mcp \
1164
1191
  before it binds that unauthenticated requests get 401. Use a certificate your clients trust for those names.
1165
1192
  - Behind a TLS-terminating reverse proxy on the same machine use `--transport http --host 127.0.0.1
1166
1193
  --public-name mcp.example.com` and pass the `Authorization` header through.
1194
+ - Behind a load balancer or gateway on another machine, keep `--transport https` (a self-signed certificate is
1195
+ fine), list in `--public-name` the host name the balancer sends as `Host`, and have it forward `Authorization`.
1196
+ Its health probe can use `GET /healthz`, which needs no token and returns only `{"status": "ok"}`
1197
+ (vmware-policy ≥ 1.23.0).
1198
+ - Send `traceparent` or `X-Request-ID` with each request to have it stored on the audit row;
1199
+ `vmware-audit log --correlation-id ID` then finds the calls behind one platform request.
1200
+ - Tools that read or write files on this server (here: guest file upload/download, OVA and deployment-spec files) never touch its configuration, tokens, audit log or
1201
+ TLS key, and over the network they are off unless `VMWARE_MCP_FILE_ROOT` names a directory for them
1202
+ (vmware-policy ≥ 1.23.1).
1167
1203
  - The audit log records each caller as `token:<name>`. Calls run side by side, with up to `VMWARE_MCP_POOL_SIZE`
1168
1204
  (default 2) sessions per target; each session is a login on the target, and a call waits at most 300 s for one.
1169
1205
  - **Everyone who can reach the endpoint acts with this server's account.** Tokens identify callers; they grant
@@ -379,6 +379,9 @@ vSphere 中 VM 名称区分大小写。请使用 `vmware-monitor inventory vms`
379
379
  1. 验证目标可达:`vmware-aiops doctor`
380
380
  2. 自签名证书:在 config.yaml 中设置 `verify_ssl: false`(仅限实验环境)
381
381
 
382
+ ### `doctor` 报 "no vSphere role",或所有命令都报 "session is not authenticated"
383
+ 账号能登录,但没有任何 vSphere 角色,所以 vCenter 不给它会话(AIops#47)。在 vCenter 根文件夹上给它分配角色(Read-Only 或更高):Administration > Access Control > Global Permissions。改密码没有用。`vmware-aiops doctor` 会直接报出这种情况,`attention` 也会把这样的目标算作未读取(总体状态至少为 WARN)。
384
+
382
385
  ---
383
386
 
384
387
  ## 支持的 AI 平台
@@ -829,6 +832,13 @@ claude mcp add --transport http vmware-aiops https://mcp.example.com:8443/mcp \
829
832
  证书要用客户端信任、且覆盖这些名字的证书。
830
833
  - 在同一台机器上由反向代理终结 TLS 时,用 `--transport http --host 127.0.0.1 --public-name mcp.example.com`,
831
834
  并让代理透传 `Authorization` 头。
835
+ - 负载均衡或网关在另一台机器上时,保持 `--transport https`(自签证书也可以),在 `--public-name` 里写上负载均衡
836
+ 转发时带的 `Host` 名,并让它透传 `Authorization`。健康检查可以用 `GET /healthz`:不需要 token,只返回
837
+ `{"status": "ok"}`(需 vmware-policy ≥ 1.23.0)。
838
+ - 每个请求带上 `traceparent` 或 `X-Request-ID`,它会记进审计记录;用 `vmware-audit log --correlation-id ID`
839
+ 就能查到平台上某一次请求对应的调用。
840
+ - 会读写本机文件的工具(这里是:虚拟机文件上传/下载、OVA 和部署 spec 文件)永远碰不到本机的配置、token、审计日志和 TLS 私钥;通过网络访问时,
841
+ 除非用 `VMWARE_MCP_FILE_ROOT` 指定一个目录,否则这些工具不能访问本机文件(需 vmware-policy ≥ 1.23.1)。
832
842
  - 审计日志把每个调用者记为 `token:<名字>`。调用并行执行,每个 target 最多 `VMWARE_MCP_POOL_SIZE`(默认 2)
833
843
  个会话;每个会话都是在该 target 上的一次登录,一个调用最多等待 300 秒拿到会话。
834
844
  - **能连到这个端点的人,都以该 server 配置的账号操作。** token 只用来识别调用者,不在平台上授予任何权限。
@@ -1,27 +1,3 @@
1
- Metadata-Version: 2.5
2
- Name: vmware-aiops
3
- Version: 2.3.0
4
- Summary: VMware vCenter/ESXi AI-powered VM lifecycle and deployment tool
5
- Project-URL: Homepage, https://github.com/vmware-skills/VMware-AIops
6
- Project-URL: Repository, https://github.com/vmware-skills/VMware-AIops
7
- Project-URL: Issues, https://github.com/vmware-skills/VMware-AIops/issues
8
- Project-URL: Changelog, https://github.com/vmware-skills/VMware-AIops/blob/main/RELEASE_NOTES.md
9
- Author-email: Wei Zhou <wei-wz.zhou@broadcom.com>
10
- License-Expression: MIT
11
- License-File: LICENSE
12
- Requires-Python: >=3.10
13
- Requires-Dist: apscheduler<4.0,>=3.10
14
- Requires-Dist: httpx<1.0,>=0.27
15
- Requires-Dist: mcp[cli]<2.0,>=1.28
16
- Requires-Dist: pyaml<27.0,>=24.0
17
- Requires-Dist: python-dotenv<2.0,>=1.0
18
- Requires-Dist: pyvmomi<10.0,>=8.0.3.0
19
- Requires-Dist: rich<16.0,>=13.0
20
- Requires-Dist: typer<1.0,>=0.12
21
- Requires-Dist: vmware-monitor<3.0,>=2.0.0
22
- Requires-Dist: vmware-policy<2.0,>=1.22.0
23
- Description-Content-Type: text/markdown
24
-
25
1
  <!-- mcp-name: io.github.vmware-skills/vmware-aiops -->
26
2
  # VMware AIops
27
3
 
@@ -460,6 +436,9 @@ Run `vmware-aiops plan list` to see failed plan status. Ask user if they want to
460
436
  1. Verify target is reachable: `vmware-aiops doctor`
461
437
  2. For self-signed certs: set `verify_ssl: false` in config.yaml (lab environments only)
462
438
 
439
+ ### `doctor` reports "no vSphere role", or every command fails with "session is not authenticated"
440
+ The account logs in but has no vSphere role, so vCenter gives it no session (AIops#47). Assign it a role (Read-Only or higher) on the vCenter root folder: Administration > Access Control > Global Permissions. Changing the password will not help. `vmware-aiops doctor` reports this case by name, and `attention` counts such a target as unread (overall at least WARN).
441
+
463
442
  ---
464
443
 
465
444
  ## Supported AI Platforms
@@ -1188,6 +1167,15 @@ claude mcp add --transport http vmware-aiops https://mcp.example.com:8443/mcp \
1188
1167
  before it binds that unauthenticated requests get 401. Use a certificate your clients trust for those names.
1189
1168
  - Behind a TLS-terminating reverse proxy on the same machine use `--transport http --host 127.0.0.1
1190
1169
  --public-name mcp.example.com` and pass the `Authorization` header through.
1170
+ - Behind a load balancer or gateway on another machine, keep `--transport https` (a self-signed certificate is
1171
+ fine), list in `--public-name` the host name the balancer sends as `Host`, and have it forward `Authorization`.
1172
+ Its health probe can use `GET /healthz`, which needs no token and returns only `{"status": "ok"}`
1173
+ (vmware-policy ≥ 1.23.0).
1174
+ - Send `traceparent` or `X-Request-ID` with each request to have it stored on the audit row;
1175
+ `vmware-audit log --correlation-id ID` then finds the calls behind one platform request.
1176
+ - Tools that read or write files on this server (here: guest file upload/download, OVA and deployment-spec files) never touch its configuration, tokens, audit log or
1177
+ TLS key, and over the network they are off unless `VMWARE_MCP_FILE_ROOT` names a directory for them
1178
+ (vmware-policy ≥ 1.23.1).
1191
1179
  - The audit log records each caller as `token:<name>`. Calls run side by side, with up to `VMWARE_MCP_POOL_SIZE`
1192
1180
  (default 2) sessions per target; each session is a login on the target, and a call waits at most 300 s for one.
1193
1181
  - **Everyone who can reach the endpoint acts with this server's account.** Tokens identify callers; they grant
@@ -1,3 +1,47 @@
1
+ ## v2.3.2 — tools may not touch this server's own control files
2
+
3
+ * **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
4
+ overwrite `$OPS_HOME/mcp-tokens` or `rules.yaml`, or read a skill's `.env`. A path that resolves into `$OPS_HOME`,
5
+ a `~/.vmware-*` directory, a `VMWARE_*_CONFIG` directory, or the server's token file or TLS key/certificate is now
6
+ refused before anything is read or written. Over a network transport these tools read and write host files only
7
+ inside `VMWARE_MCP_FILE_ROOT` (unset: refused). Needs vmware-policy 1.23.1.
8
+ * `download_vm_guest_file` writes without following a link swapped in during the transfer (`O_NOFOLLOW`, a
9
+ re-check after the transfer) and refuses a hard-linked destination; the `provision_vm_guest` preview no longer
10
+ reports the size of a file the run would refuse. Uploads read the source the same way, and a FIFO no longer
11
+ blocks either direction.
12
+ * **`plan_id` can no longer name a path.** `apply_vm_plan` / `rollback_vm_plan` / `list`/`delete` built the plan file
13
+ name from the caller's `plan_id`, and saving used the id stored in the file, so a crafted plan could be written to
14
+ any `*.json` on the host. Plan ids are now `plan-` plus letters, digits and hyphens, and a plan file whose stored id
15
+ differs from its name is refused.
16
+
17
+ ## v2.3.1 — an account with no vSphere role is named, not passed (AIops#47)
18
+
19
+ Needs vmware-policy 1.23.0, and the vmware-monitor release with the same fix for the `attention` part.
20
+
21
+ * **A login with no vSphere role fails at connect time with the reason.** vCenter accepts the login but shows no
22
+ session for it, and every call then fails with `NotAuthenticated`. The connection is now closed at once and
23
+ `doctor`, the CLI and MCP tools say the account most likely has no vSphere role and to assign one (Read-Only or
24
+ higher). Reported on vCenter 8.0.3; not yet reproduced on our own hardware.
25
+ * Refused only when two signals agree: no session, **and** vCenter refuses a view of the root folder
26
+ (`NotAuthenticated` / `NoPermission`). An account scoped to one folder can also show no session, and still
27
+ connects; anything the check cannot read lets the login through.
28
+ * **`NotAuthenticated` later on names both causes**: the session expired (retry), or the account's role was
29
+ removed. It no longer says "verify connectivity and credentials".
30
+ * **Every CLI command prints one line for a vSphere fault.** `summary` (and any other command) printed a raw
31
+ traceback for faults other than a failed login; they now print the fault's message and what to check.
32
+ * **`attention` never reports OK for targets it could not read** (computed by vmware-monitor): any unread target
33
+ makes the overall status at least WARN, none read makes it CRITICAL, and the console and HTML say how many were
34
+ not read. A role-less target is listed as unreachable with reason `NoVSphereRoleError`.
35
+
36
+ ## Unreleased — OVA deploy honours `network_name`
37
+
38
+ * **`deploy_vm_from_ova` / `deploy ova --network` now attach the NICs to the network you name.** The import spec
39
+ carried no network mapping, so the argument had no effect and each NIC landed on whatever network the OVF
40
+ descriptor named. Every network the OVF declares is now mapped to the named port group (standard or distributed).
41
+ A name that matches no network, or more than one (a standard and a distributed port group can share a name), is
42
+ refused before anything is imported, with the available names in the error. An OVF descriptor the server cannot
43
+ parse is refused the same way. Found in the 2026-10-16 pre-release review.
44
+
1
45
  ## v2.3.0 — calls on one target run side by side; `get_access_info`
2
46
 
3
47
  Needs vmware-policy 1.22.0.
@@ -11,7 +11,7 @@
11
11
  "command": "uvx",
12
12
  "args": [
13
13
  "--from",
14
- "vmware-aiops==2.3.0",
14
+ "vmware-aiops==2.3.2",
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.0",
3
+ "version": "2.3.2",
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.0"
7
+ version = "2.3.2"
8
8
  description = "VMware vCenter/ESXi AI-powered VM lifecycle and deployment tool"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -19,11 +19,11 @@ dependencies = [
19
19
  "httpx>=0.27,<1.0",
20
20
  "python-dotenv>=1.0,<2.0",
21
21
  "mcp[cli]>=1.28,<2.0",
22
- "vmware-policy>=1.22.0,<2.0",
22
+ "vmware-policy>=1.23.1,<2.0",
23
23
  # Read-only cluster-health triage (summary + HTML snapshot) is owned by
24
24
  # vmware-monitor; AIops delegates to it so a conversation with the AIops
25
25
  # entry point can produce the same report without duplicating the code.
26
- "vmware-monitor>=2.0.0,<3.0",
26
+ "vmware-monitor>=2.3.1,<3.0", # attention status floor + role-less login (AIops#47)
27
27
  ]
28
28
 
29
29
 
@@ -7,12 +7,12 @@
7
7
  "url": "https://github.com/vmware-skills/VMware-AIops",
8
8
  "source": "github"
9
9
  },
10
- "version": "2.3.0",
10
+ "version": "2.3.2",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "pypi",
14
14
  "identifier": "vmware-aiops",
15
- "version": "2.3.0",
15
+ "version": "2.3.2",
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.0
66
+ uv tool install vmware-aiops==2.3.2
67
67
  vmware-aiops doctor
68
68
  vmware-aiops hub status # see which family members are installed
69
69
  ```
@@ -285,11 +285,10 @@ Use `run_vm_guest_command_with_output` instead of `run_vm_guest_command` — it
285
285
  Large OVA files (>10GB) may exceed the default 120s timeout. The upload happens via HTTP NFC lease — ensure network between the machine running vmware-aiops and ESXi is stable.
286
286
 
287
287
  ### Snapshot delete is slow / "still running after Ns"
288
- Deleting an old or large snapshot consolidates its delta disk into the parent — the slowest write
289
- operation, often several minutes. `vm snapshot-delete` waits up to 30 min by default; if it still
290
- returns a "still running, NOT failed" message with a task id, the delete did **not** fail — poll it with
291
- `vm task-status <task-id>`. Do not re-issue the delete or hand-roll polling. For very large snapshots,
292
- prefer `vm snapshot-delete <name> --name <snap> --no-wait` to get the task id immediately and poll.
288
+ Deleting a large snapshot consolidates its delta disk, often for minutes. `vm snapshot-delete` waits
289
+ up to 30 min; a "still running, NOT failed" message with a task id means the delete did **not** fail —
290
+ poll `vm task-status <task-id>`; do not re-issue it. For large ones, use
291
+ `vm snapshot-delete <name> --name <snap> --no-wait` and poll.
293
292
 
294
293
  ### Plan apply fails mid-way
295
294
  Run `vmware-aiops plan list` to see failed plan status. Ask user if they want to rollback with `rollback_vm_plan`. Irreversible steps (delete_vm) are skipped during rollback.
@@ -298,10 +297,13 @@ Run `vmware-aiops plan list` to see failed plan status. Ask user if they want to
298
297
  1. Verify target is reachable: `vmware-aiops doctor`
299
298
  2. For self-signed certs: set `verify_ssl: false` in config.yaml (lab environments only)
300
299
 
300
+ ### Every command fails with "session is not authenticated"
301
+ The account has no vSphere role. Assign one (Read-Only or higher) on the vCenter root folder; `vmware-aiops doctor` reports this case by name.
302
+
301
303
  ## Setup
302
304
 
303
305
  ```bash
304
- uv tool install vmware-aiops==2.3.0
306
+ uv tool install vmware-aiops==2.3.2
305
307
  mkdir -p ~/.vmware-aiops
306
308
  vmware-aiops init # generates config.yaml and .env templates
307
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.0
9
+ uv tool install vmware-aiops==2.3.2
10
10
 
11
11
  # Via Skills.sh (fetches from GitHub)
12
- npx skills add vmware-skills/VMware-AIops#v2.3.0
12
+ npx skills add vmware-skills/VMware-AIops#v2.3.2
13
13
 
14
14
  # Via ClawHub (fetches from ClawHub registry snapshot of GitHub)
15
- clawhub install @zw008/vmware-aiops --version 2.3.0
15
+ clawhub install @zw008/vmware-aiops --version 2.3.2
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.0
38
+ uv tool install vmware-aiops==2.3.2
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.0 https://github.com/vmware-skills/VMware-AIops.git
76
+ git clone --branch v2.3.2 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
@@ -226,6 +226,16 @@ vmware-aiops mcp --transport https --host 0.0.0.0 --port 8443 \
226
226
  - Behind a reverse proxy that terminates TLS (Nginx, HAProxy) on the same machine, use
227
227
  `--transport http --host 127.0.0.1 --public-name mcp.example.com` (the names the proxy forwards in `Host`), and
228
228
  have the proxy pass the `Authorization` header through.
229
+ - Behind a load balancer or gateway on another machine, keep `--transport https` (a self-signed certificate is
230
+ fine: the balancer is the client that has to trust it), list in `--public-name` the host name the balancer sends
231
+ as `Host`, and have it forward `Authorization`. Point its health probe at `GET /healthz`: it needs no token, returns
232
+ only `{"status": "ok"}` and accepts any `Host` (vmware-policy ≥ 1.23.0). Every other path still needs the token.
233
+ - Correlation: each audit row stores the caller's `traceparent` trace-id, else its `X-Request-ID` /
234
+ `X-Correlation-ID`. Have the agent platform send one, and `vmware-audit log --correlation-id ID` lists the calls
235
+ behind one platform request.
236
+ - Tools that read or write files on this server (here: guest file upload/download, OVA and deployment-spec files) never touch its configuration, tokens, audit log or
237
+ TLS key, and over the network they are off unless `VMWARE_MCP_FILE_ROOT` names a directory for them
238
+ (vmware-policy ≥ 1.23.1).
229
239
  - `vmware-audit token list` / `token revoke NAME` manage callers; a revoked token is refused from its next request.
230
240
  Replace a leaked token under a new name.
231
241
  - The server will not start without at least one token, and checks before it binds that unauthenticated requests
@@ -20,8 +20,9 @@ that registered its own handler would pass with the production one deleted.
20
20
  from __future__ import annotations
21
21
 
22
22
  import gc
23
+ import itertools
23
24
  import weakref
24
- from unittest.mock import MagicMock, patch
25
+ from unittest.mock import MagicMock, PropertyMock, patch
25
26
 
26
27
  import pytest
27
28
 
@@ -44,9 +45,13 @@ def _target(name: str = "vcenter-prod") -> TargetConfig:
44
45
 
45
46
 
46
47
  def _dead_si(*_a, **_kw):
47
- """An SI whose liveness probe says dead, so the next connect() evicts it."""
48
+ """An SI that is alive at login and dead from then on, so the next connect()
49
+ evicts it. Dead from the start would be refused at login as a role-less
50
+ account (AIops#47), which is a different path."""
48
51
  si = MagicMock()
49
- si.content.sessionManager.currentSession = None
52
+ type(si.content.sessionManager).currentSession = PropertyMock(
53
+ side_effect=itertools.chain([object()], itertools.repeat(None))
54
+ )
50
55
  return si
51
56
 
52
57
 
@@ -235,6 +235,11 @@ def test_deploy_ova_maps_device_urls_by_importkey_out_of_order() -> None:
235
235
 
236
236
  content = MagicMock()
237
237
  content.ovfManager.CreateImportSpec.return_value = import_spec_result
238
+ # A descriptor that parses cleanly and declares no networks (mapping is
239
+ # covered in test_ova_network_mapping.py).
240
+ content.ovfManager.ParseDescriptor.return_value = SimpleNamespace(
241
+ error=None, warning=None, network=[]
242
+ )
238
243
  si = MagicMock()
239
244
  si.RetrieveContent.return_value = content
240
245
 
@@ -0,0 +1,239 @@
1
+ """Regression: tools could read or write this server's own control files (HLD §18 step 0).
2
+
3
+ Over HTTPS any token holder could call ``download_vm_guest_file(local_path=
4
+ "~/.vmware/mcp-tokens", overwrite=True)`` and replace the token file, or upload
5
+ a skill's ``.env`` into a VM. Every place that opens a caller-supplied host path
6
+ now refuses one that resolves into ``$OPS_HOME`` or a ``~/.vmware-*`` directory,
7
+ before anything is read, written or transferred.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import os
13
+ from pathlib import Path
14
+ from unittest.mock import MagicMock, patch
15
+
16
+ import pytest
17
+ from vmware_policy.hostpaths import HostPathRefusedError
18
+
19
+ from vmware_aiops.ops import guest_ops, ova_deploy, vm_deploy
20
+
21
+
22
+ @pytest.fixture
23
+ def ops_home() -> Path:
24
+ home = Path(os.environ["OPS_HOME"]) # the sandbox conftest set
25
+ home.mkdir(parents=True, exist_ok=True)
26
+ tokens = home / "mcp-tokens"
27
+ tokens.write_text("alice:abc\n", encoding="utf-8")
28
+ return home
29
+
30
+
31
+ def test_download_cannot_overwrite_the_token_file(ops_home):
32
+ si = MagicMock()
33
+ with pytest.raises(HostPathRefusedError):
34
+ guest_ops.guest_download(
35
+ si, "vm1", "/etc/hosts", str(ops_home / "mcp-tokens"), "u", "p", overwrite=True
36
+ )
37
+ assert (ops_home / "mcp-tokens").read_text(encoding="utf-8") == "alice:abc\n"
38
+ si.RetrieveContent.assert_not_called()
39
+
40
+
41
+ def test_upload_cannot_send_a_control_file_into_a_vm(ops_home):
42
+ si = MagicMock()
43
+ with patch.object(guest_ops, "_require_vm_with_tools", return_value=MagicMock()), \
44
+ patch("urllib.request.urlopen") as urlopen:
45
+ with pytest.raises(HostPathRefusedError):
46
+ guest_ops.guest_upload(
47
+ si, "vm1", str(ops_home / "mcp-tokens"), "/tmp/x", "u", "p"
48
+ )
49
+ urlopen.assert_not_called()
50
+ fm = si.RetrieveContent.return_value.guestOperationsManager.fileManager
51
+ fm.InitiateFileTransferToGuest.assert_not_called()
52
+
53
+
54
+ def test_a_deploy_spec_inside_ops_home_is_not_read(ops_home):
55
+ with pytest.raises(HostPathRefusedError):
56
+ vm_deploy.load_deploy_spec(str(ops_home / "rules.yaml"))
57
+
58
+
59
+ def test_an_ova_inside_ops_home_is_not_read(ops_home):
60
+ with pytest.raises(HostPathRefusedError):
61
+ ova_deploy._read_ovf_from_ova(str(ops_home / "audit.db"))
62
+
63
+
64
+ def test_the_mcp_tool_refuses_before_running(ops_home):
65
+ from vmware_aiops.mcp_server.tools import guest as guest_tools
66
+
67
+ with patch.object(guest_tools, "_get_connection") as connect:
68
+ with pytest.raises(HostPathRefusedError):
69
+ guest_tools.download_vm_guest_file(
70
+ vm_name="vm1",
71
+ guest_path="/etc/hosts",
72
+ local_path=str(ops_home / "mcp-tokens"),
73
+ username="u",
74
+ password="p",
75
+ overwrite=True,
76
+ )
77
+ connect.assert_not_called()
78
+
79
+
80
+ def test_ordinary_paths_still_work(tmp_path):
81
+ spec = tmp_path / "spec.yaml"
82
+ spec.write_text("vms: []\n", encoding="utf-8")
83
+ assert vm_deploy.load_deploy_spec(str(spec)) is not None
84
+
85
+
86
+ # ── review 2026-10-16: hard links, symlink swapped in after the check, preview ──
87
+
88
+
89
+ def _download_with_fake_transfer(local: str, overwrite: bool, before_write=None):
90
+ """Run guest_download with the guest transfer faked; call before_write after the check."""
91
+ si = MagicMock()
92
+ vm = MagicMock()
93
+ resp = MagicMock()
94
+ resp.__enter__.return_value.read.return_value = b"payload"
95
+
96
+ def urlopen(*a, **k):
97
+ if before_write:
98
+ before_write()
99
+ return resp
100
+
101
+ with patch.object(guest_ops, "_require_vm_with_tools", return_value=vm), \
102
+ patch("urllib.request.urlopen", side_effect=urlopen), \
103
+ patch.object(guest_ops, "get_verify_ssl", return_value=True):
104
+ return guest_ops.guest_download(
105
+ si, "vm1", "/etc/motd", local, "u", "p", overwrite=overwrite
106
+ )
107
+
108
+
109
+ def test_a_hard_link_to_a_protected_file_is_not_written_through(ops_home, tmp_path):
110
+ outside = tmp_path / "innocent.txt"
111
+ os.link(ops_home / "mcp-tokens", outside)
112
+ with pytest.raises(ValueError, match="hard link"):
113
+ _download_with_fake_transfer(str(outside), overwrite=True)
114
+ assert (ops_home / "mcp-tokens").read_text(encoding="utf-8") == "alice:abc\n"
115
+
116
+
117
+ def test_a_symlink_swapped_in_during_the_transfer_is_not_followed(ops_home, tmp_path):
118
+ dest = tmp_path / "out.txt"
119
+
120
+ def swap():
121
+ dest.symlink_to(ops_home / "mcp-tokens")
122
+
123
+ with pytest.raises((HostPathRefusedError, OSError)):
124
+ _download_with_fake_transfer(str(dest), overwrite=True, before_write=swap)
125
+ assert (ops_home / "mcp-tokens").read_text(encoding="utf-8") == "alice:abc\n"
126
+
127
+
128
+ def test_an_ordinary_download_is_written(tmp_path):
129
+ dest = tmp_path / "out.txt"
130
+ _download_with_fake_transfer(str(dest), overwrite=False)
131
+ assert dest.read_bytes() == b"payload"
132
+
133
+
134
+ def test_the_provision_preview_does_not_reveal_a_protected_file(ops_home):
135
+ from vmware_aiops.ops import guest_gate
136
+
137
+ info, blocker = guest_gate._local_file(str(ops_home / "mcp-tokens"))
138
+ assert info["local_size_bytes"] is None
139
+ assert blocker and "configuration, tokens and audit log" in blocker
140
+
141
+
142
+ def test_a_parent_dir_swapped_for_a_link_during_the_transfer_is_caught(ops_home, tmp_path):
143
+ # O_NOFOLLOW guards only the last component; the re-check after the transfer
144
+ # catches a parent directory replaced by a link into OPS_HOME.
145
+ sub = tmp_path / "sub"
146
+ sub.mkdir()
147
+
148
+ def swap():
149
+ sub.rmdir()
150
+ sub.symlink_to(ops_home, target_is_directory=True)
151
+
152
+ with pytest.raises(HostPathRefusedError):
153
+ _download_with_fake_transfer(str(sub / "rules.yaml"), overwrite=False, before_write=swap)
154
+ assert not (ops_home / "rules.yaml").exists()
155
+
156
+
157
+ # ── second review 2026-10-16: plan_id was an unchecked host path ──────────────
158
+
159
+
160
+ @pytest.mark.parametrize(
161
+ "bad",
162
+ ["/tmp/evil", "../../.cursor/mcp", "plan-x/../../evil", "plan-..", "", "plan-a.b", "x" * 300],
163
+ )
164
+ def test_a_plan_id_cannot_name_a_path(bad, tmp_path, monkeypatch):
165
+ from vmware_aiops.ops import planner
166
+
167
+ monkeypatch.setattr(planner, "_PLANS_DIR", tmp_path / "plans")
168
+ with pytest.raises(ValueError, match="plan id"):
169
+ planner.load_plan(bad)
170
+ with pytest.raises(ValueError, match="plan id"):
171
+ planner.delete_plan(bad)
172
+ with pytest.raises(ValueError, match="plan id"):
173
+ planner.save_plan({"plan_id": bad, "steps": []})
174
+
175
+
176
+ def test_a_plan_file_naming_another_plan_id_is_refused(tmp_path, monkeypatch):
177
+ import json
178
+
179
+ from vmware_aiops.ops import planner
180
+
181
+ plans = tmp_path / "plans"
182
+ plans.mkdir()
183
+ monkeypatch.setattr(planner, "_PLANS_DIR", plans)
184
+ (plans / "plan-20261016-000000-abcd.json").write_text(
185
+ json.dumps({"plan_id": "../../.cursor/mcp", "steps": []}), encoding="utf-8"
186
+ )
187
+ with pytest.raises(ValueError, match="plan id"):
188
+ planner.load_plan("plan-20261016-000000-abcd")
189
+
190
+
191
+ def test_a_generated_plan_id_round_trips(tmp_path, monkeypatch):
192
+ from vmware_aiops.ops import planner
193
+
194
+ monkeypatch.setattr(planner, "_PLANS_DIR", tmp_path / "plans")
195
+ (tmp_path / "plans").mkdir()
196
+ pid = planner._generate_plan_id()
197
+ planner.save_plan({"plan_id": pid, "steps": []})
198
+ assert planner.load_plan(pid)["plan_id"] == pid
199
+ planner.delete_plan(pid)
200
+ assert planner.load_plan(pid) is None
201
+
202
+
203
+ def test_download_onto_a_fifo_does_not_hang(tmp_path):
204
+ fifo = tmp_path / "pipe"
205
+ os.mkfifo(fifo)
206
+ with pytest.raises((ValueError, OSError)):
207
+ _download_with_fake_transfer(str(fifo), overwrite=True)
208
+
209
+
210
+ def test_upload_reads_a_regular_file_without_following_a_swapped_link(ops_home, tmp_path):
211
+ src = tmp_path / "src.txt"
212
+ src.write_text("data", encoding="utf-8")
213
+ assert guest_ops._read_upload_source(src) == b"data"
214
+ link = tmp_path / "link.txt"
215
+ link.symlink_to(ops_home / "mcp-tokens")
216
+ with pytest.raises(OSError):
217
+ guest_ops._read_upload_source(link)
218
+
219
+
220
+ def test_upload_source_swapped_for_a_fifo_is_refused_not_hung(tmp_path):
221
+ fifo = tmp_path / "pipe"
222
+ os.mkfifo(fifo)
223
+ with pytest.raises(ValueError, match="regular file"):
224
+ guest_ops._read_upload_source(fifo)
225
+
226
+
227
+ def test_a_plan_id_with_a_trailing_newline_is_refused(tmp_path, monkeypatch):
228
+ from vmware_aiops.ops import planner
229
+
230
+ monkeypatch.setattr(planner, "_PLANS_DIR", tmp_path / "plans")
231
+ with pytest.raises(ValueError, match="plan id"):
232
+ planner.load_plan("plan-abc\n")
233
+
234
+
235
+ def test_an_upload_source_that_is_a_hard_link_is_refused(ops_home, tmp_path):
236
+ link = tmp_path / "innocent.txt"
237
+ os.link(ops_home / "mcp-tokens", link)
238
+ with pytest.raises(ValueError, match="hard link"):
239
+ guest_ops._read_upload_source(link)