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.
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/.claude-plugin/plugin.json +1 -1
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/.mcp.json +1 -1
- vmware_aiops-2.3.0/README.md → vmware_aiops-2.3.2/PKG-INFO +36 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/README-CN.md +10 -0
- vmware_aiops-2.3.0/PKG-INFO → vmware_aiops-2.3.2/README.md +12 -24
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/RELEASE_NOTES.md +44 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/openclaw.plugin.json +1 -1
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/package.json +1 -1
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/pyproject.toml +3 -3
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/server.json +2 -2
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/skills/vmware-aiops/SKILL.md +9 -7
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/skills/vmware-aiops/references/setup-guide.md +15 -5
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_atexit_handlers_do_not_accumulate.py +8 -3
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_deploy_correctness.py +5 -0
- vmware_aiops-2.3.2/tests/eval/regression/test_host_paths_protected.py +239 -0
- vmware_aiops-2.3.2/tests/eval/regression/test_login_without_vsphere_role.py +225 -0
- vmware_aiops-2.3.2/tests/eval/regression/test_ova_network_mapping.py +107 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_guest_tools_gate.py +3 -1
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/uv.lock +7 -7
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/__init__.py +1 -1
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/cli/_common.py +19 -2
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/cli/deploy.py +4 -1
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/connection.py +32 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/mcp_server/_shared.py +25 -4
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/mcp_server/tools/deploy.py +3 -1
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/mcp_server/tools/summary.py +3 -1
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/guest_gate.py +5 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/guest_ops.py +65 -4
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/ova_deploy.py +54 -2
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/planner.py +26 -5
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/vm_deploy.py +3 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/.clawhubignore +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/.env.example +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/.github/dependabot.yml +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/.gitignore +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/Dockerfile +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/LICENSE +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/SECURITY.md +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/config.example.yaml +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/docker-compose.yml +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/docs/integrations/continue.md +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/docs/integrations/cursor.md +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/docs/integrations/goose.md +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/docs/integrations/localcowork.md +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/docs/integrations/mcp-agent.md +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/docs/integrations/vscode-copilot.md +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/docs/plans/2026-03-09-plan-apply-design.md +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/examples/deploy.yaml +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/examples/mcp-configs/README.md +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/examples/mcp-configs/claude-code.json +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/examples/mcp-configs/continue.yaml +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/examples/mcp-configs/cursor.json +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/examples/mcp-configs/goose.json +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/examples/mcp-configs/localcowork.json +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/examples/mcp-configs/mcp-agent.yaml +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/examples/mcp-configs/vscode-copilot.json +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/examples/ollama-local-setup.md +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/skills/vmware-aiops/evals/evals.json +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/skills/vmware-aiops/references/agent-guardrails.md +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/skills/vmware-aiops/references/capabilities.md +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/skills/vmware-aiops/references/cli-reference.md +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/skills/vmware-aiops/references/investigation-protocol.md +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/smithery.yaml +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/__init__.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/conftest.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/__init__.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/capability/__init__.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/capability/_family.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/capability/_scores.json +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/capability/_scoring.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/capability/_skill.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/capability/conftest.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/capability/test_entity_reachability.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/capability/test_error_actionability.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/capability/test_tool_description_quality.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/capability/test_tool_manifest_budget.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/__init__.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/_diag_fakes.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/_pc_fakes.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_aiops_specific.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_alarm_health_scale_n1.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_capability_grader.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_cli_destructive_guards.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_cli_writes_guarded.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_cluster_health_summary_delegation.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_code_review_fixes.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_config_path_resolution_is_shared.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_daemon_calls_are_audited.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_daemon_host_log_cycles.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_doctor_authenticates_every_target.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_documented_gates_match_the_registry.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_enum_params_match_implementation.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_env_secret_obfuscation.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_event_sets_match_real_class_names.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_events_are_read_newest_first.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_guest_download_writes_the_local_disk.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_guest_username_is_explicit.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_host_log_scan_reads_real_logs.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_instructions_name_targets.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_inventory_scale_issue31.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_list_envelope_conformance.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_list_plans_is_non_destructive.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_mcp_frame_marks_failures.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_release_blockers.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_result_envelope.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_returned_failure_is_audited.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_session_probe_eviction.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_set_ttl_safety.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_snapshot_async.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_text_io_declares_encoding.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_tool_annotations_match_behaviour.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_unreachable_host_vmks.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_v1_5_25_blockers.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/eval/regression/test_vim_attribute_conformance.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_access_info.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_cluster_mgmt_sanitize.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_cluster_ttl_plan_gate.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_concurrent_local_state.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_config.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_drs_rules.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_gate_review_fixes.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_host_network_mgmt.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_init_and_onboarding.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_network_mgmt.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_no_destructive_ops.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_plan_gate_narrow_fixes.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_plan_rollback_final_fixes.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_reset_alarm_gate.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_results_name_their_target.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_safe_error_passthrough.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_session_pool.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_stop_signal_runs_atexit.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_vm_delete_blast_radius.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/tests/test_vm_tools_gate.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/_fsutil.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/access_requirements.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/cli/__init__.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/cli/_root.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/cli/access.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/cli/alarm.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/cli/cluster.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/cli/doctor.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/cli/hub.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/cli/investigate.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/cli/mcp_config.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/cli/plan.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/cli/scan.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/cli/summary.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/cli/vm.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/config.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/doctor.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/init_wizard.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/mcp_server/__init__.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/mcp_server/__main__.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/mcp_server/server.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/mcp_server/tools/__init__.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/mcp_server/tools/access.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/mcp_server/tools/alarm.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/mcp_server/tools/cluster.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/mcp_server/tools/datastore.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/mcp_server/tools/guest.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/mcp_server/tools/network.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/mcp_server/tools/plan.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/mcp_server/tools/ttl.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/mcp_server/tools/vm.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/notify/__init__.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/notify/audit.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/notify/logger.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/notify/webhook.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/__init__.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/_paging.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/alarm_mgmt.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/cluster_gate.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/cluster_mgmt.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/datastore_browser.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/gate.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/health.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/host_network_mgmt.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/inventory.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/iscsi_config.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/network_mgmt.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/plan_executor.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/plan_gate.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/ttl.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/vm_delete_gate.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/vm_gate.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/ops/vm_lifecycle.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/policy_environment.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/scanner/__init__.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/scanner/alarm_scanner.py +0 -0
- {vmware_aiops-2.3.0 → vmware_aiops-2.3.2}/vmware_aiops/scanner/log_scanner.py +0 -0
- {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.
|
|
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",
|
|
@@ -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.
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vmware-aiops"
|
|
7
|
-
version = "2.3.
|
|
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
|
+
"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.
|
|
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.
|
|
10
|
+
"version": "2.3.2",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "pypi",
|
|
14
14
|
"identifier": "vmware-aiops",
|
|
15
|
-
"version": "2.3.
|
|
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.
|
|
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
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
`vm
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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 =
|
|
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)
|