vmware-debug 1.9.0__tar.gz → 1.11.0__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_debug-1.9.0 → vmware_debug-1.11.0}/.claude-plugin/plugin.json +1 -1
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/.mcp.json +1 -1
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/PKG-INFO +1 -1
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/RELEASE_NOTES.md +84 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/pyproject.toml +1 -1
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/server.json +11 -2
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/skills/vmware-debug/SKILL.md +52 -3
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/skills/vmware-debug/references/agent-guardrails.md +1 -1
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/skills/vmware-debug/references/capabilities.md +8 -1
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/skills/vmware-debug/references/event-envelope.md +1 -1
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/skills/vmware-debug/references/routing.md +1 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/eval/capability/_scores.json +54 -53
- vmware_debug-1.11.0/tests/eval/regression/test_case_id_non_ascii.py +86 -0
- vmware_debug-1.11.0/tests/eval/regression/test_evidence_payload_contract.py +124 -0
- vmware_debug-1.11.0/tests/eval/regression/test_host_lifecycle_classification.py +244 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/eval/regression/test_investigation_metrics.py +103 -4
- vmware_debug-1.11.0/tests/eval/regression/test_skill_name_aliases.py +124 -0
- vmware_debug-1.11.0/tests/eval/regression/test_spike_resolution.py +171 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/test_case_grading.py +29 -4
- vmware_debug-1.11.0/tests/test_knowledge.py +237 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/test_timeline.py +5 -1
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/uv.lock +1 -1
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/__init__.py +1 -1
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/mcp/tools.py +2 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/mcp_server/server.py +91 -21
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/api.py +49 -1
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/evidence.py +8 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/grading.py +90 -3
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/ids.py +25 -7
- vmware_debug-1.11.0/vmware_debug/ops/cases/knowledge.py +375 -0
- vmware_debug-1.11.0/vmware_debug/ops/cases/payloads.py +110 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/plan.py +48 -13
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/readiness.py +43 -15
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/store.py +8 -4
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/timeline.py +26 -29
- vmware_debug-1.11.0/vmware_debug/ops/skill_names.py +92 -0
- vmware_debug-1.11.0/vmware_debug/ops/timeline.py +568 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/rules/evidence_sources.yaml +9 -0
- vmware_debug-1.9.0/vmware_debug/ops/timeline.py +0 -326
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/.gitignore +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/README-CN.md +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/README.md +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/SECURITY.md +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/docs/plans/2026-08-28-investigation-layer-design.md +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/skills/vmware-debug/references/cli-reference.md +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/skills/vmware-debug/references/setup-guide.md +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/eval/__init__.py +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/eval/capability/__init__.py +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/eval/capability/_family.py +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/eval/capability/_scoring.py +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/eval/capability/_skill.py +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/eval/capability/conftest.py +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/eval/capability/test_entity_reachability.py +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/eval/capability/test_error_actionability.py +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/eval/capability/test_tool_description_quality.py +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/eval/capability/test_tool_manifest_budget.py +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/eval/regression/__init__.py +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/eval/regression/test_capability_grader.py +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/eval/regression/test_debug_regressions.py +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/eval/regression/test_result_envelope.py +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/eval/regression/test_tool_annotations.py +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/test_case_conclusion.py +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/test_case_evidence.py +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/test_case_hypotheses.py +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/test_case_ids.py +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/test_case_plan.py +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/test_case_readiness.py +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/test_case_store.py +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/test_case_timeline_close.py +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/test_case_tools.py +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/tests/test_safe_error_passthrough.py +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/cli.py +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/envelope.py +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/mcp/__init__.py +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/mcp_server/__init__.py +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/ops/__init__.py +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/__init__.py +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/conclusion.py +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/hypotheses.py +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/model.py +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/vmware_debug/ops/cases/sources.py +0 -0
- {vmware_debug-1.9.0 → vmware_debug-1.11.0}/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.
|
|
5
|
+
"version": "1.11.0",
|
|
6
6
|
"description": "Read-only VMware incident correlation: timeline, spikes, root-cause routing. 2 MCP tools.",
|
|
7
7
|
"author": {
|
|
8
8
|
"name": "Wei Zhou",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: vmware-debug
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.11.0
|
|
4
4
|
Summary: VMware diagnostic brain — read-only 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
|
|
@@ -1,3 +1,87 @@
|
|
|
1
|
+
## v1.11.0 — the correlation engine, on a real incident
|
|
2
|
+
|
|
3
|
+
A tester fed the investigation layer a genuine failure: on 08-03 at 05:49, four
|
|
4
|
+
ESXi hosts went `EnteringMaintenanceMode` → `EnteredMaintenanceMode` →
|
|
5
|
+
`HostShutdown` → `HostConnectionLost` → `HostSyncFailed` ×24 within 32 seconds,
|
|
6
|
+
and never came back. 3,818 real vCenter events over 40 days, pulled per this
|
|
7
|
+
skill's own envelope documentation. The case-management layer held up. The
|
|
8
|
+
correlation engine — the thing this skill is named for — did not.
|
|
9
|
+
|
|
10
|
+
**80% of events were `uncategorized`, and the headline conclusion was "cannot
|
|
11
|
+
classify, widen your search window".** The classification table's
|
|
12
|
+
`power_lifecycle` keywords were all VM-centric (`power on`/`power off`, `vmx`,
|
|
13
|
+
`ovf`, `clone`, `snapshot`); nothing covered host maintenance mode or host
|
|
14
|
+
shutdown, and `"Shut down of esxi05"` does not match `"power off"`. There is now
|
|
15
|
+
a `host_lifecycle` category — a separate category rather than more keywords,
|
|
16
|
+
because it is a separate investigation, routed to monitor + logs + harden (a
|
|
17
|
+
host that left service on cue was usually told to). `classification_coverage()`
|
|
18
|
+
reports what went unread in every result, and above ~25% it reaches
|
|
19
|
+
`next_checks`. The "widen the window" remedy is gone: the window was already 40
|
|
20
|
+
days, and widening makes the incident *less* findable.
|
|
21
|
+
|
|
22
|
+
**The only spike detected was the day the hosts came back up.**
|
|
23
|
+
`_auto_bin_seconds` was `span/30`, so a 29-day window gave 23.2-hour buckets;
|
|
24
|
+
the incident hour was the second-busiest in the dataset and absent from the
|
|
25
|
+
spike list, while `--bin-seconds 3600` found it at z=8.25. Binning now chooses
|
|
26
|
+
by event *density*, from a floor of 4 events per bin that is derived rather than
|
|
27
|
+
tuned: spikes flag at mean+2σ, and a count only exceeds twice its mean once the
|
|
28
|
+
mean reaches 4. The chosen width, how it was chosen, and the bin count are
|
|
29
|
+
reported.
|
|
30
|
+
|
|
31
|
+
**The same incident described five ways routed five different ways** — decided
|
|
32
|
+
by an incidental noun, with the most accurate description routing nowhere.
|
|
33
|
+
Categories are now ranked by matched-keyword length, so a long phrase beats a
|
|
34
|
+
stray word, and the losing categories are named.
|
|
35
|
+
|
|
36
|
+
Also fixed: `case_timeline`'s payload contract was undocumented and its MCP
|
|
37
|
+
parameter type refused the array shape the docstring implied; pure-Chinese
|
|
38
|
+
summaries collapsed to one `case_id` per second, with a collision message that
|
|
39
|
+
was false; and skill names were compared as raw strings, so `monitor` and
|
|
40
|
+
`vmware-monitor` counted as two independent evidence sources — buying *Probable*
|
|
41
|
+
out of orthography, in the layer built to prevent exactly that.
|
|
42
|
+
|
|
43
|
+
## v1.10.0 — the knowledge layer is implemented, not just described
|
|
44
|
+
|
|
45
|
+
v1.9.0 shipped the investigation layer with `applies_to` documented as the thing
|
|
46
|
+
that makes a knowledge entry decisive. Nothing parsed a knowledge file and
|
|
47
|
+
nothing checked applicability. Any file at all under the knowledge root raised
|
|
48
|
+
the reported ceiling, and anything labelled `knowledge-kb` counted as decisive.
|
|
49
|
+
The route to a wrong Confirmed that the design was written to close was open the
|
|
50
|
+
whole time, and the metric said zero only because no fixture walked it.
|
|
51
|
+
|
|
52
|
+
`case_knowledge` (the 14th tool) answers the first question anyone mounting a
|
|
53
|
+
library asks — which formats are taken — without them reading a design document:
|
|
54
|
+
|
|
55
|
+
.md .markdown YAML front-matter between --- fences, body below (preferred)
|
|
56
|
+
.yaml .yml the whole file is one entry
|
|
57
|
+
.json one entry per file
|
|
58
|
+
.jsonl one JSON object per line (what ticketing systems export)
|
|
59
|
+
.csv .tsv one entry per row; dotted columns for nested constraints
|
|
60
|
+
.txt .log a sibling <name>.yaml carries the metadata
|
|
61
|
+
|
|
62
|
+
PDF, DOCX, PPTX and HTML are named as needing conversion rather than ignored,
|
|
63
|
+
and a file that cannot be parsed is reported rather than skipped.
|
|
64
|
+
|
|
65
|
+
**An entry is decisive only if its own `applies_to` was checked against the
|
|
66
|
+
case's recorded versions and passed.** Three things follow, each found by
|
|
67
|
+
reviewing the version before it:
|
|
68
|
+
|
|
69
|
+
* Knowledge evidence must say WHICH entry it is. "Some applicable entry is
|
|
70
|
+
mounted somewhere" is a different claim from "this one applies", and the first
|
|
71
|
+
version accepted it — an agent citing the entry written for 9.x could confirm
|
|
72
|
+
a case about 8.0.3 as long as an unrelated entry happened to match.
|
|
73
|
+
* A constraint the case scope cannot answer is not a match. Silence is not a
|
|
74
|
+
pass; reading it as one is how an entry for different hardware ends a case.
|
|
75
|
+
* **A constraint the checker cannot evaluate is not a pass either.** The first
|
|
76
|
+
version understood product, build, driver and firmware and treated every other
|
|
77
|
+
key as satisfied, so `build` without a `product`, a `hardware_model` list, and
|
|
78
|
+
an outright typo all came back decisive. Unknown and satisfied must never be
|
|
79
|
+
the same answer.
|
|
80
|
+
|
|
81
|
+
Given two entries that read identically — one for `>=8.0, <9.0`, one for
|
|
82
|
+
`>=9.0` — a case scoped to vSphere 8.0.3 now reaches Confirmed citing the first
|
|
83
|
+
and stays at Probable citing the second, naming the range that excluded it.
|
|
84
|
+
|
|
1
85
|
## v1.9.0 — the investigation layer: 2 tools become 13
|
|
2
86
|
|
|
3
87
|
Correlating events answers "what happened together". A case answers "what do we
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vmware-debug"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.11.0"
|
|
8
8
|
description = "VMware diagnostic brain — read-only incident triage, log/event correlation, and root-cause routing across the VMware skill family"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -7,12 +7,21 @@
|
|
|
7
7
|
"url": "https://github.com/vmware-skills/VMware-Debug",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "1.
|
|
10
|
+
"version": "1.11.0",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "pypi",
|
|
14
14
|
"identifier": "vmware-debug",
|
|
15
|
-
"version": "1.
|
|
15
|
+
"version": "1.11.0",
|
|
16
|
+
"runtimeHint": "uvx",
|
|
17
|
+
"packageArguments": [
|
|
18
|
+
{
|
|
19
|
+
"type": "positional",
|
|
20
|
+
"valueHint": "subcommand",
|
|
21
|
+
"value": "mcp",
|
|
22
|
+
"description": "Starts the stdio MCP server. Without it the console script runs the human CLI and exits."
|
|
23
|
+
}
|
|
24
|
+
],
|
|
16
25
|
"transport": {
|
|
17
26
|
"type": "stdio"
|
|
18
27
|
}
|
|
@@ -100,7 +100,7 @@ a recommended plan.
|
|
|
100
100
|
- **MCP** (in an agent): the agent calls the other skills' read tools, then `incident_timeline` to correlate. This is the primary mode — that's where the cross-skill "联动" happens.
|
|
101
101
|
- **CLI** (humans): `vmware-debug triage --events events.json` correlates a JSON array you collected yourself.
|
|
102
102
|
|
|
103
|
-
## MCP Tools (
|
|
103
|
+
## MCP Tools (14 — 7 read, 7 write)
|
|
104
104
|
|
|
105
105
|
**Correlation** — stateless, for a single look:
|
|
106
106
|
|
|
@@ -115,6 +115,7 @@ a recommended plan.
|
|
|
115
115
|
|---|---|
|
|
116
116
|
| `case_open` | [WRITE] Define the event; returns a case id and the grade this environment can reach |
|
|
117
117
|
| `case_readiness` | [READ] What grade this environment can reach, per symptom category, **before** you start |
|
|
118
|
+
| `case_knowledge` | [READ] Which knowledge formats are accepted, what is mounted, and which entries apply to a case |
|
|
118
119
|
| `case_plan` | [READ] What to fetch next — skill, tool and purpose per step; recomputed from the case's current state |
|
|
119
120
|
| `case_list` | [READ] Cases, newest first |
|
|
120
121
|
| `case_get` | [READ] One case: scope, ledger sizes, grade history |
|
|
@@ -179,6 +180,52 @@ so the gap is visible now rather than when the conclusion refuses to firm up.
|
|
|
179
180
|
"readiness 78%" cannot. Two classes served by the *same* skill count as one
|
|
180
181
|
source, so it agrees with what `case_grade` will actually award.
|
|
181
182
|
|
|
183
|
+
### Mounting a knowledge library
|
|
184
|
+
|
|
185
|
+
`case_knowledge` answers "what can I add" without anyone reading this file.
|
|
186
|
+
Entries go under `$OPS_HOME/knowledge/{kb,runbook,sr,cases}/`:
|
|
187
|
+
|
|
188
|
+
| Format | How metadata travels |
|
|
189
|
+
|---|---|
|
|
190
|
+
| `.md` `.markdown` | YAML front-matter between `---` fences, body below — **preferred** |
|
|
191
|
+
| `.yaml` `.yml` | the whole file is one entry |
|
|
192
|
+
| `.json` | one entry per file |
|
|
193
|
+
| `.jsonl` | one JSON object per line — what ticketing systems export |
|
|
194
|
+
| `.csv` `.tsv` | one entry per row; use dotted columns (`driver.version`) for nested constraints |
|
|
195
|
+
| `.txt` `.log` | a sibling `<name>.yaml` carries the metadata |
|
|
196
|
+
|
|
197
|
+
PDF, DOCX, PPTX and HTML must be converted to Markdown first — `case_knowledge`
|
|
198
|
+
names them rather than ignoring them.
|
|
199
|
+
|
|
200
|
+
**Every entry needs an `applies_to` block to be decisive:**
|
|
201
|
+
|
|
202
|
+
```yaml
|
|
203
|
+
---
|
|
204
|
+
id: KB-2026-0417
|
|
205
|
+
applies_to:
|
|
206
|
+
product: vsphere
|
|
207
|
+
build: ">=8.0.3, <9.0"
|
|
208
|
+
driver: {name: nvme_pcie, version: ">=1.2.4"}
|
|
209
|
+
firmware: {vendor: dell, version: ">=52.26"}
|
|
210
|
+
---
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
`product`, `build`, `driver` and `firmware` are the constraints the checker
|
|
214
|
+
evaluates. **Any other key leaves the entry non-decisive** — including a typo —
|
|
215
|
+
because an unverified constraint is not a satisfied one, and `case_knowledge`
|
|
216
|
+
names the key it could not check.
|
|
217
|
+
|
|
218
|
+
Knowledge evidence must say **which** entry it is
|
|
219
|
+
(`case_submit_evidence(source_skill="knowledge-kb", knowledge_entry_id="KB-…")`)
|
|
220
|
+
— "some applicable entry is mounted somewhere" is a different claim from "this
|
|
221
|
+
one applies", and only the second can carry a conclusion.
|
|
222
|
+
|
|
223
|
+
Matching is **by version applicability, never by similarity** — an entry written
|
|
224
|
+
for the wrong build reads exactly like the right one, and similarity is the only
|
|
225
|
+
thing that would let it through. An entry with no `applies_to` can support a
|
|
226
|
+
hypothesis but can never make a case Confirmed. A constraint the case scope
|
|
227
|
+
cannot answer is not a match either: silence is not a pass.
|
|
228
|
+
|
|
182
229
|
> **On a stock install the ceiling is Probable.** Confirmed needs a decisive
|
|
183
230
|
> source, and there is neither a hardware-diagnostic channel (no Redfish/BMC, no
|
|
184
231
|
> SMART/NVMe) nor a knowledge library mounted — `~/.vmware/knowledge/` ships
|
|
@@ -209,8 +256,10 @@ vmware-debug mcp # start stdio MCP server (proxy-
|
|
|
209
256
|
## Troubleshooting
|
|
210
257
|
|
|
211
258
|
- **`incident_timeline` raises "event[N] could not be normalised"** — event N is missing a timestamp or has an unparseable one. Every event needs `ts` (ISO-8601, epoch seconds, or millis).
|
|
212
|
-
- **
|
|
213
|
-
- **No spikes detected on an obvious burst** — you
|
|
259
|
+
- **Most events come back "uncategorized"** — read `classification` in the result: it says how many, what share, and quotes the texts that matched nothing. Do **not** widen the window; a wider window adds baseline, not signal. The spikes still tell you *when*, which is answerable without a category. If the samples name a subsystem the taxonomy does not know, add a signature (see `references/routing.md`).
|
|
260
|
+
- **No spikes detected on an obvious burst** — check `binning` for the resolution you were given. The width is chosen from event density so that bins average ≥4 events; a burst shorter than one bin can still be flattened by it. Pass `bin_seconds` to narrow. Below three bins there is no baseline at all and nothing is reported.
|
|
261
|
+
- **`case_timeline` says zero events after you submitted results** — check `payload_events` in each `case_submit_evidence` reply. Events are read from a bare list, or from `items`/`events`/`rows`; a *summary* of a tool's result carries none. `note` names which items carried nothing and what keys they held instead.
|
|
262
|
+
- **`case_readiness` says a skill you have is not installed** — both spellings are accepted (`monitor` and `vmware-monitor`). A name it did not recognise comes back in `unrecognised_skills` rather than being read as missing.
|
|
214
263
|
- **It won't execute the fix** — by design. Route to vmware-aiops or vmware-pilot.
|
|
215
264
|
|
|
216
265
|
## Audit & Safety
|
{vmware_debug-1.9.0 → vmware_debug-1.11.0}/skills/vmware-debug/references/agent-guardrails.md
RENAMED
|
@@ -14,7 +14,7 @@ vmware-monitor and vmware-aria against a production vSphere estate with Llama
|
|
|
14
14
|
cross-skill rules are identical across this family; the parts below marked
|
|
15
15
|
vmware-debug are specific to this skill.
|
|
16
16
|
|
|
17
|
-
vmware-debug exposes
|
|
17
|
+
vmware-debug exposes 14 MCP tools: 7 reads and 7 writes. It connects to nothing
|
|
18
18
|
and holds no credentials — the calling agent gathers events from the other
|
|
19
19
|
skills, normalises them, and hands them over. The four writes go to the local
|
|
20
20
|
investigation ledger under `~/.vmware/cases/`, never to a VMware system. That
|
|
@@ -27,9 +27,16 @@ fixed in-process constant, so `total` is a real count and `limit` is `null`.
|
|
|
27
27
|
|
|
28
28
|
## Symptom categories
|
|
29
29
|
|
|
30
|
-
`storage`, `network`, `compute`, `ha_drs`, `
|
|
30
|
+
`storage`, `network`, `compute`, `ha_drs`, `host_lifecycle`, `power_lifecycle`,
|
|
31
|
+
`auth`, `platform`.
|
|
31
32
|
See `references/routing.md` for keyword signatures and the skill each routes to.
|
|
32
33
|
|
|
34
|
+
`host_lifecycle` is a host changing its own availability state — maintenance
|
|
35
|
+
mode, shutdown, reboot, standby, connection loss, sync failure.
|
|
36
|
+
`power_lifecycle` is the VM-level equivalent. They are separate because they are
|
|
37
|
+
separate investigations: the second is a task question for vmware-aiops, the
|
|
38
|
+
first is a cluster, DPM, vLCM or drift question.
|
|
39
|
+
|
|
33
40
|
## Design properties
|
|
34
41
|
|
|
35
42
|
- **Zero cross-skill runtime deps** — correlation is pure functions over plain
|
|
@@ -21,7 +21,7 @@ runtime dependency** on the other packages (no version lockstep, no heavy instal
|
|
|
21
21
|
| Field | Type | Notes |
|
|
22
22
|
|---|---|---|
|
|
23
23
|
| `ts` | string \| number | ISO-8601, epoch **seconds**, or epoch **millis** (auto-detected). Required. |
|
|
24
|
-
| `source` | string | `monitor` \| `aria` \| `loginsight` \| `nsx` \| `nsx-security` \| `storage` \| ... |
|
|
24
|
+
| `source` | string | `monitor` \| `aria` \| `loginsight` \| `nsx` \| `nsx-security` \| `storage` \| ... The catalogue in `rules/evidence_sources.yaml` spells the same skills `vmware-monitor`, `vmware-aria`, `vmware-log-insight`. Both spellings are understood wherever a skill is named — `case_readiness(available_skills=...)`, and the grader's count of independent sources, which treats two spellings of one skill as one source. |
|
|
25
25
|
| `severity` | string | Free text; normalised to `critical`/`error`/`warning`/`info`/`unknown`. |
|
|
26
26
|
| `entity` | string | The object the event is about (VM/host/datastore). May be empty. |
|
|
27
27
|
| `text` | string | Human-readable message — this is what the symptom classifier matches on. |
|
|
@@ -10,6 +10,7 @@ Keep this in sync with `_CATEGORY_SIGNATURES` in `vmware_debug/ops/timeline.py`
|
|
|
10
10
|
| **network** | vmotion, uplink, link down, mtu, firewall, dfw, segment, tier-0, bgp | vmware-nsx, vmware-nsx-security (traceflow, DFW hits) | pilot |
|
|
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
|
+
| **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 |
|
|
13
14
|
| **power_lifecycle** | power on/off, failed to start, boot, vmx, ovf, clone, snapshot | vmware-aiops (task status, snapshot tree), vmware-monitor | aiops |
|
|
14
15
|
| **auth** | login, authentication, denied, 401, 403, token, certificate, tls | config/.env, target cert + time sync | config fix |
|
|
15
16
|
| **platform** | vpxd, hostd, service restart, crash, 503, not responding, disconnected | vmware-monitor (connection/service), vmware-log-insight (vpxd/hostd) | pilot |
|
|
@@ -9,13 +9,14 @@
|
|
|
9
9
|
"list_symptom_categories",
|
|
10
10
|
"case_open",
|
|
11
11
|
"case_list",
|
|
12
|
-
"case_readiness"
|
|
12
|
+
"case_readiness",
|
|
13
|
+
"case_knowledge"
|
|
13
14
|
],
|
|
14
15
|
"reachable_on_surface": 8,
|
|
15
16
|
"reachable_only_via_description": 0,
|
|
16
17
|
"required_entity_params": 8,
|
|
17
18
|
"required_params_classified": "8/17",
|
|
18
|
-
"tool_count":
|
|
19
|
+
"tool_count": 14
|
|
19
20
|
},
|
|
20
21
|
"maximum": 8,
|
|
21
22
|
"pct": 100.0,
|
|
@@ -43,12 +44,12 @@
|
|
|
43
44
|
},
|
|
44
45
|
"entry_point_availability": {
|
|
45
46
|
"detail": {
|
|
46
|
-
"full_entry_points":
|
|
47
|
+
"full_entry_points": 6
|
|
47
48
|
},
|
|
48
|
-
"maximum":
|
|
49
|
-
"pct":
|
|
49
|
+
"maximum": 14,
|
|
50
|
+
"pct": 42.9,
|
|
50
51
|
"unit": "entry points",
|
|
51
|
-
"value":
|
|
52
|
+
"value": 6
|
|
52
53
|
},
|
|
53
54
|
"error_actionability": {
|
|
54
55
|
"detail": {
|
|
@@ -77,7 +78,7 @@
|
|
|
77
78
|
"names_artifact",
|
|
78
79
|
"states_remedy"
|
|
79
80
|
],
|
|
80
|
-
"where": "evidence.py:
|
|
81
|
+
"where": "evidence.py:289"
|
|
81
82
|
},
|
|
82
83
|
{
|
|
83
84
|
"message": "gaps.json in case {} is not valid JSON: {}. It was hand-edited. Restore it rather than deleting it \u2014",
|
|
@@ -85,7 +86,7 @@
|
|
|
85
86
|
"names_artifact",
|
|
86
87
|
"states_remedy"
|
|
87
88
|
],
|
|
88
|
-
"where": "evidence.py:
|
|
89
|
+
"where": "evidence.py:291"
|
|
89
90
|
},
|
|
90
91
|
{
|
|
91
92
|
"message": "Cannot read grading_rules.yaml at {}: {}",
|
|
@@ -93,7 +94,7 @@
|
|
|
93
94
|
"names_artifact",
|
|
94
95
|
"states_remedy"
|
|
95
96
|
],
|
|
96
|
-
"where": "grading.py:
|
|
97
|
+
"where": "grading.py:57"
|
|
97
98
|
},
|
|
98
99
|
{
|
|
99
100
|
"message": "grading_rules.yaml at {} is not valid YAML: {}. Fix it rather than deleting it \u2014 falling back to the",
|
|
@@ -101,7 +102,7 @@
|
|
|
101
102
|
"names_artifact",
|
|
102
103
|
"states_remedy"
|
|
103
104
|
],
|
|
104
|
-
"where": "grading.py:
|
|
105
|
+
"where": "grading.py:59"
|
|
105
106
|
},
|
|
106
107
|
{
|
|
107
108
|
"message": "A hypothesis needs a statement \u2014 what you think happened, in one line. An unnamed hypothesis cannot ",
|
|
@@ -134,7 +135,7 @@
|
|
|
134
135
|
"names_artifact",
|
|
135
136
|
"states_remedy"
|
|
136
137
|
],
|
|
137
|
-
"where": "plan.py:
|
|
138
|
+
"where": "plan.py:84"
|
|
138
139
|
},
|
|
139
140
|
{
|
|
140
141
|
"message": "evidence_sources.yaml at {} is missing 'classes' or 'routing'. An empty catalogue would make every r",
|
|
@@ -166,7 +167,7 @@
|
|
|
166
167
|
"names_artifact",
|
|
167
168
|
"states_remedy"
|
|
168
169
|
],
|
|
169
|
-
"where": "timeline.py:
|
|
170
|
+
"where": "timeline.py:137"
|
|
170
171
|
},
|
|
171
172
|
{
|
|
172
173
|
"message": "Cannot read case.json for {}: {}",
|
|
@@ -174,7 +175,7 @@
|
|
|
174
175
|
"names_artifact",
|
|
175
176
|
"states_remedy"
|
|
176
177
|
],
|
|
177
|
-
"where": "timeline.py:
|
|
178
|
+
"where": "timeline.py:151"
|
|
178
179
|
}
|
|
179
180
|
],
|
|
180
181
|
"per_dimension_pct": {
|
|
@@ -193,20 +194,20 @@
|
|
|
193
194
|
"detail": {
|
|
194
195
|
"heaviest_tools": {
|
|
195
196
|
"case_open": 354,
|
|
196
|
-
"case_plan":
|
|
197
|
+
"case_plan": 395,
|
|
197
198
|
"case_record_gap": 337,
|
|
198
|
-
"case_submit_evidence":
|
|
199
|
-
"incident_timeline":
|
|
199
|
+
"case_submit_evidence": 693,
|
|
200
|
+
"incident_timeline": 357
|
|
200
201
|
},
|
|
201
|
-
"manifest_tokens":
|
|
202
|
-
"mean_tokens_per_tool":
|
|
202
|
+
"manifest_tokens": 3988,
|
|
203
|
+
"mean_tokens_per_tool": 284.9,
|
|
203
204
|
"reference_context": 16384,
|
|
204
|
-
"tool_count":
|
|
205
|
+
"tool_count": 14
|
|
205
206
|
},
|
|
206
207
|
"maximum": 16384,
|
|
207
|
-
"pct":
|
|
208
|
+
"pct": 75.7,
|
|
208
209
|
"unit": "tokens",
|
|
209
|
-
"value":
|
|
210
|
+
"value": 12396
|
|
210
211
|
},
|
|
211
212
|
"parameter_documentation_coverage": {
|
|
212
213
|
"detail": {
|
|
@@ -215,8 +216,7 @@
|
|
|
215
216
|
"case_id"
|
|
216
217
|
],
|
|
217
218
|
"case_submit_evidence": [
|
|
218
|
-
"fetched_at"
|
|
219
|
-
"summary"
|
|
219
|
+
"fetched_at"
|
|
220
220
|
],
|
|
221
221
|
"case_timeline": [
|
|
222
222
|
"bin_seconds",
|
|
@@ -226,22 +226,23 @@
|
|
|
226
226
|
]
|
|
227
227
|
}
|
|
228
228
|
},
|
|
229
|
-
"maximum":
|
|
230
|
-
"pct":
|
|
229
|
+
"maximum": 45,
|
|
230
|
+
"pct": 86.7,
|
|
231
231
|
"unit": "parameters",
|
|
232
|
-
"value":
|
|
232
|
+
"value": 39
|
|
233
233
|
},
|
|
234
234
|
"per_tool_token_discipline": {
|
|
235
235
|
"detail": {
|
|
236
236
|
"ceiling": 350,
|
|
237
237
|
"over_ceiling": {
|
|
238
238
|
"case_open": 354,
|
|
239
|
-
"case_plan":
|
|
240
|
-
"case_submit_evidence":
|
|
239
|
+
"case_plan": 395,
|
|
240
|
+
"case_submit_evidence": 693,
|
|
241
|
+
"incident_timeline": 357
|
|
241
242
|
}
|
|
242
243
|
},
|
|
243
|
-
"maximum":
|
|
244
|
-
"pct":
|
|
244
|
+
"maximum": 14,
|
|
245
|
+
"pct": 71.4,
|
|
245
246
|
"unit": "tools",
|
|
246
247
|
"value": 10
|
|
247
248
|
},
|
|
@@ -263,10 +264,10 @@
|
|
|
263
264
|
"detail": {
|
|
264
265
|
"unmarked": []
|
|
265
266
|
},
|
|
266
|
-
"maximum":
|
|
267
|
+
"maximum": 14,
|
|
267
268
|
"pct": 100.0,
|
|
268
269
|
"unit": "points",
|
|
269
|
-
"value":
|
|
270
|
+
"value": 14
|
|
270
271
|
},
|
|
271
272
|
"remedy_survives_truncation": {
|
|
272
273
|
"detail": {
|
|
@@ -277,10 +278,10 @@
|
|
|
277
278
|
"envelope.py:138",
|
|
278
279
|
"conclusion.py:135",
|
|
279
280
|
"conclusion.py:144",
|
|
280
|
-
"evidence.py:
|
|
281
|
-
"evidence.py:
|
|
282
|
-
"evidence.py:
|
|
283
|
-
"grading.py:
|
|
281
|
+
"evidence.py:179",
|
|
282
|
+
"evidence.py:211",
|
|
283
|
+
"evidence.py:248",
|
|
284
|
+
"grading.py:67",
|
|
284
285
|
"hypotheses.py:47",
|
|
285
286
|
"hypotheses.py:70"
|
|
286
287
|
]
|
|
@@ -300,14 +301,14 @@
|
|
|
300
301
|
"tool_description_quality": {
|
|
301
302
|
"detail": {
|
|
302
303
|
"per_dimension_pct": {
|
|
303
|
-
"args":
|
|
304
|
-
"gotcha":
|
|
304
|
+
"args": 78.6,
|
|
305
|
+
"gotcha": 85.7,
|
|
305
306
|
"marker": 100.0,
|
|
306
|
-
"next_hop":
|
|
307
|
+
"next_hop": 57.1,
|
|
307
308
|
"what": 100.0,
|
|
308
|
-
"when": 92.
|
|
309
|
+
"when": 92.9
|
|
309
310
|
},
|
|
310
|
-
"tools_graded":
|
|
311
|
+
"tools_graded": 14,
|
|
311
312
|
"weakest_tools": {
|
|
312
313
|
"case_close": [
|
|
313
314
|
"next_hop"
|
|
@@ -319,6 +320,9 @@
|
|
|
319
320
|
"args",
|
|
320
321
|
"next_hop"
|
|
321
322
|
],
|
|
323
|
+
"case_knowledge": [
|
|
324
|
+
"next_hop"
|
|
325
|
+
],
|
|
322
326
|
"case_list": [
|
|
323
327
|
"gotcha",
|
|
324
328
|
"next_hop"
|
|
@@ -332,29 +336,26 @@
|
|
|
332
336
|
],
|
|
333
337
|
"case_submit_evidence": [
|
|
334
338
|
"args"
|
|
335
|
-
],
|
|
336
|
-
"case_timeline": [
|
|
337
|
-
"args"
|
|
338
339
|
]
|
|
339
340
|
}
|
|
340
341
|
},
|
|
341
|
-
"maximum":
|
|
342
|
-
"pct": 85.
|
|
342
|
+
"maximum": 84,
|
|
343
|
+
"pct": 85.7,
|
|
343
344
|
"unit": "points",
|
|
344
|
-
"value":
|
|
345
|
+
"value": 72
|
|
345
346
|
},
|
|
346
347
|
"tool_failure_payload_quality": {
|
|
347
348
|
"detail": {
|
|
348
349
|
"bare_string_error_lines": [],
|
|
349
|
-
"carries_hint":
|
|
350
|
-
"dict_shaped":
|
|
351
|
-
"error_return_sites":
|
|
352
|
-
"hint_names_artifact":
|
|
350
|
+
"carries_hint": 13,
|
|
351
|
+
"dict_shaped": 13,
|
|
352
|
+
"error_return_sites": 13,
|
|
353
|
+
"hint_names_artifact": 13
|
|
353
354
|
},
|
|
354
|
-
"maximum":
|
|
355
|
+
"maximum": 39,
|
|
355
356
|
"pct": 100.0,
|
|
356
357
|
"unit": "checks",
|
|
357
|
-
"value":
|
|
358
|
+
"value": 39
|
|
358
359
|
}
|
|
359
360
|
}
|
|
360
361
|
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"""A case opened in Chinese got no identity of its own.
|
|
2
|
+
|
|
3
|
+
``slugify`` reduces on ``[^a-z0-9]+``, so a summary written entirely in Chinese
|
|
4
|
+
leaves nothing behind and the id falls back to the literal word ``case``:
|
|
5
|
+
``20260830-061014-case``. Two completely different investigations opened in the
|
|
6
|
+
same second then collide — and the collision message said they were "the same
|
|
7
|
+
summary", which was false, and sent the reporter looking for a duplicate that
|
|
8
|
+
did not exist.
|
|
9
|
+
|
|
10
|
+
The id has to stay inside its character set: it is a directory name, and
|
|
11
|
+
``validate_case_id`` is the only thing between a model-chosen string and
|
|
12
|
+
``open(root / case_id)``. So the fix is not to widen the alphabet. It is that an
|
|
13
|
+
id must distinguish summaries that differ, and that the collision message must
|
|
14
|
+
describe what actually collided.
|
|
15
|
+
|
|
16
|
+
The control is readability: an ASCII summary must still produce the readable
|
|
17
|
+
slug the listing was designed around, not a digest for everybody.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import pytest
|
|
23
|
+
|
|
24
|
+
from vmware_debug.ops.cases.ids import new_case_id, validate_case_id
|
|
25
|
+
from vmware_debug.ops.cases.model import Scope
|
|
26
|
+
from vmware_debug.ops.cases.store import CaseExists, create_case
|
|
27
|
+
|
|
28
|
+
AT = "2026-08-30T06:10:14Z"
|
|
29
|
+
|
|
30
|
+
#: Both must be free of Latin characters. An earlier draft wrote the first one
|
|
31
|
+
#: as "四台 ESXi 主机…", and "esxi" survived slugify — so the two ids differed
|
|
32
|
+
#: whatever the fallback did, and the test passed against the defect it was
|
|
33
|
+
#: written to catch.
|
|
34
|
+
ZH_A = "四台主机同时进入维护模式后关机"
|
|
35
|
+
ZH_B = "存储集群延迟升高导致虚拟机无响应"
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@pytest.fixture(autouse=True)
|
|
39
|
+
def isolated_home(tmp_path, monkeypatch):
|
|
40
|
+
monkeypatch.setenv("OPS_HOME", str(tmp_path / "vmware"))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class TestANonAsciiSummaryStillGetsAnIdentity:
|
|
44
|
+
def test_two_different_chinese_summaries_do_not_collide(self):
|
|
45
|
+
assert new_case_id(ZH_A, AT) != new_case_id(ZH_B, AT)
|
|
46
|
+
|
|
47
|
+
def test_the_id_is_still_a_safe_directory_name(self):
|
|
48
|
+
validate_case_id(new_case_id(ZH_A, AT))
|
|
49
|
+
|
|
50
|
+
def test_the_same_summary_still_yields_the_same_id(self):
|
|
51
|
+
"""Ids are replayable: a case folder re-opened from its scope has to
|
|
52
|
+
reconstruct the id it already has."""
|
|
53
|
+
assert new_case_id(ZH_A, AT) == new_case_id(ZH_A, AT)
|
|
54
|
+
|
|
55
|
+
def test_both_cases_can_actually_be_opened_in_the_same_second(self):
|
|
56
|
+
create_case(Scope(summary=ZH_A, determined_by="user report"), at=AT)
|
|
57
|
+
create_case(Scope(summary=ZH_B, determined_by="user report"), at=AT)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class TestTheCollisionMessageIsTrue:
|
|
61
|
+
def test_it_does_not_claim_the_summaries_were_identical(self):
|
|
62
|
+
"""Two summaries that differ only outside the id character set still
|
|
63
|
+
collide, and telling the reporter they submitted the same summary sends
|
|
64
|
+
them looking for something that is not there."""
|
|
65
|
+
create_case(Scope(summary="vsan latency!", determined_by="a"), at=AT)
|
|
66
|
+
with pytest.raises(CaseExists) as exc:
|
|
67
|
+
create_case(Scope(summary="vsan latency?", determined_by="a"), at=AT)
|
|
68
|
+
assert "same summary" not in str(exc.value)
|
|
69
|
+
|
|
70
|
+
def test_it_says_what_to_do_about_it(self):
|
|
71
|
+
create_case(Scope(summary="vsan latency", determined_by="a"), at=AT)
|
|
72
|
+
with pytest.raises(CaseExists) as exc:
|
|
73
|
+
create_case(Scope(summary="vsan latency", determined_by="a"), at=AT)
|
|
74
|
+
assert "case_get" in str(exc.value)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class TestControlReadabilityIsNotSacrificed:
|
|
78
|
+
def test_an_ascii_summary_still_gets_its_readable_slug(self):
|
|
79
|
+
"""A digest for every case would pass every test above and destroy the
|
|
80
|
+
one property the slug exists for — a directory listing you can read."""
|
|
81
|
+
assert new_case_id("vSAN datastore latency on cluster-01", AT).endswith(
|
|
82
|
+
"-vsan-datastore-latency-on-cluster-01"
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
def test_a_mixed_summary_keeps_the_part_that_survives(self):
|
|
86
|
+
assert "esxi05" in new_case_id("主机 esxi05 关机", AT)
|