vmware-debug 1.11.4__tar.gz → 1.12.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (94) hide show
  1. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/.claude-plugin/plugin.json +1 -1
  2. vmware_debug-1.12.1/.clawhubignore +19 -0
  3. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/.mcp.json +1 -1
  4. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/PKG-INFO +2 -2
  5. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/RELEASE_NOTES.md +44 -0
  6. vmware_debug-1.12.1/openclaw.plugin.json +24 -0
  7. vmware_debug-1.12.1/package.json +20 -0
  8. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/pyproject.toml +2 -2
  9. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/server.json +2 -2
  10. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/skills/vmware-debug/SKILL.md +1 -1
  11. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/skills/vmware-debug/references/capabilities.md +7 -1
  12. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/skills/vmware-debug/references/routing.md +5 -2
  13. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/skills/vmware-debug/references/setup-guide.md +2 -2
  14. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/eval/regression/test_classification_against_vsphere_vocabulary.py +7 -0
  15. vmware_debug-1.12.1/tests/eval/regression/test_real_alert_titles_classify.py +153 -0
  16. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/uv.lock +5 -5
  17. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/vmware_debug/__init__.py +1 -1
  18. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/vmware_debug/cli.py +5 -0
  19. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/vmware_debug/mcp_server/server.py +4 -2
  20. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/vmware_debug/ops/timeline.py +31 -1
  21. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/vmware_debug/rules/evidence_sources.yaml +62 -2
  22. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/.gitignore +0 -0
  23. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/README-CN.md +0 -0
  24. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/README.md +0 -0
  25. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/SECURITY.md +0 -0
  26. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/docs/plans/2026-08-28-investigation-layer-design.md +0 -0
  27. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/skills/vmware-debug/references/agent-guardrails.md +0 -0
  28. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/skills/vmware-debug/references/cli-reference.md +0 -0
  29. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/skills/vmware-debug/references/event-envelope.md +0 -0
  30. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/__init__.py +0 -0
  31. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/conftest.py +0 -0
  32. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/eval/__init__.py +0 -0
  33. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/eval/capability/__init__.py +0 -0
  34. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/eval/capability/_family.py +0 -0
  35. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/eval/capability/_scores.json +0 -0
  36. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/eval/capability/_scoring.py +0 -0
  37. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/eval/capability/_skill.py +0 -0
  38. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/eval/capability/conftest.py +0 -0
  39. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/eval/capability/test_entity_reachability.py +0 -0
  40. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/eval/capability/test_error_actionability.py +0 -0
  41. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/eval/capability/test_tool_description_quality.py +0 -0
  42. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/eval/capability/test_tool_manifest_budget.py +0 -0
  43. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/eval/regression/__init__.py +0 -0
  44. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/eval/regression/_vsphere_event_vocabulary.py +0 -0
  45. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/eval/regression/test_audit_isolation.py +0 -0
  46. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/eval/regression/test_capability_grader.py +0 -0
  47. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/eval/regression/test_case_id_non_ascii.py +0 -0
  48. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/eval/regression/test_case_plan_max_steps.py +0 -0
  49. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/eval/regression/test_debug_regressions.py +0 -0
  50. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/eval/regression/test_environment_resolver_isolation.py +0 -0
  51. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/eval/regression/test_every_tool_parameter_is_documented.py +0 -0
  52. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/eval/regression/test_evidence_payload_contract.py +0 -0
  53. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/eval/regression/test_host_lifecycle_classification.py +0 -0
  54. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/eval/regression/test_investigation_metrics.py +0 -0
  55. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/eval/regression/test_result_envelope.py +0 -0
  56. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/eval/regression/test_skill_name_aliases.py +0 -0
  57. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/eval/regression/test_spike_resolution.py +0 -0
  58. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/eval/regression/test_tool_annotations.py +0 -0
  59. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/test_case_conclusion.py +0 -0
  60. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/test_case_evidence.py +0 -0
  61. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/test_case_grading.py +0 -0
  62. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/test_case_hypotheses.py +0 -0
  63. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/test_case_ids.py +0 -0
  64. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/test_case_ledger_concurrency.py +0 -0
  65. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/test_case_plan.py +0 -0
  66. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/test_case_readiness.py +0 -0
  67. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/test_case_store.py +0 -0
  68. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/test_case_timeline_close.py +0 -0
  69. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/test_case_tools.py +0 -0
  70. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/test_knowledge.py +0 -0
  71. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/test_safe_error_passthrough.py +0 -0
  72. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/tests/test_timeline.py +0 -0
  73. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/vmware_debug/envelope.py +0 -0
  74. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/vmware_debug/mcp/__init__.py +0 -0
  75. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/vmware_debug/mcp/tools.py +0 -0
  76. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/vmware_debug/mcp_server/__init__.py +0 -0
  77. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/vmware_debug/ops/__init__.py +0 -0
  78. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/vmware_debug/ops/cases/__init__.py +0 -0
  79. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/vmware_debug/ops/cases/api.py +0 -0
  80. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/vmware_debug/ops/cases/conclusion.py +0 -0
  81. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/vmware_debug/ops/cases/evidence.py +0 -0
  82. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/vmware_debug/ops/cases/grading.py +0 -0
  83. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/vmware_debug/ops/cases/hypotheses.py +0 -0
  84. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/vmware_debug/ops/cases/ids.py +0 -0
  85. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/vmware_debug/ops/cases/knowledge.py +0 -0
  86. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/vmware_debug/ops/cases/model.py +0 -0
  87. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/vmware_debug/ops/cases/payloads.py +0 -0
  88. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/vmware_debug/ops/cases/plan.py +0 -0
  89. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/vmware_debug/ops/cases/readiness.py +0 -0
  90. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/vmware_debug/ops/cases/sources.py +0 -0
  91. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/vmware_debug/ops/cases/store.py +0 -0
  92. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/vmware_debug/ops/cases/timeline.py +0 -0
  93. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/vmware_debug/ops/skill_names.py +0 -0
  94. {vmware_debug-1.11.4 → vmware_debug-1.12.1}/vmware_debug/rules/grading_rules.yaml +0 -0
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://anthropic.com/claude-code/plugin.schema.json",
3
3
  "name": "vmware-debug",
