vmware-debug 1.12.1__tar.gz → 1.13.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.
Files changed (96) hide show
  1. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/.claude-plugin/plugin.json +1 -1
  2. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/.mcp.json +1 -1
  3. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/PKG-INFO +2 -2
  4. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/RELEASE_NOTES.md +57 -0
  5. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/openclaw.plugin.json +1 -1
  6. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/package.json +1 -1
  7. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/pyproject.toml +2 -2
  8. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/server.json +2 -2
  9. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/skills/vmware-debug/SKILL.md +31 -6
  10. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/skills/vmware-debug/references/agent-guardrails.md +1 -1
  11. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/skills/vmware-debug/references/setup-guide.md +29 -6
  12. vmware_debug-1.13.0/tests/test_case_grading_per_hypothesis.py +131 -0
  13. vmware_debug-1.13.0/tests/test_mcp_audit.py +274 -0
  14. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/uv.lock +5 -5
  15. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/vmware_debug/__init__.py +1 -1
  16. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/vmware_debug/cli.py +3 -3
  17. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/vmware_debug/mcp_server/server.py +54 -22
  18. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/vmware_debug/ops/cases/api.py +1 -0
  19. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/vmware_debug/ops/cases/grading.py +95 -24
  20. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/.clawhubignore +0 -0
  21. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/.gitignore +0 -0
  22. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/README-CN.md +0 -0
  23. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/README.md +0 -0
  24. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/SECURITY.md +0 -0
  25. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/docs/plans/2026-08-28-investigation-layer-design.md +0 -0
  26. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/skills/vmware-debug/references/capabilities.md +0 -0
  27. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/skills/vmware-debug/references/cli-reference.md +0 -0
  28. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/skills/vmware-debug/references/event-envelope.md +0 -0
  29. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/skills/vmware-debug/references/routing.md +0 -0
  30. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/__init__.py +0 -0
  31. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/conftest.py +0 -0
  32. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/eval/__init__.py +0 -0
  33. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/eval/capability/__init__.py +0 -0
  34. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/eval/capability/_family.py +0 -0
  35. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/eval/capability/_scores.json +0 -0
  36. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/eval/capability/_scoring.py +0 -0
  37. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/eval/capability/_skill.py +0 -0
  38. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/eval/capability/conftest.py +0 -0
  39. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/eval/capability/test_entity_reachability.py +0 -0
  40. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/eval/capability/test_error_actionability.py +0 -0
  41. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/eval/capability/test_tool_description_quality.py +0 -0
  42. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/eval/capability/test_tool_manifest_budget.py +0 -0
  43. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/eval/regression/__init__.py +0 -0
  44. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/eval/regression/_vsphere_event_vocabulary.py +0 -0
  45. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/eval/regression/test_audit_isolation.py +0 -0
  46. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/eval/regression/test_capability_grader.py +0 -0
  47. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/eval/regression/test_case_id_non_ascii.py +0 -0
  48. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/eval/regression/test_case_plan_max_steps.py +0 -0
  49. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/eval/regression/test_classification_against_vsphere_vocabulary.py +0 -0
  50. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/eval/regression/test_debug_regressions.py +0 -0
  51. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/eval/regression/test_environment_resolver_isolation.py +0 -0
  52. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/eval/regression/test_every_tool_parameter_is_documented.py +0 -0
  53. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/eval/regression/test_evidence_payload_contract.py +0 -0
  54. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/eval/regression/test_host_lifecycle_classification.py +0 -0
  55. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/eval/regression/test_investigation_metrics.py +0 -0
  56. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/eval/regression/test_real_alert_titles_classify.py +0 -0
  57. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/eval/regression/test_result_envelope.py +0 -0
  58. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/eval/regression/test_skill_name_aliases.py +0 -0
  59. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/eval/regression/test_spike_resolution.py +0 -0
  60. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/eval/regression/test_tool_annotations.py +0 -0
  61. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/test_case_conclusion.py +0 -0
  62. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/test_case_evidence.py +0 -0
  63. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/test_case_grading.py +0 -0
  64. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/test_case_hypotheses.py +0 -0
  65. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/test_case_ids.py +0 -0
  66. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/test_case_ledger_concurrency.py +0 -0
  67. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/test_case_plan.py +0 -0
  68. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/test_case_readiness.py +0 -0
  69. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/test_case_store.py +0 -0
  70. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/test_case_timeline_close.py +0 -0
  71. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/test_case_tools.py +0 -0
  72. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/test_knowledge.py +0 -0
  73. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/test_safe_error_passthrough.py +0 -0
  74. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/tests/test_timeline.py +0 -0
  75. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/vmware_debug/envelope.py +0 -0
  76. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/vmware_debug/mcp/__init__.py +0 -0
  77. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/vmware_debug/mcp/tools.py +0 -0
  78. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/vmware_debug/mcp_server/__init__.py +0 -0
  79. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/vmware_debug/ops/__init__.py +0 -0
  80. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/vmware_debug/ops/cases/__init__.py +0 -0
  81. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/vmware_debug/ops/cases/conclusion.py +0 -0
  82. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/vmware_debug/ops/cases/evidence.py +0 -0
  83. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/vmware_debug/ops/cases/hypotheses.py +0 -0
  84. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/vmware_debug/ops/cases/ids.py +0 -0
  85. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/vmware_debug/ops/cases/knowledge.py +0 -0
  86. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/vmware_debug/ops/cases/model.py +0 -0
  87. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/vmware_debug/ops/cases/payloads.py +0 -0
  88. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/vmware_debug/ops/cases/plan.py +0 -0
  89. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/vmware_debug/ops/cases/readiness.py +0 -0
  90. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/vmware_debug/ops/cases/sources.py +0 -0
  91. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/vmware_debug/ops/cases/store.py +0 -0
  92. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/vmware_debug/ops/cases/timeline.py +0 -0
  93. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/vmware_debug/ops/skill_names.py +0 -0
  94. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/vmware_debug/ops/timeline.py +0 -0
  95. {vmware_debug-1.12.1 → vmware_debug-1.13.0}/vmware_debug/rules/evidence_sources.yaml +0 -0
  96. {vmware_debug-1.12.1 → vmware_debug-1.13.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.12.1",
5
+ "version": "1.13.0",
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",
@@ -5,7 +5,7 @@
5
5
  "command": "uvx",
6
6
  "args": [
7
7
  "--from",
8
- "vmware-debug==1.12.1",
8
+ "vmware-debug==1.13.0",
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.12.1
3
+ Version: 1.13.0
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.15.0
21
+ Requires-Dist: vmware-policy<2.0,>=1.16.0
22
22
  Description-Content-Type: text/markdown
23
23
 
24
24
  <!-- mcp-name: io.github.vmware-skills/vmware-debug -->
@@ -1,3 +1,60 @@
1
+ ## v1.13.0 — exclusion is per hypothesis
2
+
3
+ Found in a real investigation of an Aria "vCenter app health is affected" alert on 2026-09-15, then corrected
4
+ after an independent review the same day.
5
+
6
+ **Ruling out one hypothesis no longer excludes the case.** Two sources ruled out H2 (host memory reclaim) while the
7
+ leading H1 stayed open behind three gaps, and `case_grade` graded the whole case Excluded — which reads as "the
8
+ answer was ruled out". Now the case is Excluded only when every registered hypothesis is ruled out; otherwise it
9
+ is graded for what remains.
10
+
11
+ * **Only evidence that rules nothing out counts for the hypotheses still open.** Evidence records what it
12
+ falsifies, never what it supports. The first version of this change counted the observations that ruled H2 out
13
+ as corroboration for H1, and a hypothesis added with no evidence at all inherited Probable. Independent sources
14
+ and decisive items for the open hypotheses are now counted only from items with an empty `falsifies`.
15
+ * A gap whose `blocks` lists only ruled-out hypotheses no longer holds the grade back; the reasons list it under
16
+ "No longer blocking" instead of "Blocking gap(s)".
17
+ * **Behaviour change:** `case_grade` returns `hypotheses`, every registered hypothesis as `{id, status}` with
18
+ status `open` or `excluded`. A case directory without a readable `hypotheses.md` is graded as if none were
19
+ registered, with a reason, instead of failing.
20
+ * Re-running the same investigation after the change: H2 and H3 excluded, H1 open, **Probable** from two sources
21
+ that rule nothing out, held below Confirmed by four open gaps.
22
+
23
+ Known and unchanged: excluding a hypothesis still needs two independent sources across the whole case, not two
24
+ that falsify that hypothesis; `case_hypotheses` marks a hypothesis refuted on a single falsifier.
25
+
26
+ ## v1.12.2 — every tool call is audited, failures included
27
+
28
+ Until now no vmware-debug MCP tool wrote to `~/.vmware/audit.db`. The server said that was on
29
+ purpose — nothing here acts on a VMware target — and the case ledger stood in as the record. It
30
+ records the investigation, not the calls: in a live session case evidence was submitted with
31
+ invented fetch times, and nothing could show when the calls had really been made or which failed.
32
+
33
+ * All 14 MCP tools now go through `@vmware_tool`. Each call writes one row under the tool's own
34
+ name; a call that raises or returns `{"error": …}` is recorded as `error`.
35
+ * `payload` (`case_submit_evidence`) and `events` (`incident_timeline`) are kept out of the row.
36
+ The row still has the case id, source skill and tool, query, fetch time and outcome; the data
37
+ itself is in the case folder, or with the caller.
38
+ * The CLI twins audit too: `categories` as `list_symptom_categories`, `triage` as
39
+ `incident_timeline` — including a `triage` that fails on bad input. `version` and `mcp` still
40
+ write no row.
41
+ * **Event text is not copied into the row.** `incident_timeline` and `case_timeline` return event
42
+ text (`hypotheses[].sample_text`, `classification.unmatched_samples`, `rejected`), and a probe
43
+ with `password=…` and `https://user:pass@host` in an event stored both in the row's result. Both
44
+ tools now declare `sensitive_result`: the row keeps the call, arguments and status (a returned
45
+ `{"error": …}` is still `error`) and records the result as `[redacted: return value declared
46
+ sensitive]`. The other tools return ids, counts, grades and text you passed as an argument, and
47
+ are stored after vmware-policy's credential scrubbing.
48
+ * Because the tools now pass through `guard()`, vmware-policy's rules apply to them: a `deny` rule
49
+ naming a debug tool, or with no `operations`, refuses it; a malformed `~/.vmware/rules.yaml`
50
+ refuses every tool (the `categories`/`triage` commands exit 1 with a traceback ending in the
51
+ reason); the runaway guard refuses the 26th identical call within 120 s. It compares a digest of the
52
+ arguments the tool received, not the audit row's redacted copy, so `incident_timeline` calls with
53
+ different events are not identical — this needs vmware-policy 1.16.0, which this release requires.
54
+ Environment-scoped rules still never match (debug has no targets), and maintenance windows never
55
+ gate a debug tool (all are `low` risk). `references/setup-guide.md` said the opposite — that no
56
+ policy rule applies — and is corrected.
57
+
1
58
  ## v1.12.1 — every CLI command declares what it reaches
2
59
 
3
60
  Across the family, CLI reads never wrote `~/.vmware/audit.db`; they now do (`@audited`, under the
@@ -11,7 +11,7 @@
11
11
  "command": "uvx",
12
12
  "args": [
13
13
  "--from",
14
- "vmware-debug==1.12.1",
14
+ "vmware-debug==1.13.0",
15
15
  "vmware-debug-mcp"
16
16
  ]
17
17
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zw008/vmware-debug",
3
- "version": "1.12.1",
3
+ "version": "1.13.0",
4
4
  "description": "VMware incident timeline, root-cause routing, local case ledger; no vSphere access. 14 MCP tools.",
5
5
  "private": true,
6
6
  "license": "MIT",
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "vmware-debug"
7
- version = "1.12.1"
7
+ version = "1.13.0"
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.15.0,<2.0",
24
+ "vmware-policy>=1.16.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.12.1",
10
+ "version": "1.13.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "pypi",
14
14
  "identifier": "vmware-debug",
15
- "version": "1.12.1",
15
+ "version": "1.13.0",
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.12.1
54
+ uv tool install vmware-debug==1.13.0
55
55
  vmware-debug categories # see what it can diagnose
56
56
  ```
57
57
 
@@ -175,7 +175,10 @@ ledger — submit the missing evidence, or record the gap that is blocking it.
175
175
  - **Confirmed** — that, plus a decisive item: a direct hardware diagnostic, a
176
176
  version-checked knowledge-base entry, or a vendor SR; and no gap left open
177
177
  - **Excluded** — an observation that actually rules it out. "We looked and found
178
- nothing" is a gap, not an exclusion
178
+ nothing" is a gap, not an exclusion. Exclusion is per hypothesis: the case is
179
+ Excluded only when every registered hypothesis is ruled out. Otherwise
180
+ `case_grade` grades what remains, counting only evidence that rules nothing
181
+ out, and lists each hypothesis as `open` or `excluded` in `hypotheses`
179
182
 
180
183
  `case_plan` is not a checklist: submit evidence and the next plan is shorter,
181
184
  lose a source and it routes around it. Its `unavailable` half is the important
@@ -275,10 +278,32 @@ No network, nothing executed. The seven [WRITE] tools write only to the local ca
275
278
  ledger under `$OPS_HOME`; nothing here touches a remote VMware estate. Evidence, gaps,
276
279
  hypotheses and grade history are only ever added to — `timeline.md` is regenerated from
277
280
  the evidence and `case.json` holds the current grade and state. Remediation is always routed to
278
- aiops/pilot, where the double-confirm / approval / audit gates live (audit DB
279
- `~/.vmware/audit.db`). debug has no config and no connection, so it registers no
280
- environment resolver, and its tools do not pass through the policy engine — no policy
281
- rule applies to them. See `references/setup-guide.md`.
281
+ aiops/pilot, where the double-confirm / approval gates live.
282
+
283
+ **What the audit row holds.** Every debug tool call — a failed one included — writes one
284
+ row to `~/.vmware/audit.db` through `@vmware_tool`, as do the `categories` and `triage`
285
+ CLI commands under their MCP tools' names. The row records the tool, time, OS user,
286
+ detected agent, arguments, status (`ok`, `error`, `denied`, `budget_exceeded`,
287
+ `interrupted`) and a result. Two arguments are stored as `***`: evidence `payload` and
288
+ `incident_timeline` `events`. Two results are not stored at all — `incident_timeline` and
289
+ `case_timeline` quote event text (`sample_text`, `unmatched_samples`, `rejected`), which
290
+ can carry anything the source logged, so their row says
291
+ `[redacted: return value declared sensitive]`. Every other result (ids, counts, grades,
292
+ your own summaries and statements) is stored after credential scrubbing. The data itself
293
+ is in the case folder, or with you.
294
+
295
+ **What debug inherits from vmware-policy**, since its tools pass through `guard()`:
296
+ - **Runaway guard** — the 26th call to one tool with identical arguments within 120 s is
297
+ refused (`budget_exceeded`; `VMWARE_RUNAWAY_MAX`, `VMWARE_RUNAWAY_WINDOW_SEC`). It
298
+ compares a digest of the arguments the tool received (vmware-policy ≥1.16.0), so
299
+ calls with different `events` are not identical.
300
+ - **A malformed `~/.vmware/rules.yaml` fails closed** — every tool is refused with a reason
301
+ naming the file; `categories`/`triage` exit 1 with a traceback ending in that message.
302
+ - **A `deny` rule without `operations`** matches every tool, debug's included.
303
+ - **Environment-scoped rules never match** — debug registers no resolver (no targets).
304
+ - **Maintenance windows** gate only `high`/`critical` risk; every debug tool is `low`.
305
+
306
+ See `references/setup-guide.md`.
282
307
 
283
308
  **Case data is sensitive and is kept until you delete it.** Each case lives in
284
309
  `$OPS_HOME/cases/<case-id>/` (default `~/.vmware/cases/`), created owner-only (`0700`).
@@ -46,7 +46,7 @@ These are structural, so it cannot.
46
46
  | "Do not fabricate a timeline — build it from the events I gave you" | **`incident_timeline` correlates only its input.** It is source-agnostic and has no way to fetch anything, so the timeline cannot contain an event the agent did not supply. |
47
47
  | "Tell me when the symptom is outside what you can recognise" | **`list_symptom_categories`** states the catalogue, and unmatched symptoms come back as `uncategorized` rather than being forced into the nearest signature. |
48
48
  | "Use explicit limits for queries that may return large amounts of data" | **The list envelope.** `list_symptom_categories` returns `{items, returned, limit, total, truncated, hint}` with `truncated` always `false` — which is the point: it states that the catalogue is complete instead of leaving you to infer it. |
49
- | "Log everything you looked at" | **The case ledger, for investigations.** Evidence submitted to a case is recorded with its source skill, tool, query and fetch time. Debug's own tool calls are not written to `~/.vmware/audit.db`: none of them acts on a VMware target. |
49
+ | "Log everything you looked at" | **`~/.vmware/audit.db` and the case ledger.** Every debug tool call, a failed one included, writes one audit row (tool, time, user, agent, arguments, status and result — evidence `payload` and `events` are stored as `***`, and the results of `incident_timeline` and `case_timeline`, which quote event text, are not stored at all). Evidence submitted to a case is also recorded in the ledger with its source skill, tool, query and fetch time — so pass the real fetch time, never a placeholder. |
50
50
 
51
51
  ---
52
52
 
@@ -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.12.1
9
+ uv tool install vmware-debug==1.13.0
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.12.1", "vmware-debug-mcp"]
18
+ "args": ["--from", "vmware-debug==1.13.0", "vmware-debug-mcp"]
19
19
  }
20
20
  ```
21
21
 
@@ -53,8 +53,31 @@ routes fixes to (vmware-aiops, vmware-pilot).
53
53
  `staging` / `lab`) per target in their own `config.yaml` as an optional label
54
54
  an environment-scoped `deny` rule can match on. debug has no config and no
55
55
  connection to declare one about, so it registers no environment resolver —
56
- it has no basis to answer for any target. Its tools do not pass through the
57
- policy engine either, so no policy rule, environment-scoped or not, applies
58
- to them.
59
- 7. **Static analysis** — `uvx bandit -r vmware_debug/` (release bar:
56
+ it has no basis to answer for any target — and an environment-scoped rule
57
+ never matches its tools. Every other policy rule does apply: each MCP tool
58
+ and the `categories` / `triage` CLI commands pass through vmware-policy's
59
+ `guard()`. Verified behaviour:
60
+ - A `deny` rule naming a debug tool, or with no `operations` key at all,
61
+ refuses it (status `denied`, with the rule's reason).
62
+ - A malformed `~/.vmware/rules.yaml` fails closed: every tool is refused with
63
+ a reason naming the file and the way out (fix it, or
64
+ `VMWARE_POLICY_DISABLED=1`). The `categories` and `triage` commands exit 1
65
+ with a Python traceback whose last lines are that message; `version` and
66
+ `mcp` are unaffected.
67
+ - Runaway guard: within one MCP server process, the 26th call to the same
68
+ tool with identical arguments inside 120 s is refused (`budget_exceeded`).
69
+ Tune with `VMWARE_RUNAWAY_MAX` / `VMWARE_RUNAWAY_WINDOW_SEC`. The guard
70
+ compares a SHA-256 digest of the arguments the tool actually received
71
+ (vmware-policy ≥1.16.0), not the redacted copy in the audit row — so calls
72
+ with different `events` or `payload` are never counted as identical.
73
+ - A `maintenance_window` gates only `high`/`critical` risk operations; every
74
+ debug tool is `low`, so a window never blocks one.
75
+ 7. **Audit row contents** — one row per call in `~/.vmware/audit.db`: tool,
76
+ time, OS user, detected agent, arguments, status and result. `payload` and
77
+ `events` are stored as `***`. The results of `incident_timeline` and
78
+ `case_timeline` are not stored (`[redacted: return value declared
79
+ sensitive]`) because they quote event text; other results are stored after
80
+ credential scrubbing. The status is still truthful — a call that returns
81
+ `{"error": …}` is `error`.
82
+ 8. **Static analysis** — `uvx bandit -r vmware_debug/` (release bar:
60
83
  0 Medium+).
@@ -0,0 +1,131 @@
1
+ """Excluded is a verdict about a hypothesis, not about the whole case.
2
+
3
+ 2026-09-15, a real investigation of a vCenter app-health alert: two sources
4
+ ruled out H2 (host memory reclaim) while the leading H1 stayed open behind three
5
+ gaps. ``case_grade`` graded the whole case Excluded, which reads as "the answer
6
+ was ruled out". The investigator had to explain in prose that only H2 was.
7
+
8
+ The first version of the fix over-corrected, and an independent review caught
9
+ it: the observations that ruled H2 out were then counted as corroboration for
10
+ H1, and a hypothesis added with no evidence at all inherited Probable. Evidence
11
+ records only what it falsifies, never what it supports, so an item that rules a
12
+ hypothesis out must not count for the ones left open.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import pytest
18
+
19
+ from vmware_debug.ops.cases.api import grade
20
+ from vmware_debug.ops.cases.evidence import Evidence, Gap, record_evidence, record_gap
21
+ from vmware_debug.ops.cases.grading import grade_case
22
+ from vmware_debug.ops.cases.hypotheses import add_hypothesis
23
+ from vmware_debug.ops.cases.model import Scope
24
+ from vmware_debug.ops.cases.store import create_case
25
+
26
+ AT = "2026-09-15T13:30:00Z"
27
+
28
+
29
+ @pytest.fixture(autouse=True)
30
+ def isolated_home(tmp_path, monkeypatch):
31
+ monkeypatch.setenv("OPS_HOME", str(tmp_path / "vmware"))
32
+
33
+
34
+ @pytest.fixture
35
+ def two_hypotheses():
36
+ cid = create_case(
37
+ Scope(
38
+ summary="vcenter app health",
39
+ determined_by="aria alert",
40
+ product_versions={"vsphere": "8.0.3"},
41
+ ),
42
+ at=AT,
43
+ ).case_id
44
+ add_hypothesis(cid, "appliance memory pressure") # H1
45
+ add_hypothesis(cid, "host reclaims appliance memory") # H2
46
+ return cid
47
+
48
+
49
+ def _evidence(skill: str, falsifies: tuple[str, ...] = ()) -> Evidence:
50
+ return Evidence(
51
+ source_skill=skill,
52
+ source_tool="t",
53
+ query={},
54
+ fetched_at="2026-09-15T13:31:00Z",
55
+ summary="s",
56
+ falsifies=falsifies,
57
+ )
58
+
59
+
60
+ def _gap(blocks: tuple[str, ...], could_falsify: bool = True) -> Gap:
61
+ return Gap(
62
+ what="swap history",
63
+ why="not collected",
64
+ blocks=blocks,
65
+ could_falsify=could_falsify,
66
+ how_to_close="vami",
67
+ )
68
+
69
+
70
+ def test_ruling_out_one_hypothesis_does_not_exclude_the_case(two_hypotheses):
71
+ record_evidence(two_hypotheses, _evidence("vmware-aria", falsifies=("H2",)))
72
+ record_evidence(two_hypotheses, _evidence("vmware-monitor", falsifies=("H2",)))
73
+ r = grade_case(two_hypotheses)
74
+ assert r.grade == "candidate"
75
+ assert r.hypotheses == (("H1", "open"), ("H2", "excluded"))
76
+ assert any("Still open: H1" in reason for reason in r.reasons)
77
+
78
+
79
+ def test_the_case_is_excluded_when_every_hypothesis_is_ruled_out(two_hypotheses):
80
+ record_evidence(two_hypotheses, _evidence("vmware-aria", falsifies=("H1", "H2")))
81
+ record_evidence(two_hypotheses, _evidence("vmware-monitor"))
82
+ assert grade_case(two_hypotheses).grade == "excluded"
83
+
84
+
85
+ def test_evidence_that_rules_one_out_is_not_corroboration_for_the_rest(two_hypotheses):
86
+ """aria rules H2 out; monitor says something unrelated. Nothing is about H1."""
87
+ record_evidence(two_hypotheses, _evidence("vmware-aria", falsifies=("H2",)))
88
+ record_evidence(two_hypotheses, _evidence("vmware-monitor"))
89
+ assert grade_case(two_hypotheses).grade == "candidate"
90
+
91
+
92
+ def test_a_new_hypothesis_does_not_inherit_probable(two_hypotheses):
93
+ record_evidence(two_hypotheses, _evidence("vmware-aria", falsifies=("H1", "H2")))
94
+ record_evidence(two_hypotheses, _evidence("vmware-monitor", falsifies=("H1", "H2")))
95
+ assert grade_case(two_hypotheses).grade == "excluded"
96
+ add_hypothesis(two_hypotheses, "something else") # H3, no evidence at all
97
+ r = grade_case(two_hypotheses)
98
+ assert r.grade == "candidate"
99
+ assert ("H3", "open") in r.hypotheses
100
+
101
+
102
+ def test_a_gap_that_blocks_only_a_ruled_out_hypothesis_holds_nothing_back(two_hypotheses):
103
+ """Otherwise a question nobody needs answered any more pins H1 at Candidate."""
104
+ record_evidence(two_hypotheses, _evidence("vmware-aria", falsifies=("H2",)))
105
+ record_evidence(two_hypotheses, _evidence("vmware-monitor", falsifies=("H2",)))
106
+ record_evidence(two_hypotheses, _evidence("vmware-monitor"))
107
+ record_evidence(two_hypotheses, _evidence("vmware-log-insight"))
108
+ record_gap(two_hypotheses, _gap(blocks=("H2",)))
109
+ r = grade_case(two_hypotheses)
110
+ assert r.grade == "probable"
111
+ assert not any(reason.startswith("Blocking gap(s)") for reason in r.reasons)
112
+ assert any("No longer blocking" in reason for reason in r.reasons)
113
+
114
+
115
+ def test_a_gap_that_still_blocks_an_open_hypothesis_still_holds(two_hypotheses):
116
+ record_evidence(two_hypotheses, _evidence("vmware-aria", falsifies=("H2",)))
117
+ record_evidence(two_hypotheses, _evidence("vmware-monitor", falsifies=("H2",)))
118
+ record_evidence(two_hypotheses, _evidence("vmware-monitor"))
119
+ record_evidence(two_hypotheses, _evidence("vmware-log-insight"))
120
+ record_gap(two_hypotheses, _gap(blocks=("H1", "H2")))
121
+ assert grade_case(two_hypotheses).grade == "candidate"
122
+
123
+
124
+ def test_the_tool_result_lists_each_hypothesis(two_hypotheses):
125
+ record_evidence(two_hypotheses, _evidence("vmware-aria", falsifies=("H2",)))
126
+ record_evidence(two_hypotheses, _evidence("vmware-monitor"))
127
+ out = grade(two_hypotheses, at=AT)
128
+ assert out["hypotheses"] == [
129
+ {"id": "H1", "status": "open"},
130
+ {"id": "H2", "status": "excluded"},
131
+ ]