vmware-aiops 2.4.0__tar.gz → 2.4.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.4.0 → vmware_aiops-2.4.2}/.claude-plugin/plugin.json +1 -1
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/.mcp.json +1 -1
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/PKG-INFO +3 -3
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/RELEASE_NOTES.md +79 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/openclaw.plugin.json +1 -1
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/package.json +1 -1
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/pyproject.toml +3 -3
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/server.json +2 -2
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/skills/vmware-aiops/SKILL.md +2 -2
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/skills/vmware-aiops/references/setup-guide.md +5 -5
- vmware_aiops-2.4.2/tests/eval/regression/test_browse_ds_path_join.py +79 -0
- vmware_aiops-2.4.2/tests/eval/regression/test_browse_sees_every_file.py +70 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_cli_writes_guarded.py +3 -1
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_code_review_fixes.py +3 -3
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_daemon_calls_are_audited.py +8 -8
- vmware_aiops-2.4.2/tests/eval/regression/test_deploy_failures_are_failures.py +205 -0
- vmware_aiops-2.4.2/tests/eval/regression/test_error_hints_fit_the_surface.py +85 -0
- vmware_aiops-2.4.2/tests/eval/regression/test_image_registry_cross_process.py +134 -0
- vmware_aiops-2.4.2/tests/eval/regression/test_image_registry_legacy_rows.py +150 -0
- vmware_aiops-2.4.2/tests/eval/regression/test_image_registry_per_target.py +64 -0
- vmware_aiops-2.4.2/tests/eval/regression/test_linked_clone_failure_on_the_wire.py +71 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_list_envelope_conformance.py +1 -1
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_login_without_vsphere_role.py +56 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_set_ttl_safety.py +12 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_snapshot_async.py +6 -1
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_v1_5_25_blockers.py +8 -3
- vmware_aiops-2.4.2/tests/eval/regression/test_write_round_low_findings.py +104 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/test_cluster_ttl_plan_gate.py +2 -2
- vmware_aiops-2.4.2/tests/test_composite_tools_obey_policy.py +218 -0
- vmware_aiops-2.4.2/tests/test_filelock.py +81 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/test_gate_review_fixes.py +2 -2
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/test_host_network_mgmt.py +10 -0
- vmware_aiops-2.4.2/tests/test_migrate_compatibility_check.py +405 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/test_plan_gate_narrow_fixes.py +3 -3
- vmware_aiops-2.4.2/tests/test_plan_steps_obey_policy.py +299 -0
- vmware_aiops-2.4.2/tests/test_power_off_and_preview_policy.py +581 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/uv.lock +10 -10
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/__init__.py +1 -1
- vmware_aiops-2.4.2/vmware_aiops/_filelock.py +94 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/cli/cluster.py +7 -1
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/cli/deploy.py +29 -2
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/cli/vm.py +40 -4
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/config.py +7 -2
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/connection.py +39 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/mcp_server/_shared.py +42 -1
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/mcp_server/tools/datastore.py +15 -3
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/mcp_server/tools/deploy.py +24 -1
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/mcp_server/tools/guest.py +23 -5
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/mcp_server/tools/plan.py +70 -11
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/mcp_server/tools/ttl.py +31 -5
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/mcp_server/tools/vm.py +3 -2
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/ops/cluster_mgmt.py +4 -1
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/ops/datastore_browser.py +147 -24
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/ops/host_network_mgmt.py +16 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/ops/inventory.py +4 -3
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/ops/ova_deploy.py +44 -11
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/ops/plan_executor.py +28 -6
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/ops/plan_gate.py +121 -9
- vmware_aiops-2.4.2/vmware_aiops/ops/step_policy.py +225 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/ops/ttl.py +19 -3
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/ops/vm_deploy.py +86 -23
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/ops/vm_gate.py +129 -4
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/ops/vm_lifecycle.py +103 -38
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/policy_roles.py +19 -3
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/scanner/scheduler.py +46 -2
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/.clawhubignore +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/.env.example +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/.github/ISSUE_TEMPLATE/config.yml +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/.github/ISSUE_TEMPLATE/feature_request.yml +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/.github/dependabot.yml +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/.gitignore +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/Dockerfile +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/LICENSE +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/README-CN.md +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/README.md +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/SECURITY.md +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/config.example.yaml +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/docker-compose.yml +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/docs/integrations/continue.md +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/docs/integrations/cursor.md +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/docs/integrations/goose.md +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/docs/integrations/localcowork.md +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/docs/integrations/mcp-agent.md +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/docs/integrations/vscode-copilot.md +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/docs/plans/2026-03-09-plan-apply-design.md +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/examples/deploy.yaml +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/examples/mcp-configs/README.md +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/examples/mcp-configs/claude-code.json +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/examples/mcp-configs/continue.yaml +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/examples/mcp-configs/cursor.json +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/examples/mcp-configs/goose.json +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/examples/mcp-configs/localcowork.json +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/examples/mcp-configs/mcp-agent.yaml +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/examples/mcp-configs/vscode-copilot.json +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/examples/ollama-local-setup.md +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/skills/vmware-aiops/evals/evals.json +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/skills/vmware-aiops/references/agent-guardrails.md +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/skills/vmware-aiops/references/capabilities.md +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/skills/vmware-aiops/references/cli-reference.md +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/skills/vmware-aiops/references/investigation-protocol.md +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/smithery.yaml +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/__init__.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/conftest.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/__init__.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/capability/__init__.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/capability/_family.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/capability/_scores.json +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/capability/_scoring.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/capability/_skill.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/capability/conftest.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/capability/test_entity_reachability.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/capability/test_error_actionability.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/capability/test_tool_description_quality.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/capability/test_tool_manifest_budget.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/__init__.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/_diag_fakes.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/_pc_fakes.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_aiops_specific.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_alarm_health_scale_n1.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_atexit_handlers_do_not_accumulate.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_capability_grader.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_cli_destructive_guards.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_cluster_health_summary_delegation.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_config_path_resolution_is_shared.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_daemon_host_log_cycles.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_deploy_correctness.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_doctor_authenticates_every_target.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_documented_gates_match_the_registry.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_enum_params_match_implementation.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_env_secret_obfuscation.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_event_sets_match_real_class_names.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_events_are_read_newest_first.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_guest_download_writes_the_local_disk.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_guest_username_is_explicit.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_host_log_scan_reads_real_logs.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_host_paths_protected.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_instructions_name_targets.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_inventory_scale_issue31.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_list_plans_is_non_destructive.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_mcp_frame_marks_failures.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_ova_network_mapping.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_release_blockers.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_result_envelope.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_returned_failure_is_audited.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_session_probe_eviction.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_text_io_declares_encoding.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_tool_annotations_match_behaviour.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_unreachable_host_vmks.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_vim_attribute_conformance.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/test_access_info.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/test_cluster_mgmt_sanitize.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/test_concurrent_local_state.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/test_config.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/test_drs_rules.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/test_guest_tools_gate.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/test_init_and_onboarding.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/test_network_mgmt.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/test_no_destructive_ops.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/test_plan_rollback_final_fixes.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/test_policy_roles.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/test_reset_alarm_gate.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/test_results_name_their_target.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/test_safe_error_passthrough.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/test_session_pool.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/test_stop_signal_runs_atexit.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/test_vm_delete_blast_radius.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/test_vm_tools_gate.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/_fsutil.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/access_requirements.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/cli/__init__.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/cli/_common.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/cli/_root.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/cli/access.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/cli/alarm.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/cli/doctor.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/cli/hub.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/cli/investigate.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/cli/mcp_config.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/cli/plan.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/cli/scan.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/cli/summary.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/doctor.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/init_wizard.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/mcp_server/__init__.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/mcp_server/__main__.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/mcp_server/server.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/mcp_server/tools/__init__.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/mcp_server/tools/access.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/mcp_server/tools/alarm.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/mcp_server/tools/cluster.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/mcp_server/tools/network.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/mcp_server/tools/summary.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/notify/__init__.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/notify/audit.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/notify/logger.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/notify/webhook.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/ops/__init__.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/ops/_paging.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/ops/alarm_mgmt.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/ops/cluster_gate.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/ops/gate.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/ops/guest_gate.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/ops/guest_ops.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/ops/health.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/ops/iscsi_config.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/ops/network_mgmt.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/ops/planner.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/ops/vm_delete_gate.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/policy_environment.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/scanner/__init__.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/scanner/alarm_scanner.py +0 -0
- {vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/vmware_aiops/scanner/log_scanner.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.4.
|
|
5
|
+
"version": "2.4.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,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: vmware-aiops
|
|
3
|
-
Version: 2.4.
|
|
3
|
+
Version: 2.4.2
|
|
4
4
|
Summary: VMware vCenter/ESXi AI-powered VM lifecycle and deployment tool
|
|
5
5
|
Project-URL: Homepage, https://github.com/vmware-skills/VMware-AIops
|
|
6
6
|
Project-URL: Repository, https://github.com/vmware-skills/VMware-AIops
|
|
@@ -12,13 +12,13 @@ License-File: LICENSE
|
|
|
12
12
|
Requires-Python: >=3.10
|
|
13
13
|
Requires-Dist: apscheduler<4.0,>=3.10
|
|
14
14
|
Requires-Dist: httpx<1.0,>=0.27
|
|
15
|
-
Requires-Dist: mcp[cli]<2.0,>=1.
|
|
15
|
+
Requires-Dist: mcp[cli]<2.0,>=1.30
|
|
16
16
|
Requires-Dist: pyaml<27.0,>=24.0
|
|
17
17
|
Requires-Dist: python-dotenv<2.0,>=1.0
|
|
18
18
|
Requires-Dist: pyvmomi<10.0,>=8.0.3.0
|
|
19
19
|
Requires-Dist: rich<16.0,>=13.0
|
|
20
20
|
Requires-Dist: typer<1.0,>=0.12
|
|
21
|
-
Requires-Dist: vmware-monitor<3.0,>=2.
|
|
21
|
+
Requires-Dist: vmware-monitor<3.0,>=2.4.2
|
|
22
22
|
Requires-Dist: vmware-policy<2.0,>=1.23.1
|
|
23
23
|
Description-Content-Type: text/markdown
|
|
24
24
|
|
|
@@ -1,3 +1,82 @@
|
|
|
1
|
+
## v2.4.2 — a login refused for lack of a role says so; an unreadable host is not called unreachable (2026-10-19)
|
|
2
|
+
|
|
3
|
+
Needs vmware-monitor 2.4.2 (now the floor). Found on a vCenter 8.0.3 lab with two restricted SSO accounts
|
|
4
|
+
(follow-up to AIops#47).
|
|
5
|
+
|
|
6
|
+
* **A login vCenter refuses with `NoPermission` gets the role error, not the raw fault.** For an account with no
|
|
7
|
+
permission anywhere, vCenter refuses the login itself (`System.View` on the root folder), so the post-login
|
|
8
|
+
no-role check never ran and `doctor` and every tool showed the raw fault with a connectivity hint. It now raises
|
|
9
|
+
the no-role error: the "no vSphere role / Global Permissions" remedy only for a vCenter target and `System.View`;
|
|
10
|
+
an ESXi target or another privilege gets a neutral text naming the (sanitized) privilege. A wrong password
|
|
11
|
+
(`InvalidLogin`) is unchanged. Same text as vmware-monitor.
|
|
12
|
+
* **vmk tools and `cluster info` no longer call an unreadable host unreachable.** When the account cannot read a
|
|
13
|
+
host's connection state, `list_host_vmks` rows and the vmk write tools' refusal say the account cannot read the
|
|
14
|
+
host (`reachable: false` means "not read"), instead of advising a reconnect. `cluster info` colours the state
|
|
15
|
+
yellow, not red.
|
|
16
|
+
* **From vmware-monitor 2.4.2 (through `get_cluster_health_summary` / `get_cross_vcenter_attention`):** a host this
|
|
17
|
+
account cannot read is counted `hosts_unreadable` with a warning issue `host_unreadable`, not ranked as a
|
|
18
|
+
critical `host_down`.
|
|
19
|
+
|
|
20
|
+
## Unreleased — follow-ups to the 2.4.1 review
|
|
21
|
+
|
|
22
|
+
* **Previews show policy refusals.** `restore_vm_clean_slate`, `provision_vm_guest` and
|
|
23
|
+
`run_vm_guest_command_with_output` were refused by `rules.yaml` only at `confirm=True`; the preview now lists that
|
|
24
|
+
refusal in `blast_radius.blockers`, so the user sees it before agreeing.
|
|
25
|
+
* **A delete that powers the VM off first is checked as `power_off_vm` too:** the CLI `vm delete`, the TTL daemon,
|
|
26
|
+
and a plan rollback deleting the VM it created (checked when the VM is running or its state cannot be read). The
|
|
27
|
+
TTL daemon records one `delete_vm` row per change of outcome, as for a deny on `delete_vm`, and keeps the entry.
|
|
28
|
+
**Behaviour change for custom network roles:** a role that allows `set_vm_ttl` must also allow `power_off_vm`.
|
|
29
|
+
* **Image cache:** writes take a lock across processes (a CLI and an MCP scan at the same time lost one side's
|
|
30
|
+
rows); a scan keeps rows on datastores it failed to browse (its own or legacy ones) and drops this target's rows
|
|
31
|
+
on datastores that no longer exist. A cache that cannot be written no longer fails the scan: the images come back
|
|
32
|
+
with a `cache_note`, as in vmware-storage.
|
|
33
|
+
* **Each call is checked for what it will do, at the moment it does it.** `restore_vm_clean_slate` asks about
|
|
34
|
+
`power_off_vm` only when it actually powers the VM off; `provision_vm_guest` asks about the guest tools its step
|
|
35
|
+
types use (an exec-only provision is not refused by a deny on `upload_vm_guest_file`); a plan rollback asks about
|
|
36
|
+
the power-off after confirming it is the VM the plan created, just before powering it off.
|
|
37
|
+
* **`set_vm_ttl` is checked as the deletion it schedules:** the preview lists a deny on `delete_vm` (and on
|
|
38
|
+
`power_off_vm` for a running VM), and `confirm=True` refuses it, instead of scheduling a TTL the daemon can never
|
|
39
|
+
act on. The daemon still asks again at expiry.
|
|
40
|
+
* **`migrate_vm` previews run vCenter's own vMotion compatibility check** (`CheckMigrate_Task`, read-only): its
|
|
41
|
+
errors (CPU features, devices, networks, datastores) become blockers, with the CPU-feature remedy where it applies,
|
|
42
|
+
so a move vCenter would refuse is refused in the preview and before the CLI asks for confirmation; warnings are
|
|
43
|
+
shown and do not block. On a standalone ESXi host, or when the check cannot run, the preview says "not checked"
|
|
44
|
+
and does not block (vCenter checks again when the move starts). Plan migrate steps get the same check.
|
|
45
|
+
* The TTL store takes the same cross-process lock as the image cache (`set_vm_ttl` and the daemon run in separate
|
|
46
|
+
processes).
|
|
47
|
+
|
|
48
|
+
## v2.4.1 — fixes from the 2026-10-17 real-hardware read and write rounds
|
|
49
|
+
|
|
50
|
+
* **Tools that run other tools' operations obey `rules.yaml` for those tools**, on the MCP and CLI surfaces.
|
|
51
|
+
`restore_vm_clean_slate`, the batch tools, `provision_vm_guest`, and the deploy tools' `power_on` / snapshot /
|
|
52
|
+
CPU-memory follow-ups are refused, with a `denied` audit row, when the policy denies the tool that does that part
|
|
53
|
+
(a deny on `revert_vm_snapshot` used to leave clean slate working; a deny on `power_on_vm` did not stop
|
|
54
|
+
`batch_clone_vms(power_on=True)`). A follow-up is checked only when the call asks for it. `run_vm_guest_command_with_output` is
|
|
55
|
+
checked as `run_vm_guest_command` (and `download_vm_guest_file`) too.
|
|
56
|
+
* **Behaviour change for custom network roles:** a role check reads no arguments, so a role that allows
|
|
57
|
+
`deploy_vm_from_template`, `deploy_vm_from_ova` or `deploy_linked_clone` must now also allow `power_on_vm`,
|
|
58
|
+
`create_vm_snapshot` and `reconfigure_vm` (and one allowing `run_vm_guest_command_with_output` must allow
|
|
59
|
+
`run_vm_guest_command` and `download_vm_guest_file`), or every call is refused. The built-in `reader` and `admin`
|
|
60
|
+
roles are unaffected.
|
|
61
|
+
* **Plan steps obey `rules.yaml`.** Each step of `apply_vm_plan` / `rollback_vm_plan` is authorized by vmware-policy
|
|
62
|
+
under the tool that does the same thing (a `delete_vm` step as `delete_vm`, `power_off` as `power_off_vm`, the
|
|
63
|
+
rollback that deletes a VM the plan created as `delete_vm`), at that tool's risk level — in the preview and again
|
|
64
|
+
just before the step runs, with a `denied` audit row. A deny on `delete_vm` used to stop the tool but not a plan step.
|
|
65
|
+
* **Failures are failures.** Deploy, clone, linked clone, template conversion, ISO attach, `create_vm`, `migrate_vm`,
|
|
66
|
+
`reconfigure_vm` and `configure_cluster` with nothing to change returned their refusals as status strings, which
|
|
67
|
+
the MCP frame and the audit recorded as success; they now raise. A plan that stops part-way, a rollback that leaves
|
|
68
|
+
a step not undone, and a plan that fails validation now carry the family `error` key.
|
|
69
|
+
* **`deploy_vm_from_template` works on a vCenter with standalone hosts**: the clone gets the template's resource
|
|
70
|
+
pool (it failed with `spec.location.pool`), and the tool now takes `target_host`.
|
|
71
|
+
* **`scan_all_datastore_images` keeps each target's images**: the cache was one list replaced by every scan, so
|
|
72
|
+
scanning `datastore1` on an ESXi host erased what a vCenter scan had found. Entries now carry `target`; a corrupt
|
|
73
|
+
cache file is rebuilt by the next scan and explained by the error instead of a bare JSON error.
|
|
74
|
+
* **`browse_datastore` sees every file** (`.ova`, `.ovf`, `.vmx`, `.log` were filtered out), `ds_path` is well formed
|
|
75
|
+
for ESXi and vCenter folder paths, and a leading `/` is refused with the corrected path, as vmware-storage does.
|
|
76
|
+
* Error text names MCP tools and parameters (CLI form in brackets); doctor is suggested only for connectivity errors;
|
|
77
|
+
a CPU-feature vMotion refusal leads with the remedy; `get_vm_task_status` no longer says an unknown id means the
|
|
78
|
+
task finished; powering off a VM that is already off is a plain `noop`.
|
|
79
|
+
|
|
1
80
|
## v2.4.0 — roles for network callers (HLD §18)
|
|
2
81
|
|
|
3
82
|
* Declares the role metadata vmware-policy ≥ 1.24.0 needs to apply **roles** to this server over HTTPS
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vmware-aiops"
|
|
7
|
-
version = "2.4.
|
|
7
|
+
version = "2.4.2"
|
|
8
8
|
description = "VMware vCenter/ESXi AI-powered VM lifecycle and deployment tool"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -18,12 +18,12 @@ dependencies = [
|
|
|
18
18
|
"apscheduler>=3.10,<4.0",
|
|
19
19
|
"httpx>=0.27,<1.0",
|
|
20
20
|
"python-dotenv>=1.0,<2.0",
|
|
21
|
-
"mcp[cli]>=1.
|
|
21
|
+
"mcp[cli]>=1.30,<2.0",
|
|
22
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.4.2,<3.0", # unreadable host != down, login refused -> role error
|
|
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.4.
|
|
10
|
+
"version": "2.4.2",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "pypi",
|
|
14
14
|
"identifier": "vmware-aiops",
|
|
15
|
-
"version": "2.4.
|
|
15
|
+
"version": "2.4.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.4.
|
|
66
|
+
uv tool install vmware-aiops==2.4.2
|
|
67
67
|
vmware-aiops doctor
|
|
68
68
|
vmware-aiops hub status # see which family members are installed
|
|
69
69
|
```
|
|
@@ -303,7 +303,7 @@ The account has no vSphere role. Assign one (Read-Only or higher) on the vCenter
|
|
|
303
303
|
## Setup
|
|
304
304
|
|
|
305
305
|
```bash
|
|
306
|
-
uv tool install vmware-aiops==2.4.
|
|
306
|
+
uv tool install vmware-aiops==2.4.2
|
|
307
307
|
mkdir -p ~/.vmware-aiops
|
|
308
308
|
vmware-aiops init # generates config.yaml and .env templates
|
|
309
309
|
chmod 600 ~/.vmware-aiops/.env
|
|
@@ -6,13 +6,13 @@ All install methods fetch from the same source: [github.com/vmware-skills/VMware
|
|
|
6
6
|
|
|
7
7
|
```bash
|
|
8
8
|
# Via PyPI (recommended for version pinning)
|
|
9
|
-
uv tool install vmware-aiops==2.4.
|
|
9
|
+
uv tool install vmware-aiops==2.4.2
|
|
10
10
|
|
|
11
11
|
# Via Skills.sh (fetches from GitHub)
|
|
12
|
-
npx skills add vmware-skills/VMware-AIops#v2.4.
|
|
12
|
+
npx skills add vmware-skills/VMware-AIops#v2.4.2
|
|
13
13
|
|
|
14
14
|
# Via ClawHub (fetches from ClawHub registry snapshot of GitHub)
|
|
15
|
-
clawhub install @zw008/vmware-aiops --version 2.4.
|
|
15
|
+
clawhub install @zw008/vmware-aiops --version 2.4.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.4.
|
|
38
|
+
uv tool install vmware-aiops==2.4.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.4.
|
|
76
|
+
git clone --branch v2.4.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
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""browse_datastore must return a ds_path that deploy/attach tools can use.
|
|
2
|
+
|
|
3
|
+
Regression source: real ESXi 8 host (home-esxi, 2026-10-17). The browse task
|
|
4
|
+
reports each folder's ``folderPath`` with no trailing separator —
|
|
5
|
+
``"[datastore1]"`` for the root, ``"[datastore1] backup"`` for a folder —
|
|
6
|
+
and the old code glued the file name straight on, producing
|
|
7
|
+
``"[datastore1] backupVMware-VCSA.iso"`` and ``"[datastore1]Hermers - TT"``.
|
|
8
|
+
Agents pass that string into deploy_vm_from_ova / attach_iso_to_vm, so a
|
|
9
|
+
malformed path breaks the very flow the browse exists for. The earlier tests
|
|
10
|
+
mocked ``"[ds1] images/"`` (trailing slash), the one shape that hid the bug.
|
|
11
|
+
"""
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from types import SimpleNamespace
|
|
15
|
+
|
|
16
|
+
import pytest
|
|
17
|
+
from pyVmomi import vim
|
|
18
|
+
|
|
19
|
+
from vmware_aiops.ops import datastore_browser as dsb
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@pytest.mark.unit
|
|
23
|
+
@pytest.mark.parametrize(
|
|
24
|
+
("folder", "name", "expected"),
|
|
25
|
+
[
|
|
26
|
+
# Root, as ESXi reports it: no space after the bracket.
|
|
27
|
+
("[datastore1]", "Hermers - TT", "[datastore1] Hermers - TT"),
|
|
28
|
+
("[datastore1]", "x.iso", "[datastore1] x.iso"),
|
|
29
|
+
# Root, as vCenter reports it: trailing space.
|
|
30
|
+
("[datastore1] ", "x.iso", "[datastore1] x.iso"),
|
|
31
|
+
# Folder without a trailing slash (ESXi).
|
|
32
|
+
("[datastore1] backup", "x.iso", "[datastore1] backup/x.iso"),
|
|
33
|
+
(
|
|
34
|
+
"[datastore1] Hermers - TT",
|
|
35
|
+
"Hermers - TT.vmdk",
|
|
36
|
+
"[datastore1] Hermers - TT/Hermers - TT.vmdk",
|
|
37
|
+
),
|
|
38
|
+
("[datastore1] a/b", "x.iso", "[datastore1] a/b/x.iso"),
|
|
39
|
+
# Folder with a trailing slash (vCenter) — must not double it.
|
|
40
|
+
("[datastore1] backup/", "x.iso", "[datastore1] backup/x.iso"),
|
|
41
|
+
],
|
|
42
|
+
)
|
|
43
|
+
def test_join_ds_path(folder, name, expected):
|
|
44
|
+
assert dsb._join_ds_path(folder, name) == expected
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@pytest.mark.unit
|
|
48
|
+
def test_browse_root_returns_well_formed_paths_for_every_folder(monkeypatch):
|
|
49
|
+
"""Root browse is recursive: subfolder rows come back too, each well formed."""
|
|
50
|
+
def _file(path):
|
|
51
|
+
return SimpleNamespace(path=path, fileSize=1024 * 1024, modification="2026-10-17")
|
|
52
|
+
|
|
53
|
+
results = [
|
|
54
|
+
SimpleNamespace(folderPath="[datastore1]", file=[_file("backup")]),
|
|
55
|
+
SimpleNamespace(folderPath="[datastore1] backup", file=[_file("x.iso")]),
|
|
56
|
+
]
|
|
57
|
+
seen = {}
|
|
58
|
+
|
|
59
|
+
def fake_search(datastorePath, searchSpec): # noqa: N803 — pyVmomi API names
|
|
60
|
+
seen["path"] = datastorePath
|
|
61
|
+
return SimpleNamespace(
|
|
62
|
+
info=SimpleNamespace(state=vim.TaskInfo.State.success, result=results, error=None)
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
monkeypatch.setattr(
|
|
66
|
+
dsb,
|
|
67
|
+
"find_datastore_by_name",
|
|
68
|
+
lambda si, name: SimpleNamespace(
|
|
69
|
+
browser=SimpleNamespace(SearchDatastoreSubFolders_Task=fake_search)
|
|
70
|
+
),
|
|
71
|
+
)
|
|
72
|
+
|
|
73
|
+
rows = dsb.browse_datastore(object(), "datastore1")["items"]
|
|
74
|
+
|
|
75
|
+
assert seen["path"] == "[datastore1]"
|
|
76
|
+
assert {r["name"]: r["ds_path"] for r in rows} == {
|
|
77
|
+
"backup": "[datastore1] backup",
|
|
78
|
+
"x.iso": "[datastore1] backup/x.iso",
|
|
79
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
"""browse_datastore must see every file, and treat a leading '/' one way.
|
|
2
|
+
|
|
3
|
+
Real hardware, 2026-10-17: vmware-aiops' search spec listed only IsoImageQuery,
|
|
4
|
+
VmDiskQuery and FolderQuery under a comment saying "include all file types".
|
|
5
|
+
A DatastoreBrowser query is a filter, not a hint -- with those three, .ova /
|
|
6
|
+
.ovf / .vmx / .log files are never returned. On the lab vCenter it found 0 of
|
|
7
|
+
the 2 OVAs on 'datastore1 (1)' that vmware-storage (which also passes the
|
|
8
|
+
generic Query) found, so scan_all_datastore_images could not do the one job its
|
|
9
|
+
description gives it: finding an OVA for deploy_vm_from_ova.
|
|
10
|
+
|
|
11
|
+
The twin also drifted on a leading slash: vmware-aiops passed '/vcsa8' through,
|
|
12
|
+
vmware-storage refused it as "absolute" (its 1.5.35 hardening). Both now refuse
|
|
13
|
+
it, and the error names the corrected path.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from types import SimpleNamespace
|
|
18
|
+
|
|
19
|
+
import pytest
|
|
20
|
+
from pyVmomi import vim
|
|
21
|
+
|
|
22
|
+
from vmware_aiops.ops import datastore_browser as dsb
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def _browse(monkeypatch, path=""):
|
|
26
|
+
seen: dict = {}
|
|
27
|
+
|
|
28
|
+
def fake_search(datastorePath, searchSpec): # noqa: N803 -- pyVmomi API names
|
|
29
|
+
seen["path"] = datastorePath
|
|
30
|
+
seen["spec"] = searchSpec
|
|
31
|
+
return SimpleNamespace(
|
|
32
|
+
info=SimpleNamespace(state=vim.TaskInfo.State.success, result=[], error=None)
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
monkeypatch.setattr(
|
|
36
|
+
dsb,
|
|
37
|
+
"find_datastore_by_name",
|
|
38
|
+
lambda si, name: SimpleNamespace(
|
|
39
|
+
browser=SimpleNamespace(SearchDatastoreSubFolders_Task=fake_search)
|
|
40
|
+
),
|
|
41
|
+
)
|
|
42
|
+
dsb.browse_datastore(object(), "datastore1", path=path, pattern="*.ova")
|
|
43
|
+
return seen
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@pytest.mark.unit
|
|
47
|
+
def test_search_spec_includes_the_generic_file_query(monkeypatch) -> None:
|
|
48
|
+
spec = _browse(monkeypatch)["spec"]
|
|
49
|
+
# Exact type: every specific query subclasses Query, so isinstance would pass
|
|
50
|
+
# for the three that hid the OVAs.
|
|
51
|
+
assert any(type(q) is vim.host.DatastoreBrowser.Query for q in spec.query)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@pytest.mark.unit
|
|
55
|
+
def test_relative_path_is_joined_under_the_datastore(monkeypatch) -> None:
|
|
56
|
+
assert _browse(monkeypatch, "vcsa8")["path"] == "[datastore1] vcsa8"
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pytest.mark.unit
|
|
60
|
+
@pytest.mark.parametrize("path", ["/vcsa8", "//vcsa8"])
|
|
61
|
+
def test_leading_slash_is_refused_with_the_fix(monkeypatch, path) -> None:
|
|
62
|
+
with pytest.raises(ValueError, match="path='vcsa8'"):
|
|
63
|
+
_browse(monkeypatch, path)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
@pytest.mark.unit
|
|
67
|
+
@pytest.mark.parametrize("path", ["../other", "a/../../b", "x\x00y", "\\\\server\\share", "/"])
|
|
68
|
+
def test_traversal_and_odd_paths_are_refused(monkeypatch, path) -> None:
|
|
69
|
+
with pytest.raises(ValueError, match="datastore-relative"):
|
|
70
|
+
_browse(monkeypatch, path)
|
|
@@ -51,7 +51,9 @@ def _ops_refs(tree: ast.AST) -> tuple[dict[str, str], set[str]]:
|
|
|
51
51
|
for n in ast.walk(tree):
|
|
52
52
|
if isinstance(n, ast.ImportFrom) and n.module:
|
|
53
53
|
parts = n.module.split(".")
|
|
54
|
-
|
|
54
|
+
# step_policy authorizes the operation; it is not the operation, and
|
|
55
|
+
# every composite command calls it (2026-10-17).
|
|
56
|
+
if "ops" in parts and parts[-1] != "step_policy":
|
|
55
57
|
if parts[-1] == "ops":
|
|
56
58
|
mods.update(a.asname or a.name for a in n.names)
|
|
57
59
|
else:
|
|
@@ -127,7 +127,7 @@ def test_ttl_entry_kept_on_transient_failure(monkeypatch: pytest.MonkeyPatch) ->
|
|
|
127
127
|
monkeypatch.setattr(scheduler, "remove_entry", removed.append)
|
|
128
128
|
monkeypatch.setattr(
|
|
129
129
|
scheduler, "delete_vm",
|
|
130
|
-
lambda si, name: (_ for _ in ()).throw(RuntimeError("vCenter task timeout")),
|
|
130
|
+
lambda si, name, **k: (_ for _ in ()).throw(RuntimeError("vCenter task timeout")),
|
|
131
131
|
)
|
|
132
132
|
|
|
133
133
|
scheduler._run_ttl_check(MagicMock())
|
|
@@ -143,7 +143,7 @@ def test_ttl_entry_removed_when_vm_already_gone(monkeypatch: pytest.MonkeyPatch)
|
|
|
143
143
|
monkeypatch.setattr(scheduler, "remove_entry", removed.append)
|
|
144
144
|
monkeypatch.setattr(
|
|
145
145
|
scheduler, "delete_vm",
|
|
146
|
-
lambda si, name: (_ for _ in ()).throw(VMNotFoundError("VM 'ttl-vm' not found")),
|
|
146
|
+
lambda si, name, **k: (_ for _ in ()).throw(VMNotFoundError("VM 'ttl-vm' not found")),
|
|
147
147
|
)
|
|
148
148
|
|
|
149
149
|
scheduler._run_ttl_check(MagicMock())
|
|
@@ -156,7 +156,7 @@ def test_ttl_entry_removed_on_success(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
|
156
156
|
removed: list[str] = []
|
|
157
157
|
monkeypatch.setattr(scheduler, "get_expired_entries", lambda: [_ttl_entry()])
|
|
158
158
|
monkeypatch.setattr(scheduler, "remove_entry", removed.append)
|
|
159
|
-
monkeypatch.setattr(scheduler, "delete_vm", lambda si, name: f"VM '{name}' deleted")
|
|
159
|
+
monkeypatch.setattr(scheduler, "delete_vm", lambda si, name, **k: f"VM '{name}' deleted")
|
|
160
160
|
|
|
161
161
|
scheduler._run_ttl_check(MagicMock())
|
|
162
162
|
assert removed == ["ttl-vm"]
|
{vmware_aiops-2.4.0 → vmware_aiops-2.4.2}/tests/eval/regression/test_daemon_calls_are_audited.py
RENAMED
|
@@ -71,7 +71,7 @@ def ttl(monkeypatch):
|
|
|
71
71
|
|
|
72
72
|
def test_a_ttl_delete_writes_one_vm_delete_row(ttl, monkeypatch):
|
|
73
73
|
scheduler, removed = ttl
|
|
74
|
-
monkeypatch.setattr(scheduler, "delete_vm", lambda si, name: f"VM '{name}' deleted")
|
|
74
|
+
monkeypatch.setattr(scheduler, "delete_vm", lambda si, name, **k: f"VM '{name}' deleted")
|
|
75
75
|
|
|
76
76
|
scheduler._run_ttl_check(MagicMock())
|
|
77
77
|
|
|
@@ -88,7 +88,7 @@ def test_a_ttl_delete_writes_one_vm_delete_row(ttl, monkeypatch):
|
|
|
88
88
|
def test_a_failed_ttl_delete_is_recorded_as_error_and_the_entry_kept(ttl, monkeypatch):
|
|
89
89
|
scheduler, removed = ttl
|
|
90
90
|
|
|
91
|
-
def boom(si, name):
|
|
91
|
+
def boom(si, name, **k):
|
|
92
92
|
raise RuntimeError("vCenter task timeout")
|
|
93
93
|
|
|
94
94
|
monkeypatch.setattr(scheduler, "delete_vm", boom)
|
|
@@ -106,7 +106,7 @@ def test_a_ttl_vm_already_gone_is_recorded_and_the_stale_entry_dropped(ttl, monk
|
|
|
106
106
|
|
|
107
107
|
scheduler, removed = ttl
|
|
108
108
|
|
|
109
|
-
def gone(si, name):
|
|
109
|
+
def gone(si, name, **k):
|
|
110
110
|
raise VMNotFoundError(f"VM '{name}' not found")
|
|
111
111
|
|
|
112
112
|
monkeypatch.setattr(scheduler, "delete_vm", gone)
|
|
@@ -122,7 +122,7 @@ def test_a_ttl_vm_already_gone_is_recorded_and_the_stale_entry_dropped(ttl, monk
|
|
|
122
122
|
def test_a_ttl_delete_refused_by_policy_never_runs(ttl, monkeypatch):
|
|
123
123
|
scheduler, removed = ttl
|
|
124
124
|
deleted: list[str] = []
|
|
125
|
-
monkeypatch.setattr(scheduler, "delete_vm", lambda si, name: deleted.append(name))
|
|
125
|
+
monkeypatch.setattr(scheduler, "delete_vm", lambda si, name, **k: deleted.append(name))
|
|
126
126
|
|
|
127
127
|
def deny(skill, tool, params=None, *, risk_level="low", target=""):
|
|
128
128
|
raise PolicyDenied(
|
|
@@ -144,7 +144,7 @@ def test_a_refused_ttl_entry_writes_one_row_per_outcome_not_one_per_minute(ttl,
|
|
|
144
144
|
failing entry wrote a delete_vm row on every run — 1,440 a day per entry, on
|
|
145
145
|
this lab whose rules.yaml denies delete_vm — burying real calls."""
|
|
146
146
|
scheduler, removed = ttl
|
|
147
|
-
monkeypatch.setattr(scheduler, "delete_vm", lambda si, name: None)
|
|
147
|
+
monkeypatch.setattr(scheduler, "delete_vm", lambda si, name, **k: None)
|
|
148
148
|
|
|
149
149
|
def deny(skill, tool, params=None, *, risk_level="low", target=""):
|
|
150
150
|
raise PolicyDenied(
|
|
@@ -165,7 +165,7 @@ def test_a_failing_ttl_delete_writes_again_only_when_its_outcome_changes(ttl, mo
|
|
|
165
165
|
scheduler, removed = ttl
|
|
166
166
|
attempts: list[str] = []
|
|
167
167
|
|
|
168
|
-
def flaky(si, name):
|
|
168
|
+
def flaky(si, name, **k):
|
|
169
169
|
attempts.append(name)
|
|
170
170
|
if len(attempts) <= 3:
|
|
171
171
|
raise RuntimeError("vCenter task timeout")
|
|
@@ -315,7 +315,7 @@ def test_a_ttl_vm_whose_name_is_ambiguous_is_not_deleted_and_the_entry_dropped(t
|
|
|
315
315
|
|
|
316
316
|
scheduler, removed = ttl
|
|
317
317
|
|
|
318
|
-
def ambiguous(si, name):
|
|
318
|
+
def ambiguous(si, name, **k):
|
|
319
319
|
raise AmbiguousVMError(f"2 VMs are named '{name}'.")
|
|
320
320
|
|
|
321
321
|
monkeypatch.setattr(scheduler, "delete_vm", ambiguous)
|
|
@@ -332,7 +332,7 @@ def test_daemon_rows_say_daemon_not_mcp(ttl, monkeypatch):
|
|
|
332
332
|
"""HLD §16 D-14: a TTL delete made by the daemon is not an MCP call. Without
|
|
333
333
|
an explicit surface the row read `mcp-stdio` — non-empty, and wrong."""
|
|
334
334
|
scheduler, _removed = ttl
|
|
335
|
-
monkeypatch.setattr(scheduler, "delete_vm", lambda si, name: f"VM '{name}' deleted")
|
|
335
|
+
monkeypatch.setattr(scheduler, "delete_vm", lambda si, name, **k: f"VM '{name}' deleted")
|
|
336
336
|
|
|
337
337
|
scheduler._run_ttl_check(MagicMock())
|
|
338
338
|
|