4
4
  "displayName": "VMware Debug",
5
- "version": "1.11.4",
5
+ "version": "1.12.1",
6
6
  "description": "VMware incident timeline, root-cause routing, local case ledger; no vSphere access. 14 MCP tools.",
7
7
  "author": {
8
8
  "name": "Wei Zhou",
@@ -0,0 +1,19 @@
1
+ # What `clawhub package publish` uploads from this folder.
2
+ #
3
+ # The CLI does not read .gitignore — only this file. Without it, publishing from
4
+ # the repo root uploads everything on disk: .venv/, dist/, and any untracked
5
+ # credential file (mcp-publisher leaves .mcpregistry_*_token here). So this is an
6
+ # allowlist: ignore every top-level entry, then re-include only the plugin.
7
+ /*
8
+ !/openclaw.plugin.json
9
+ !/package.json
10
+ !/.claude-plugin/
11
+ !/.mcp.json
12
+ !/skills/
13
+ !/README.md
14
+ !/LICENSE
15
+
16
+ # Never ship these even inside an allowed directory.
17
+ .DS_Store
18
+ __pycache__/
19
+ *.pyc
@@ -5,7 +5,7 @@
5
5
  "command": "uvx",
6
6
  "args": [
7
7
  "--from",
8
- "vmware-debug==1.11.4",
8
+ "vmware-debug==1.12.1",
9
9
  "vmware-debug-mcp"
10
10
  ]
11
11
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: vmware-debug
3
- Version: 1.11.4
3
+ Version: 1.12.1
4
4
  Summary: VMware diagnostic brain — offline incident triage, log/event correlation, and root-cause routing across the VMware skill family
5
5
  Project-URL: Homepage, https://github.com/vmware-skills/VMware-Debug
6
6
  Project-URL: Repository, https://github.com/vmware-skills/VMware-Debug
@@ -18,7 +18,7 @@ Requires-Dist: mcp[cli]<2.0,>=1.10
18
18
  Requires-Dist: pyyaml<7.0,>=6.0
19
19
  Requires-Dist: rich<15.0,>=13.0
20
20
  Requires-Dist: typer<1.0,>=0.12
21
- Requires-Dist: vmware-policy<2.0,>=1.13.1
21
+ Requires-Dist: vmware-policy<2.0,>=1.15.0
22
22
  Description-Content-Type: text/markdown
23
23
 
24
24
  <!-- mcp-name: io.github.vmware-skills/vmware-debug -->
@@ -1,3 +1,47 @@
1
+ ## v1.12.1 — every CLI command declares what it reaches
2
+
3
+ Across the family, CLI reads never wrote `~/.vmware/audit.db`; they now do (`@audited`, under the
4
+ MCP tool's name). To keep that from regressing, every CLI command must declare `@guarded` (a write),
5
+ `@audited` (reaches a remote system) or `@cli_local` (reaches nothing remote), and a family gate fails
6
+ any command that declares none.
7
+
8
+ This skill's 4 CLI commands reach nothing remote; each is marked `@cli_local` with its reason.
9
+ Their behaviour does not change.
10
+
11
+ Requires `vmware-policy>=1.15.0`.
12
+
13
+ ## v1.12.0 — real alert titles get a category
14
+
15
+ Eight alerts read off a lab vCenter 8.0.3 and Aria 8.18.7 were passed through `triage`, and six
16
+ came back `uncategorized`; the top hypothesis was "uncategorized" itself. Five of them were not odd
17
+ spellings of subsystems the taxonomy already had — they were subsystems it had no category for:
18
+
19
+ * **`hardware`** (tpm, attestation, ipmi, sensor, bmc) — "Host TPM attestation alarm". The catalogue
20
+ already routed `hardware`, but the classifier could never emit it. Its plan now asks vmware-monitor
21
+ for the host sensors and the CIM Server state that supplies them (new `host_hardware` class).
22
+ * **`licensing`** (license) — "License will soon expire", "Expired vCenter Server license". Routes to
23
+ vmware-monitor `license_status` (which asset holds which key). An Aria alert about a vCenter license
24
+ is built from vCenter's data, so it is not counted as a second source: licensing stays at Candidate.
25
+ * **`data_collection`** (adapter instance, not receiving data, collector group, cloud proxy) — "Objects
26
+ are not receiving data from adapter instance". Routes to Aria's adapters, collector groups, node
27
+ health and resources, plus vmware-monitor for whether the source vCenter is reachable. Not a bare
28
+ "collector": vCenter's own services are named that way ("ESXi Dump Collector service") and stay
29
+ under `platform`.
30
+ * **`auth`** now reads "password" — "Root user password expired".
31
+
32
+ Seven of the eight now classify. "Group population health is degraded" stays uncategorized on purpose:
33
+ it is a roll-up of member health and names no subsystem. Against all 427 vSphere event types the
34
+ taxonomy now reads 140 (was 124): 14 licensing and 2 password events, and no type changed its first
35
+ category. The keyword table is at 100, the cap its padding test allows — the next category will have
36
+ to argue for moving it.
37
+
38
+ **`case_readiness` recognises vmware-aiops.** It used to land in `unrecognised_skills` although routing
39
+ sent power_lifecycle cases to it. A new `vm_operations` class lists `vm_list_snapshots` only. AIops's
40
+ cluster summary and investigation bundles call vmware-monitor's code and would have counted monitor's
41
+ data as a second source; `vm_task_status` needs a task id a plan does not have, and called with a VM
42
+ name it answers "gone" with no error. With monitor and aiops, power_lifecycle now reaches Probable;
43
+ data_collection reaches Probable with monitor and aria.
44
+
1
45
  ## v1.11.4 — concurrent writers stop losing case entries
2
46
 
3
47
  Two agents on one case — a supported setup, since `OPS_HOME` can point at a shared folder — lost
@@ -0,0 +1,24 @@
1
+ {
2
+ "id": "vmware-debug",
3
+ "name": "VMware Debug",
4
+ "description": "VMware incident timeline, root-cause routing, local case ledger; no vSphere access. 14 MCP tools.",
5
+ "skills": [
6
+ "./skills"
7
+ ],
8
+ "mcpServers": {
9
+ "vmware-debug": {
10
+ "transport": "stdio",
11
+ "command": "uvx",
12
+ "args": [
13
+ "--from",
14
+ "vmware-debug==1.12.1",
15
+ "vmware-debug-mcp"
16
+ ]
17
+ }
18
+ },
19
+ "configSchema": {
20
+ "type": "object",
21
+ "additionalProperties": false,
22
+ "properties": {}
23
+ }
24
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@zw008/vmware-debug",
3
+ "version": "1.12.1",
4
+ "description": "VMware incident timeline, root-cause routing, local case ledger; no vSphere access. 14 MCP tools.",
5
+ "private": true,
6
+ "license": "MIT",
7
+ "homepage": "https://github.com/vmware-skills/VMware-Debug",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "https://github.com/vmware-skills/VMware-Debug.git"
11
+ },
12
+ "openclaw": {
13
+ "compat": {
14
+ "pluginApi": ">=2026.3.24-beta.2"
15
+ },
16
+ "build": {
17
+ "openclawVersion": "2026.3.24-beta.2"
18
+ }
19
+ }
20
+ }
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "vmware-debug"
7
- version = "1.11.4"
7
+ version = "1.12.1"
8
8
  description = "VMware diagnostic brain — offline incident triage, log/event correlation, and root-cause routing across the VMware skill family"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -21,7 +21,7 @@ dependencies = [
21
21
  "typer>=0.12,<1.0",
22
22
  "rich>=13.0,<15.0",
23
23
  "mcp[cli]>=1.10,<2.0",
24
- "vmware-policy>=1.13.1,<2.0",
24
+ "vmware-policy>=1.15.0,<2.0",
25
25
  # Imported directly by ops/cases/grading.py for the rules file. It has
26
26
  # always arrived transitively via vmware-policy; a package should
27
27
  # declare what it imports rather than rely on a dependency's dependency.
@@ -7,12 +7,12 @@
7
7
  "url": "https://github.com/vmware-skills/VMware-Debug",
8
8
  "source": "github"
9
9
  },
10
- "version": "1.11.4",
10
+ "version": "1.12.1",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "pypi",
14
14
  "identifier": "vmware-debug",
15
- "version": "1.11.4",
15
+ "version": "1.12.1",
16
16
  "runtimeHint": "uvx",
17
17
  "packageArguments": [
18
18
  {
@@ -51,7 +51,7 @@ write tools touch only the local case ledger.
51
51
  ## Quick Install
52
52
 
53
53
  ```bash
54
- uv tool install vmware-debug==1.11.4
54
+ uv tool install vmware-debug==1.12.1
55
55
  vmware-debug categories # see what it can diagnose
56
56
  ```
57
57
 
@@ -31,9 +31,15 @@ fixed in-process constant, so `total` is a real count and `limit` is `null`.
31
31
  ## Symptom categories
32
32
 
33
33
  `storage`, `network`, `compute`, `ha_drs`, `host_lifecycle`, `power_lifecycle`,
34
- `auth`, `platform`.
34
+ `auth`, `platform`, `hardware`, `licensing`, `data_collection`.
35
35
  See `references/routing.md` for keyword signatures and the skill each routes to.
36
36
 
37
+ `hardware`, `licensing` and `data_collection` were added after real alert titles
38
+ ("Host TPM attestation alarm", "License will soon expire", "Objects are not
39
+ receiving data from adapter instance") matched no category at all. A roll-up
40
+ such as "Group population health is degraded" is deliberately left
41
+ uncategorized: it names no subsystem, and the cause is in one of its members.
42
+
37
43
  `host_lifecycle` is a host changing its own availability state — maintenance
38
44
  mode, shutdown, reboot, standby, connection loss, sync failure.
39
45
  `power_lifecycle` is the VM-level equivalent. They are separate because they are
@@ -11,9 +11,12 @@ Keep this in sync with `_CATEGORY_SIGNATURES` in `vmware_debug/ops/timeline.py`
11
11
  | **compute** | cpu ready, memory, balloon, swap, contention, numa | vmware-aria (metrics + anomalies) | pilot (rightsizing) |
12
12
  | **ha_drs** | ha, high availability, drs, failover, admission control, isolation | vmware-monitor, vmware-aiops (cluster) | pilot |
13
13
  | **host_lifecycle** | maintenance mode, shut down of, host reboot, standby mode, lost connection to, cannot synchronize, not responding | vmware-monitor (host/cluster state), vmware-harden (drift — a host that left service on cue was told to), vmware-log-insight (vpxd/hostd) | pilot |
14
- | **power_lifecycle** | power on/off, failed to start, boot, vmx, ovf, clone, snapshot | vmware-aiops (task status, snapshot tree), vmware-monitor | aiops |
15
- | **auth** | login, authentication, denied, 401, 403, token, certificate, tls | config/.env, target cert + time sync | config fix |
14
+ | **power_lifecycle** | power on/off, failed to start, boot, vmx, ovf, clone, snapshot | vmware-aiops (snapshot tree), vmware-monitor | aiops |
15
+ | **auth** | login, authentication, denied, 401, 403, token, certificate, tls, password | config/.env, target cert + time sync | config fix |
16
16
  | **platform** | vpxd, hostd, service restart, crash, 503, not responding, disconnected | vmware-monitor (connection/service), vmware-log-insight (vpxd/hostd) | pilot |
17
+ | **hardware** | tpm, attestation, ipmi, sensor, bmc | vmware-monitor (host sensors and the CIM Server that supplies them, host logs for ipmi/cim), the server's BMC | site / vendor |
18
+ | **licensing** | license | vmware-monitor (license_status — which asset holds which key); an Aria alert on "Unlicensed Group" is Aria's own license | license portal |
19
+ | **data_collection** | adapter instance, not receiving data, collector group, cloud proxy | vmware-aria (adapters, collector groups, node health, objects that stopped reporting), vmware-monitor (is the source vCenter reachable) | aria admin |
17
20
 
18
21
  ## Remediation handoff (advisor → executor)
19
22
 
@@ -6,7 +6,7 @@ is a pure, offline correlation engine. There is no `config.yaml` and no `.env`.
6
6
  ## Install
7
7
 
8
8
  ```bash
9
- uv tool install vmware-debug==1.11.4
9
+ uv tool install vmware-debug==1.12.1
10
10
  vmware-debug categories # verify it runs
11
11
  ```
12
12
 
@@ -15,7 +15,7 @@ vmware-debug categories # verify it runs
15
15
  ```json
16
16
  {
17
17
  "command": "uvx",
18
- "args": ["--from", "vmware-debug==1.11.4", "vmware-debug-mcp"]
18
+ "args": ["--from", "vmware-debug==1.12.1", "vmware-debug-mcp"]
19
19
  }
20
20
  ```
21
21
 
@@ -50,6 +50,13 @@ So the change this file guards is not a bigger vocabulary. It is:
50
50
  know" and to go find that subsystem's read tools. For ``VmPoweredOnEvent``
51
51
  that is a wasted round trip after a false premise.
52
52
 
53
+ **Re-measured 2026-09-15: reads 140, misses 287.** The 16 newly read types are
54
+ the exception the paragraph above does not cover — 14 are licensing events
55
+ (``LicenseExpiredEvent`` and its family) and 2 are password events, and licensing
56
+ *was* a subsystem the taxonomy lacked. The category was added because real lab
57
+ alert titles needed it (``test_real_alert_titles_classify``), not to move this
58
+ number; the claim about the rest of the residue still holds.
59
+
53
60
  The numbers below are recorded, not required. If someone later raises coverage by
54
61
  padding ``_CATEGORY_SIGNATURES``, ``test_the_taxonomy_was_not_quietly_padded``
55
62
  fails and says so, because a coverage figure that goes up without an explanation
@@ -0,0 +1,153 @@
1
+ """Alert titles from a real estate, and what the taxonomy made of them.
2
+
3
+ On 2026-09-14 eight alerts were read off the lab vCenter 8.0.3 and Aria 8.18.7
4
+ and passed through ``triage``. Six came back ``uncategorized`` and the top
5
+ hypothesis was "uncategorized" itself. Unlike the residue measured in
6
+ ``test_classification_against_vsphere_vocabulary`` — events whose subsystem the
7
+ taxonomy already had, spelled differently — these named subsystems it had no
8
+ category for at all:
9
+
10
+ Root user password expired credential lifecycle; auth read only
11
+ login/token/certificate words
12
+ License will soon expire licensing — no category
13
+ Expired vCenter Server license licensing — no category
14
+ Host TPM attestation alarm hardware — routed in the catalogue,
15
+ but the classifier could never emit it
16
+ Objects are not receiving data the monitoring pipeline itself — no
17
+ from adapter instance category
18
+
19
+ So the change is new categories with their own routing, not more synonyms for
20
+ the old ones. One title stays unread on purpose, and is pinned below.
21
+ """
22
+
23
+ from __future__ import annotations
24
+
25
+ import pytest
26
+
27
+ from vmware_debug.envelope import normalize_event
28
+ from vmware_debug.ops.cases.readiness import readiness
29
+ from vmware_debug.ops.cases.sources import load_catalogue
30
+ from vmware_debug.ops.timeline import classification_coverage, classify_symptom
31
+
32
+ #: (alert title, source, the category it has to land in first)
33
+ LAB_ALERTS = (
34
+ ("Root user password expired", "monitor", "auth"),
35
+ ("License will soon expire", "aria", "licensing"),
36
+ ("Expired vCenter Server license", "monitor", "licensing"),
37
+ ("Host TPM attestation alarm", "monitor", "hardware"),
38
+ ("Objects are not receiving data from adapter instance", "aria", "data_collection"),
39
+ ("vCenter appliance health service is down", "aria", "platform"),
40
+ ("Host connection and power state", "monitor", "host_lifecycle"),
41
+ )
42
+
43
+ #: A roll-up of its members' health. It names no subsystem: whatever is wrong is
44
+ #: wrong with one of the members, and any category given to the title would be
45
+ #: a guess dressed as a routing decision.
46
+ ROLLUP_ALERT = "Group population health is degraded"
47
+
48
+
49
+ def _event(title: str, source: str) -> object:
50
+ return normalize_event(
51
+ {"ts": "2026-09-14T08:00:00Z", "source": source, "severity": "warning", "text": title}
52
+ )
53
+
54
+
55
+ @pytest.mark.parametrize(("title", "source", "category"), LAB_ALERTS)
56
+ def test_each_lab_alert_lands_in_its_category_first(title, source, category):
57
+ event = _event(title, source)
58
+ found = classify_symptom(event.text, event.entity, event.fields)
59
+ assert found and found[0] == category, f"{title!r} -> {found}"
60
+
61
+
62
+ def test_the_population_rollup_stays_unread():
63
+ """Pinned so a later keyword does not quietly start routing it somewhere."""
64
+ assert classify_symptom(ROLLUP_ALERT) == []
65
+
66
+
67
+ def test_the_lab_stream_is_now_mostly_readable():
68
+ events = [_event(t, s) for t, s, _c in LAB_ALERTS] + [_event(ROLLUP_ALERT, "aria")]
69
+ coverage = classification_coverage(events)
70
+ assert coverage["uncategorized"] == 1
71
+ assert coverage["unmatched_samples"] == [ROLLUP_ALERT]
72
+
73
+
74
+ @pytest.mark.parametrize(
75
+ ("text", "category"),
76
+ [
77
+ # Each contains a word the new categories added, and was already about
78
+ # something else. A bare "collector" took the first two from platform.
79
+ ("The ESXi Dump Collector service is not running on vcsa01", "platform"),
80
+ ("vCenter Server Syslog Collector service stopped", "platform"),
81
+ ("vSAN license expired for datastore vsan-01", "storage"),
82
+ ("Password for user root@192.168.60.10 denied: authentication failed", "auth"),
83
+ ],
84
+ )
85
+ def test_a_new_category_does_not_steal_an_existing_match(text, category):
86
+ assert classify_symptom(text)[0] == category, classify_symptom(text)
87
+
88
+
89
+ class TestReadiness:
90
+ def test_aiops_is_a_recognised_skill(self):
91
+ """It used to land in unrecognised_skills while routing pointed
92
+ power_lifecycle cases at it."""
93
+ out = readiness(["vmware-monitor", "vmware-aiops"])
94
+ assert out["unrecognised_skills"] == []
95
+
96
+ def test_aiops_lifts_power_lifecycle_to_probable(self):
97
+ without = readiness(["vmware-monitor"])["categories"]["power_lifecycle"]
98
+ with_aiops = readiness(["vmware-monitor", "vmware-aiops"])["categories"]["power_lifecycle"]
99
+ assert without["ceiling"] == "candidate"
100
+ assert with_aiops["ceiling"] == "probable"
101
+ assert with_aiops["independent_sources"] == ["vmware-aiops", "vmware-monitor"]
102
+
103
+ def test_aiops_classes_name_only_reads_aiops_does_itself(self):
104
+ """AIops's cluster summary and investigation bundles call vmware-monitor's
105
+ code. Listed under aiops they would be monitor's data counted as a second
106
+ source — a Probable bought with one source under two names."""
107
+ delegated = {
108
+ "cluster_health_summary",
109
+ "cross_vcenter_attention",
110
+ "vm_investigation_bundle",
111
+ "host_investigation_bundle",
112
+ "datastore_investigation_bundle",
113
+ }
114
+ for name, spec in load_catalogue()["classes"].items():
115
+ if spec.get("skill") == "vmware-aiops":
116
+ tools = {e["tool"] for e in spec["tools"]}
117
+ assert not tools & delegated, f"{name} lists delegated tools {tools & delegated}"
118
+
119
+ def test_task_status_is_not_offered_as_evidence(self):
120
+ """It takes a task id; a plan has VM names. Called with a name it answers
121
+ "gone" and no error, which the grader would count as a second source."""
122
+ tools = {
123
+ e["tool"]
124
+ for spec in load_catalogue()["classes"].values()
125
+ if spec.get("skill") == "vmware-aiops"
126
+ for e in spec["tools"]
127
+ }
128
+ assert "vm_task_status" not in tools
129
+
130
+ def test_collection_reaches_probable_with_monitor_and_aria(self):
131
+ """Aria's collector state and vCenter's own reachability are two observations."""
132
+ out = readiness(["vmware-monitor", "vmware-aria"])["categories"]["data_collection"]
133
+ assert out["ceiling"] == "probable"
134
+
135
+ def test_licensing_does_not_count_an_aria_alert_as_a_second_source(self):
136
+ """An Aria alert about a vCenter license is built from vCenter's data, so
137
+ monitor plus aria would be one source counted twice."""
138
+ out = readiness(["vmware-monitor", "vmware-aria"])["categories"]["licensing"]
139
+ assert out["ceiling"] == "candidate"
140
+
141
+ def test_one_skill_alone_is_still_one_source(self):
142
+ out = readiness(["vmware-aria"])["categories"]["data_collection"]
143
+ assert out["ceiling"] == "candidate"
144
+
145
+ def test_a_hardware_plan_can_ask_for_the_sensors(self):
146
+ routed = load_catalogue()["routing"]["hardware"]["supporting"]
147
+ tools = {e["tool"] for c in routed for e in load_catalogue()["classes"][c]["tools"]}
148
+ assert {"get_host_sensors", "get_host_services"} <= tools
149
+
150
+
151
+ @pytest.fixture(autouse=True)
152
+ def _isolated_home(tmp_path, monkeypatch):
153
+ monkeypatch.setenv("OPS_HOME", str(tmp_path / "vmware"))
@@ -1293,7 +1293,7 @@ wheels = [
1293
1293
 
1294
1294
  [[package]]
1295
1295
  name = "vmware-debug"
1296
- version = "1.11.4"
1296
+ version = "1.12.1"
1297
1297
  source = { editable = "." }
1298
1298
  dependencies = [
1299
1299
  { name = "mcp", extra = ["cli"] },
@@ -1316,7 +1316,7 @@ requires-dist = [
1316
1316
  { name = "pyyaml", specifier = ">=6.0,<7.0" },
1317
1317
  { name = "rich", specifier = ">=13.0,<15.0" },
1318
1318
  { name = "typer", specifier = ">=0.12,<1.0" },
1319
- { name = "vmware-policy", specifier = ">=1.13.1,<2.0" },
1319
+ { name = "vmware-policy", specifier = ">=1.15.0,<2.0" },
1320
1320
  ]
1321
1321
 
1322
1322
  [package.metadata.requires-dev]
@@ -1328,14 +1328,14 @@ dev = [
1328
1328
 
1329
1329
  [[package]]
1330
1330
  name = "vmware-policy"
1331
- version = "1.13.1"
1331
+ version = "1.15.0"
1332
1332
  source = { registry = "https://pypi.org/simple" }
1333
1333
  dependencies = [
1334
1334
  { name = "pyyaml" },
1335
1335
  { name = "rich" },
1336
1336
  { name = "typer" },
1337
1337
  ]
1338
- sdist = { url = "https://files.pythonhosted.org/packages/df/76/1c97d2243ee2190627fce431cfebe548db42fb6d541753a7bf5275d5d2db/vmware_policy-1.13.1.tar.gz", hash = "sha256:d1d22ae3ee954ccefbe42d43525834d45fac83217ee9bceb356a275c8683932c", size = 175470, upload-time = "2026-09-12T00:04:32.99Z" }
1338
+ sdist = { url = "https://files.pythonhosted.org/packages/ac/0c/c7b6c9e2e82b5c8d40b0721bfc9a6428bdd9fd41d8c1c6ec492b94f183f8/vmware_policy-1.15.0.tar.gz", hash = "sha256:48e1850c434dd3f34401b1e3785c073d72b451af904cc2f8c7d3bde772309e8a", size = 181525, upload-time = "2026-09-15T05:43:38.307Z" }
1339
1339
  wheels = [
1340
- { url = "https://files.pythonhosted.org/packages/6f/63/8ea229871a27d4a187ed546d007776a4209d2bff066d41a7207ca519a9af/vmware_policy-1.13.1-py3-none-any.whl", hash = "sha256:7946a4acb54d16754d5d447a4a56a06787d34b6aa33dda9434ff5753522c3959", size = 67768, upload-time = "2026-09-12T00:04:31.448Z" },
1340
+ { url = "https://files.pythonhosted.org/packages/37/26/088f02325e51109cfe8e433d861c28f75f22ce26bb375944d365c46f85c4/vmware_policy-1.15.0-py3-none-any.whl", hash = "sha256:2f5a7b0e57fc084ea9083e67fb6e3c27059e6bd9c75b3eabfed58d6ee4366db4", size = 69524, upload-time = "2026-09-15T05:43:36.606Z" },
1341
1341
  ]
@@ -6,4 +6,4 @@ remediation to vmware-aiops / vmware-pilot. Never writes to any VMware system
6
6
  and never executes fixes; its only writes go to the local case ledger.
7
7
  """
8
8
 
9
- __version__ = "1.11.4"
9
+ __version__ = "1.12.1"
@@ -20,6 +20,7 @@ from rich.table import Table
20
20
 
21
21
  from vmware_debug import __version__
22
22
  from vmware_debug.mcp.tools import incident_timeline, list_symptom_categories
23
+ from vmware_policy import cli_local
23
24
 
24
25
 
25
26
  def _harden_console_encoding() -> None:
@@ -53,12 +54,14 @@ console = Console()
53
54
 
54
55
 
55
56
  @app.command()
57
+ @cli_local("prints the installed version")
56
58
  def version() -> None:
57
59
  """Print the installed version."""
58
60
  console.print(f"vmware-debug {__version__}")
59
61
 
60
62
 
61
63
  @app.command()
64
+ @cli_local("prints the built-in symptom catalogue; contacts nothing")
62
65
  def categories() -> None:
63
66
  """List the symptom categories debug recognises and what to check for each."""
64
67
  table = Table(title="vmware-debug symptom categories")
@@ -71,6 +74,7 @@ def categories() -> None:
71
74
 
72
75
 
73
76
  @app.command()
77
+ @cli_local("correlates events read from a file or stdin; contacts nothing")
74
78
  def triage(
75
79
  events_file: Optional[Path] = typer.Option(
76
80
  None, "--events", "-e", help="JSON file of event envelopes; reads stdin if omitted."
@@ -99,6 +103,7 @@ def triage(
99
103
 
100
104
 
101
105
  @app.command()
106
+ @cli_local("starts the MCP server; its tools audit themselves")
102
107
  def mcp() -> None:
103
108
  """Start the stdio MCP server (no network access; proxy-safe)."""
104
109
  from vmware_debug.mcp_server.server import main as _main
@@ -588,7 +588,8 @@ def build_server() -> FastMCP:
588
588
  whether it is
589
589
  available, through which tools, and if not, `how_to_supply`. Per
590
590
  symptom category (storage, network, compute, ha_drs, configuration,
591
- accelerator, kubernetes, hardware): a `ceiling` and the
591
+ accelerator, kubernetes, hardware, host_lifecycle, power_lifecycle,
592
+ auth, platform, licensing, data_collection): a `ceiling` and the
592
593
  `independent_sources` behind it. There is deliberately no single score —
593
594
  "readiness 78%" cannot be acted on, "storage reaches Probable, hardware
594
595
  reaches Candidate" can.
@@ -648,7 +649,8 @@ def build_server() -> FastMCP:
648
649
  category: Force the symptom class instead of inferring it. Exactly
649
650
  one of: storage, network, compute, ha_drs, configuration,
650
651
  accelerator, kubernetes, hardware, host_lifecycle,
651
- power_lifecycle, auth, platform. Omit to infer, then read
652
+ power_lifecycle, auth, platform, licensing,
653
+ data_collection. Omit to infer, then read
652
654
  `category_signals` for the word that decided it.
653
655
  available_skills: Narrow to the skills actually installed, in either
654
656
  spelling ("monitor" or "vmware-monitor"). Omit to assume all of
@@ -79,7 +79,7 @@ _CATEGORY_SIGNATURES: tuple[tuple[str, tuple[str, ...], str], ...] = (
79
79
  (
80
80
  "auth",
81
81
  ("login", "authentication", "permission", "denied", "unauthorized",
82
- "401", "403", "token", "certificate", "tls"),
82
+ "401", "403", "token", "certificate", "tls", "password"),
83
83
  "check the service account + credentials in config/.env; verify the "
84
84
  "target's certificate/time sync",
85
85
  ),
@@ -90,6 +90,36 @@ _CATEGORY_SIGNATURES: tuple[tuple[str, tuple[str, ...], str], ...] = (
90
90
  "vmware-monitor (host connection state + service health) + "
91
91
  "vmware-log-insight (vpxd/hostd logs around the first error)",
92
92
  ),
93
+ # The three below are subsystems, not synonyms. Real alert titles from a lab
94
+ # estate ("Host TPM attestation alarm", "License will soon expire", "Objects
95
+ # are not receiving data from adapter instance") matched nothing because no
96
+ # category covered them at all — see test_real_alert_titles_classify.
97
+ (
98
+ "hardware",
99
+ ("tpm", "attestation", "ipmi", "sensor", "bmc"),
100
+ "vmware-monitor (get_host_sensors; host_log_scan for ipmi/cim errors — "
101
+ "no sensor data usually means no IPMI device or a stopped CIM service) + "
102
+ "the server's own BMC (iDRAC/iLO/XCC). A TPM attestation alarm is most "
103
+ "often a firmware setting (TPM 2.0 / TIS mode) rather than a failing part",
104
+ ),
105
+ (
106
+ "licensing",
107
+ ("license",),
108
+ "vmware-monitor (license_status: per-asset assignments show whether the "
109
+ "alarmed asset still holds the expiring key, or the alarm is stale) + "
110
+ "vmware-aria (an alert on 'Unlicensed Group' is Aria's own license, not "
111
+ "vCenter's)",
112
+ ),
113
+ (
114
+ "data_collection",
115
+ # Not a bare "collector": vCenter's own services are named that way
116
+ # ("ESXi Dump Collector service is not running") and belong to platform.
117
+ ("adapter instance", "not receiving data", "collector group", "cloud proxy"),
118
+ "vmware-aria (list_adapters for when each adapter last collected, "
119
+ "list_collector_groups, get_aria_health; list_resources to find the "
120
+ "objects that stopped reporting) + vmware-monitor (whether the source "
121
+ "vCenter itself is reachable)",
122
+ ),
93
123
  )
94
124
 
95
125
 
@@ -109,6 +109,52 @@ classes:
109
109
  - {tool: list_tkc_clusters, gives: "workload clusters and their phase"}
110
110
  - {tool: list_namespace_storage_usage, gives: "namespace storage pressure"}
111
111
 
112
+ # Same skill as virtualization_state, so never a second source for a monitor
113
+ # case. Separate only so a licensing plan asks for license_status and not
114
+ # for seven unrelated tools.
115
+ license_state:
116
+ label: License inventory and which asset holds which license
117
+ skill: vmware-monitor
118
+ decisive: false
119
+ tools:
120
+ - {tool: license_status, gives: "license expiry, and the license assigned to each vCenter and host"}
121
+ - {tool: get_alarms, gives: "whether the license alarm is active, and whether its condition still holds"}
122
+
123
+ collection_pipeline:
124
+ label: Aria's own collection pipeline (adapters, collectors, node health)
125
+ skill: vmware-aria
126
+ decisive: false
127
+ tools:
128
+ - {tool: list_adapters, gives: "each adapter instance and when it last collected"}
129
+ - {tool: list_collector_groups, gives: "collector groups and their members"}
130
+ - {tool: get_aria_health, gives: "Aria node and service health"}
131
+ - {tool: list_resources, gives: "objects under the adapter, to find the ones that stopped reporting"}
132
+
133
+ # Same skill as virtualization_state, so never a second source. Separate so
134
+ # a hardware plan asks for the sensors and the CIM Server state that supplies
135
+ # them, which is what its suggested_check tells the reader to look at.
136
+ host_hardware:
137
+ label: Host hardware sensors and the CIM Server that supplies them
138
+ skill: vmware-monitor
139
+ decisive: false
140
+ tools:
141
+ - {tool: get_host_sensors, gives: "sensor readings per host, and hosts reporting none with their CIM Server state"}
142
+ - {tool: get_host_services, gives: "whether sfcbd-watchdog (CIM Server) is running on the host"}
143
+
144
+ # Only reads vmware-aiops performs itself. Its cluster_health_summary,
145
+ # cross_vcenter_attention and *_investigation_bundle tools call
146
+ # vmware-monitor's code; listed here they would be monitor's data counted as
147
+ # a second, independent source.
148
+ # vm_task_status is left out on purpose: it takes a task id, a plan only has
149
+ # VM names, and called with a name it answers "gone" with no error — a reply
150
+ # that would count as a second source while saying nothing.
151
+ vm_operations:
152
+ label: VM snapshot tree
153
+ skill: vmware-aiops
154
+ decisive: false
155
+ tools:
156
+ - {tool: vm_list_snapshots, gives: "the VM's snapshot tree"}
157
+
112
158
  # ── The two that do not exist ──────────────────────────────────────────
113
159
  # Both are decisive: either one can take a conclusion to Confirmed. Neither
114
160
  # has a tool. Listing them empty is what lets case_readiness report an honest
@@ -169,7 +215,7 @@ routing:
169
215
  supporting: [kubernetes, virtualization_state, logs]
170
216
  decisive: [knowledge]
171
217
  hardware:
172
- supporting: [virtualization_state, logs]
218
+ supporting: [host_hardware, virtualization_state, logs]
173
219
  decisive: [hardware]
174
220
 
175
221
  # The three below complete the set that ops/timeline.py's keyword classifier
@@ -188,7 +234,7 @@ routing:
188
234
  decisive: [knowledge]
189
235
 
190
236
  power_lifecycle:
191
- supporting: [virtualization_state, logs]
237
+ supporting: [virtualization_state, logs, vm_operations]
192
238
  decisive: [knowledge]
193
239
 
194
240
  auth:
@@ -198,3 +244,17 @@ routing:
198
244
  platform:
199
245
  supporting: [virtualization_state, logs, metrics_and_anomalies]
200
246
  decisive: [knowledge]
247
+
248
+ # Licensing has one source here. An Aria alert about a vCenter license is
249
+ # built from vCenter's own data, so counting it would be one source twice —
250
+ # the same double-count vm_operations refuses for AIops's bundles.
251
+ licensing:
252
+ supporting: [license_state]
253
+ decisive: [knowledge]
254
+
255
+ # Two observations: Aria's own collector state, and whether the vCenter it
256
+ # collects from is reachable.
257
+
258
+ data_collection:
259
+ supporting: [collection_pipeline, virtualization_state]
260
+ decisive: [knowledge]
File without changes
File without changes
File without changes