vmware-debug 1.12.1__tar.gz → 1.12.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/.claude-plugin/plugin.json +1 -1
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/.mcp.json +1 -1
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/PKG-INFO +2 -2
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/RELEASE_NOTES.md +32 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/openclaw.plugin.json +1 -1
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/package.json +1 -1
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/pyproject.toml +2 -2
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/server.json +2 -2
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/skills/vmware-debug/SKILL.md +27 -5
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/skills/vmware-debug/references/agent-guardrails.md +1 -1
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/skills/vmware-debug/references/setup-guide.md +29 -6
- vmware_debug-1.12.2/tests/test_mcp_audit.py +274 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/uv.lock +5 -5
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/vmware_debug/__init__.py +1 -1
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/vmware_debug/cli.py +3 -3
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/vmware_debug/mcp_server/server.py +45 -19
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/.clawhubignore +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/.gitignore +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/README-CN.md +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/README.md +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/SECURITY.md +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/docs/plans/2026-08-28-investigation-layer-design.md +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/skills/vmware-debug/references/capabilities.md +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/skills/vmware-debug/references/cli-reference.md +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/skills/vmware-debug/references/event-envelope.md +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/skills/vmware-debug/references/routing.md +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/__init__.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/conftest.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/__init__.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/capability/__init__.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/capability/_family.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/capability/_scores.json +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/capability/_scoring.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/capability/_skill.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/capability/conftest.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/capability/test_entity_reachability.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/capability/test_error_actionability.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/capability/test_tool_description_quality.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/capability/test_tool_manifest_budget.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/regression/__init__.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/regression/_vsphere_event_vocabulary.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/regression/test_audit_isolation.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/regression/test_capability_grader.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/regression/test_case_id_non_ascii.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/regression/test_case_plan_max_steps.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/regression/test_classification_against_vsphere_vocabulary.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/regression/test_debug_regressions.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/regression/test_environment_resolver_isolation.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/regression/test_every_tool_parameter_is_documented.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/regression/test_evidence_payload_contract.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/regression/test_host_lifecycle_classification.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/regression/test_investigation_metrics.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/regression/test_real_alert_titles_classify.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/regression/test_result_envelope.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/regression/test_skill_name_aliases.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/regression/test_spike_resolution.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/regression/test_tool_annotations.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/test_case_conclusion.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/test_case_evidence.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/test_case_grading.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/test_case_hypotheses.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/test_case_ids.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/test_case_ledger_concurrency.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/test_case_plan.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/test_case_readiness.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/test_case_store.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/test_case_timeline_close.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/test_case_tools.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/test_knowledge.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/test_safe_error_passthrough.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/test_timeline.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/vmware_debug/envelope.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/vmware_debug/mcp/__init__.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/vmware_debug/mcp/tools.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/vmware_debug/mcp_server/__init__.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/vmware_debug/ops/__init__.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/vmware_debug/ops/cases/__init__.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/vmware_debug/ops/cases/api.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/vmware_debug/ops/cases/conclusion.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/vmware_debug/ops/cases/evidence.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/vmware_debug/ops/cases/grading.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/vmware_debug/ops/cases/hypotheses.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/vmware_debug/ops/cases/ids.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/vmware_debug/ops/cases/knowledge.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/vmware_debug/ops/cases/model.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/vmware_debug/ops/cases/payloads.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/vmware_debug/ops/cases/plan.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/vmware_debug/ops/cases/readiness.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/vmware_debug/ops/cases/sources.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/vmware_debug/ops/cases/store.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/vmware_debug/ops/cases/timeline.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/vmware_debug/ops/skill_names.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/vmware_debug/ops/timeline.py +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/vmware_debug/rules/evidence_sources.yaml +0 -0
- {vmware_debug-1.12.1 → vmware_debug-1.12.2}/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.
|
|
5
|
+
"version": "1.12.2",
|
|
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",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: vmware-debug
|
|
3
|
-
Version: 1.12.
|
|
3
|
+
Version: 1.12.2
|
|
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.
|
|
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,35 @@
|
|
|
1
|
+
## v1.12.2 — every tool call is audited, failures included
|
|
2
|
+
|
|
3
|
+
Until now no vmware-debug MCP tool wrote to `~/.vmware/audit.db`. The server said that was on
|
|
4
|
+
purpose — nothing here acts on a VMware target — and the case ledger stood in as the record. It
|
|
5
|
+
records the investigation, not the calls: in a live session case evidence was submitted with
|
|
6
|
+
invented fetch times, and nothing could show when the calls had really been made or which failed.
|
|
7
|
+
|
|
8
|
+
* All 14 MCP tools now go through `@vmware_tool`. Each call writes one row under the tool's own
|
|
9
|
+
name; a call that raises or returns `{"error": …}` is recorded as `error`.
|
|
10
|
+
* `payload` (`case_submit_evidence`) and `events` (`incident_timeline`) are kept out of the row.
|
|
11
|
+
The row still has the case id, source skill and tool, query, fetch time and outcome; the data
|
|
12
|
+
itself is in the case folder, or with the caller.
|
|
13
|
+
* The CLI twins audit too: `categories` as `list_symptom_categories`, `triage` as
|
|
14
|
+
`incident_timeline` — including a `triage` that fails on bad input. `version` and `mcp` still
|
|
15
|
+
write no row.
|
|
16
|
+
* **Event text is not copied into the row.** `incident_timeline` and `case_timeline` return event
|
|
17
|
+
text (`hypotheses[].sample_text`, `classification.unmatched_samples`, `rejected`), and a probe
|
|
18
|
+
with `password=…` and `https://user:pass@host` in an event stored both in the row's result. Both
|
|
19
|
+
tools now declare `sensitive_result`: the row keeps the call, arguments and status (a returned
|
|
20
|
+
`{"error": …}` is still `error`) and records the result as `[redacted: return value declared
|
|
21
|
+
sensitive]`. The other tools return ids, counts, grades and text you passed as an argument, and
|
|
22
|
+
are stored after vmware-policy's credential scrubbing.
|
|
23
|
+
* Because the tools now pass through `guard()`, vmware-policy's rules apply to them: a `deny` rule
|
|
24
|
+
naming a debug tool, or with no `operations`, refuses it; a malformed `~/.vmware/rules.yaml`
|
|
25
|
+
refuses every tool (the `categories`/`triage` commands exit 1 with a traceback ending in the
|
|
26
|
+
reason); the runaway guard refuses the 26th identical call within 120 s. It compares a digest of the
|
|
27
|
+
arguments the tool received, not the audit row's redacted copy, so `incident_timeline` calls with
|
|
28
|
+
different events are not identical — this needs vmware-policy 1.16.0, which this release requires.
|
|
29
|
+
Environment-scoped rules still never match (debug has no targets), and maintenance windows never
|
|
30
|
+
gate a debug tool (all are `low` risk). `references/setup-guide.md` said the opposite — that no
|
|
31
|
+
policy rule applies — and is corrected.
|
|
32
|
+
|
|
1
33
|
## v1.12.1 — every CLI command declares what it reaches
|
|
2
34
|
|
|
3
35
|
Across the family, CLI reads never wrote `~/.vmware/audit.db`; they now do (`@audited`, under the
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vmware-debug"
|
|
7
|
-
version = "1.12.
|
|
7
|
+
version = "1.12.2"
|
|
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.
|
|
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.
|
|
10
|
+
"version": "1.12.2",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "pypi",
|
|
14
14
|
"identifier": "vmware-debug",
|
|
15
|
-
"version": "1.12.
|
|
15
|
+
"version": "1.12.2",
|
|
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.
|
|
54
|
+
uv tool install vmware-debug==1.12.2
|
|
55
55
|
vmware-debug categories # see what it can diagnose
|
|
56
56
|
```
|
|
57
57
|
|
|
@@ -275,10 +275,32 @@ No network, nothing executed. The seven [WRITE] tools write only to the local ca
|
|
|
275
275
|
ledger under `$OPS_HOME`; nothing here touches a remote VMware estate. Evidence, gaps,
|
|
276
276
|
hypotheses and grade history are only ever added to — `timeline.md` is regenerated from
|
|
277
277
|
the evidence and `case.json` holds the current grade and state. Remediation is always routed to
|
|
278
|
-
aiops/pilot, where the double-confirm / approval
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
278
|
+
aiops/pilot, where the double-confirm / approval gates live.
|
|
279
|
+
|
|
280
|
+
**What the audit row holds.** Every debug tool call — a failed one included — writes one
|
|
281
|
+
row to `~/.vmware/audit.db` through `@vmware_tool`, as do the `categories` and `triage`
|
|
282
|
+
CLI commands under their MCP tools' names. The row records the tool, time, OS user,
|
|
283
|
+
detected agent, arguments, status (`ok`, `error`, `denied`, `budget_exceeded`,
|
|
284
|
+
`interrupted`) and a result. Two arguments are stored as `***`: evidence `payload` and
|
|
285
|
+
`incident_timeline` `events`. Two results are not stored at all — `incident_timeline` and
|
|
286
|
+
`case_timeline` quote event text (`sample_text`, `unmatched_samples`, `rejected`), which
|
|
287
|
+
can carry anything the source logged, so their row says
|
|
288
|
+
`[redacted: return value declared sensitive]`. Every other result (ids, counts, grades,
|
|
289
|
+
your own summaries and statements) is stored after credential scrubbing. The data itself
|
|
290
|
+
is in the case folder, or with you.
|
|
291
|
+
|
|
292
|
+
**What debug inherits from vmware-policy**, since its tools pass through `guard()`:
|
|
293
|
+
- **Runaway guard** — the 26th call to one tool with identical arguments within 120 s is
|
|
294
|
+
refused (`budget_exceeded`; `VMWARE_RUNAWAY_MAX`, `VMWARE_RUNAWAY_WINDOW_SEC`). It
|
|
295
|
+
compares a digest of the arguments the tool received (vmware-policy ≥1.16.0), so
|
|
296
|
+
calls with different `events` are not identical.
|
|
297
|
+
- **A malformed `~/.vmware/rules.yaml` fails closed** — every tool is refused with a reason
|
|
298
|
+
naming the file; `categories`/`triage` exit 1 with a traceback ending in that message.
|
|
299
|
+
- **A `deny` rule without `operations`** matches every tool, debug's included.
|
|
300
|
+
- **Environment-scoped rules never match** — debug registers no resolver (no targets).
|
|
301
|
+
- **Maintenance windows** gate only `high`/`critical` risk; every debug tool is `low`.
|
|
302
|
+
|
|
303
|
+
See `references/setup-guide.md`.
|
|
282
304
|
|
|
283
305
|
**Case data is sensitive and is kept until you delete it.** Each case lives in
|
|
284
306
|
`$OPS_HOME/cases/<case-id>/` (default `~/.vmware/cases/`), created owner-only (`0700`).
|
{vmware_debug-1.12.1 → vmware_debug-1.12.2}/skills/vmware-debug/references/agent-guardrails.md
RENAMED
|
@@ -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" |
|
|
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.
|
|
9
|
+
uv tool install vmware-debug==1.12.2
|
|
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.
|
|
18
|
+
"args": ["--from", "vmware-debug==1.12.2", "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
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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,274 @@
|
|
|
1
|
+
"""Every call to this skill's MCP tools — and every failed call — writes one audit row.
|
|
2
|
+
|
|
3
|
+
Until 2026-09-15 no vmware-debug tool wrote to ``~/.vmware/audit.db``. The server
|
|
4
|
+
said that was deliberate ("nothing in this skill acts on a VMware target"), and
|
|
5
|
+
the case ledger was offered as the record. It is a record of the investigation,
|
|
6
|
+
not of the calls: a live session submitted evidence with invented timestamps,
|
|
7
|
+
and nothing could show when the calls had really been made or which had failed.
|
|
8
|
+
HLD §8.1 / I-10 (amended 2026-09-15b) now require a row for every MCP call,
|
|
9
|
+
local tools included, and for the CLI twins of those tools under the same name.
|
|
10
|
+
|
|
11
|
+
Rows are read back from the sandbox database the engine is bound to, so these
|
|
12
|
+
tests would fail if the call reached the audit engine but not the file.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import asyncio
|
|
18
|
+
import json
|
|
19
|
+
import sqlite3
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
|
|
22
|
+
import pytest
|
|
23
|
+
from typer.testing import CliRunner
|
|
24
|
+
from vmware_policy import get_engine
|
|
25
|
+
from vmware_policy.audit import reset_engine
|
|
26
|
+
|
|
27
|
+
from vmware_debug.cli import app
|
|
28
|
+
from vmware_debug.mcp_server.server import build_server
|
|
29
|
+
|
|
30
|
+
EXPECTED_TOOLS = {
|
|
31
|
+
"incident_timeline",
|
|
32
|
+
"list_symptom_categories",
|
|
33
|
+
"case_open",
|
|
34
|
+
"case_list",
|
|
35
|
+
"case_get",
|
|
36
|
+
"case_submit_evidence",
|
|
37
|
+
"case_record_gap",
|
|
38
|
+
"case_grade",
|
|
39
|
+
"case_readiness",
|
|
40
|
+
"case_plan",
|
|
41
|
+
"case_hypotheses",
|
|
42
|
+
"case_timeline",
|
|
43
|
+
"case_close",
|
|
44
|
+
"case_knowledge",
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
AT = "2026-09-15T05:48:16Z"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@pytest.fixture(autouse=True)
|
|
51
|
+
def isolated_audit(tmp_path, monkeypatch):
|
|
52
|
+
"""A fresh OPS_HOME per test, and an engine bound to it."""
|
|
53
|
+
monkeypatch.setenv("OPS_HOME", str(tmp_path / "vmware"))
|
|
54
|
+
reset_engine()
|
|
55
|
+
yield
|
|
56
|
+
reset_engine()
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@pytest.fixture
|
|
60
|
+
def server():
|
|
61
|
+
return build_server()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def rows(tool: str | None = None) -> list[dict]:
|
|
65
|
+
db = Path(get_engine()._path)
|
|
66
|
+
if not db.exists():
|
|
67
|
+
return []
|
|
68
|
+
with sqlite3.connect(f"file:{db}?mode=ro", uri=True) as con:
|
|
69
|
+
con.row_factory = sqlite3.Row
|
|
70
|
+
query = "SELECT skill, tool, status, params, result FROM audit_log"
|
|
71
|
+
args: tuple = ()
|
|
72
|
+
if tool is not None:
|
|
73
|
+
query += " WHERE tool = ?"
|
|
74
|
+
args = (tool,)
|
|
75
|
+
return [dict(r) for r in con.execute(query + " ORDER BY id", args)]
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def call(server, name: str, arguments: dict):
|
|
79
|
+
return asyncio.run(server.call_tool(name, arguments))
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def test_every_registered_tool_wears_vmware_tool(server) -> None:
|
|
83
|
+
tools = server._tool_manager._tools
|
|
84
|
+
assert set(tools) >= EXPECTED_TOOLS, f"tools missing from the registry: {EXPECTED_TOOLS - set(tools)}"
|
|
85
|
+
unwrapped = sorted(name for name, tool in tools.items() if not getattr(tool.fn, "_is_vmware_tool", False))
|
|
86
|
+
assert not unwrapped, f"MCP tools that write no audit row: {unwrapped}"
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def test_a_successful_call_writes_one_ok_row_under_the_tool_name(server) -> None:
|
|
90
|
+
call(server, "case_open", {"summary": "vsan latency on cluster-01", "determined_by": "alarm 42"})
|
|
91
|
+
|
|
92
|
+
found = rows("case_open")
|
|
93
|
+
assert len(found) == 1, f"expected one case_open row, got {rows()}"
|
|
94
|
+
assert found[0]["skill"] == "debug"
|
|
95
|
+
assert found[0]["status"] == "ok"
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def test_a_failed_call_writes_an_error_row(server) -> None:
|
|
99
|
+
call(server, "case_get", {"case_id": "20990101-000000-no-such-case"})
|
|
100
|
+
|
|
101
|
+
found = rows("case_get")
|
|
102
|
+
assert len(found) == 1, f"a failed call left no row: {rows()}"
|
|
103
|
+
assert found[0]["status"] == "error"
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def test_a_malformed_incident_timeline_call_is_recorded_as_an_error(server) -> None:
|
|
107
|
+
call(server, "incident_timeline", {"events": [{"not": "an event"}]})
|
|
108
|
+
|
|
109
|
+
found = rows("incident_timeline")
|
|
110
|
+
assert len(found) == 1
|
|
111
|
+
assert found[0]["status"] == "error"
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def test_evidence_payload_is_not_copied_into_the_audit_row(server) -> None:
|
|
115
|
+
opened = call(server, "case_open", {"summary": "vcenter memory exhaustion", "determined_by": "alarm"})
|
|
116
|
+
case_id = _structured(opened)["case_id"]
|
|
117
|
+
marker = "PAYLOAD-MARKER-3f9c"
|
|
118
|
+
|
|
119
|
+
call(
|
|
120
|
+
server,
|
|
121
|
+
"case_submit_evidence",
|
|
122
|
+
{
|
|
123
|
+
"case_id": case_id,
|
|
124
|
+
"source_skill": "vmware-monitor",
|
|
125
|
+
"source_tool": "get_events",
|
|
126
|
+
"summary": "one event",
|
|
127
|
+
"fetched_at": AT,
|
|
128
|
+
"query": {"target": "home-vcenter", "hours": 1},
|
|
129
|
+
"payload": [{"time": AT, "message": marker}],
|
|
130
|
+
},
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
found = rows("case_submit_evidence")
|
|
134
|
+
assert len(found) == 1
|
|
135
|
+
assert found[0]["status"] == "ok"
|
|
136
|
+
assert marker not in found[0]["params"], "the raw payload was duplicated into audit.db"
|
|
137
|
+
params = json.loads(found[0]["params"])
|
|
138
|
+
assert params["case_id"] == case_id
|
|
139
|
+
assert params["source_tool"] == "get_events"
|
|
140
|
+
assert params["fetched_at"] == AT
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def test_cli_categories_is_audited_under_its_mcp_twin_name() -> None:
|
|
144
|
+
result = CliRunner().invoke(app, ["categories"])
|
|
145
|
+
|
|
146
|
+
assert result.exit_code == 0, result.output
|
|
147
|
+
found = rows("list_symptom_categories")
|
|
148
|
+
assert len(found) == 1 and found[0]["status"] == "ok", rows()
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_cli_triage_is_audited_under_its_mcp_twin_name(tmp_path) -> None:
|
|
152
|
+
events = tmp_path / "events.json"
|
|
153
|
+
events.write_text(
|
|
154
|
+
json.dumps([{"ts": AT, "source": "vcenter", "severity": "warning", "entity": "vcsa", "text": "memory"}]),
|
|
155
|
+
encoding="utf-8",
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
result = CliRunner().invoke(app, ["triage", "--events", str(events)])
|
|
159
|
+
|
|
160
|
+
assert result.exit_code == 0, result.output
|
|
161
|
+
found = rows("incident_timeline")
|
|
162
|
+
assert len(found) == 1 and found[0]["status"] == "ok", rows()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def test_a_failed_cli_triage_is_recorded_as_an_error(tmp_path) -> None:
|
|
166
|
+
bad = tmp_path / "events.json"
|
|
167
|
+
bad.write_text("{not json", encoding="utf-8")
|
|
168
|
+
|
|
169
|
+
result = CliRunner().invoke(app, ["triage", "--events", str(bad)])
|
|
170
|
+
|
|
171
|
+
assert result.exit_code != 0
|
|
172
|
+
found = rows("incident_timeline")
|
|
173
|
+
assert len(found) == 1, f"a failed triage left no row: {rows()}"
|
|
174
|
+
assert found[0]["status"] == "error"
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def test_version_writes_no_row() -> None:
|
|
178
|
+
result = CliRunner().invoke(app, ["version"])
|
|
179
|
+
|
|
180
|
+
assert result.exit_code == 0
|
|
181
|
+
assert rows() == []
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def _structured(result) -> dict:
|
|
185
|
+
"""FastMCP returns (content, structured) or a content list depending on version."""
|
|
186
|
+
if isinstance(result, tuple):
|
|
187
|
+
content, structured = result
|
|
188
|
+
if isinstance(structured, dict):
|
|
189
|
+
return structured.get("result", structured)
|
|
190
|
+
result = content
|
|
191
|
+
return json.loads("".join(getattr(c, "text", "") for c in result))
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# ── Result bodies that carry event text are not copied (review D3) ──────────
|
|
195
|
+
|
|
196
|
+
REDACTED_RESULT = json.dumps("[redacted: return value declared sensitive]")
|
|
197
|
+
EVENT_MARKER = "EVENT-TEXT-MARKER-7d21"
|
|
198
|
+
|
|
199
|
+
#: Tools whose result quotes event text the caller fetched from another system —
|
|
200
|
+
#: vCenter event messages, log lines — which can carry anything, credentials
|
|
201
|
+
#: included. incident_timeline returns it as hypotheses[].sample_text and
|
|
202
|
+
#: classification.unmatched_samples; case_timeline returns the same structure
|
|
203
|
+
#: rebuilt from the ledger, plus `rejected` entries that quote the offending row.
|
|
204
|
+
#: Every other tool returns ids, counts, grades and text the caller wrote as an
|
|
205
|
+
#: argument (a summary, a hypothesis statement, a gap's how_to_close), which the
|
|
206
|
+
#: row already holds in `params`.
|
|
207
|
+
SENSITIVE_RESULT_TOOLS = {"incident_timeline", "case_timeline"}
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def _events(text: str) -> list[dict]:
|
|
211
|
+
return [{"ts": AT, "source": "vcenter", "severity": "error", "entity": "vc01", "text": text}]
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def test_only_tools_that_return_event_text_declare_a_sensitive_result(server) -> None:
|
|
215
|
+
declared = {
|
|
216
|
+
name for name, tool in server._tool_manager._tools.items() if getattr(tool.fn, "_sensitive_result", False)
|
|
217
|
+
}
|
|
218
|
+
assert declared == SENSITIVE_RESULT_TOOLS
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def test_incident_timeline_result_body_is_not_copied(server) -> None:
|
|
222
|
+
returned = _structured(call(server, "incident_timeline", {"events": _events(f"{EVENT_MARKER} login failed")}))
|
|
223
|
+
|
|
224
|
+
assert any(EVENT_MARKER in h["sample_text"] for h in returned["hypotheses"]), "the caller must get the real result"
|
|
225
|
+
found = rows("incident_timeline")
|
|
226
|
+
assert len(found) == 1
|
|
227
|
+
assert found[0]["status"] == "ok"
|
|
228
|
+
assert EVENT_MARKER not in found[0]["result"], "event text was copied into audit.db"
|
|
229
|
+
assert found[0]["result"] == REDACTED_RESULT
|
|
230
|
+
assert json.loads(found[0]["params"])["top_n"] == 5
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def test_incident_timeline_returned_error_is_still_recorded_as_error(server) -> None:
|
|
234
|
+
call(server, "incident_timeline", {"events": [{"text": EVENT_MARKER}]})
|
|
235
|
+
|
|
236
|
+
found = rows("incident_timeline")
|
|
237
|
+
assert len(found) == 1
|
|
238
|
+
assert found[0]["status"] == "error", "declaring the result sensitive hid the failure"
|
|
239
|
+
assert EVENT_MARKER not in found[0]["result"]
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def test_case_timeline_result_body_is_not_copied(server) -> None:
|
|
243
|
+
case_id = _structured(call(server, "case_open", {"summary": "auth failures on vc01", "determined_by": "alarm"}))[
|
|
244
|
+
"case_id"
|
|
245
|
+
]
|
|
246
|
+
call(
|
|
247
|
+
server,
|
|
248
|
+
"case_submit_evidence",
|
|
249
|
+
{
|
|
250
|
+
"case_id": case_id,
|
|
251
|
+
"source_skill": "vmware-monitor",
|
|
252
|
+
"source_tool": "get_events",
|
|
253
|
+
"summary": "one event",
|
|
254
|
+
"fetched_at": AT,
|
|
255
|
+
"payload": _events(f"{EVENT_MARKER} login failed"),
|
|
256
|
+
},
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
returned = _structured(call(server, "case_timeline", {"case_id": case_id}))
|
|
260
|
+
|
|
261
|
+
assert returned["event_count"] == 1
|
|
262
|
+
found = rows("case_timeline")
|
|
263
|
+
assert len(found) == 1
|
|
264
|
+
assert found[0]["status"] == "ok"
|
|
265
|
+
assert EVENT_MARKER not in found[0]["result"], "event text was copied into audit.db"
|
|
266
|
+
assert json.loads(found[0]["params"])["case_id"] == case_id
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def test_case_timeline_returned_error_is_still_recorded_as_error(server) -> None:
|
|
270
|
+
call(server, "case_timeline", {"case_id": "20990101-000000-no-such-case"})
|
|
271
|
+
|
|
272
|
+
found = rows("case_timeline")
|
|
273
|
+
assert len(found) == 1
|
|
274
|
+
assert found[0]["status"] == "error"
|
|
@@ -1293,7 +1293,7 @@ wheels = [
|
|
|
1293
1293
|
|
|
1294
1294
|
[[package]]
|
|
1295
1295
|
name = "vmware-debug"
|
|
1296
|
-
version = "1.12.
|
|
1296
|
+
version = "1.12.2"
|
|
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.
|
|
1319
|
+
{ name = "vmware-policy", specifier = ">=1.16.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.
|
|
1331
|
+
version = "1.16.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/
|
|
1338
|
+
sdist = { url = "https://files.pythonhosted.org/packages/6c/b2/489ce2ab84e7d365c2a32ca37598766a78f998865a9c267d588e638217d7/vmware_policy-1.16.0.tar.gz", hash = "sha256:cbb9f387f5fa6aba749f5c9abfdf8678ad9deed1f8272bbf39a0000f6e54a9d5", size = 194662, upload-time = "2026-09-15T08:49:31.863Z" }
|
|
1339
1339
|
wheels = [
|
|
1340
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
1340
|
+
{ url = "https://files.pythonhosted.org/packages/29/50/fa251d9560db49d606f53ea84e05db69db3237d68fdcf3bf68d2ce9dcf97/vmware_policy-1.16.0-py3-none-any.whl", hash = "sha256:0a35621fece1aeaea70157f0d360c7ac0ecd582bd2aaba38c69f32e4eeab415f", size = 73037, upload-time = "2026-09-15T08:49:30.252Z" },
|
|
1341
1341
|
]
|
|
@@ -20,7 +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
|
+
from vmware_policy import audited, cli_local
|
|
24
24
|
|
|
25
25
|
|
|
26
26
|
def _harden_console_encoding() -> None:
|
|
@@ -61,7 +61,7 @@ def version() -> None:
|
|
|
61
61
|
|
|
62
62
|
|
|
63
63
|
@app.command()
|
|
64
|
-
@
|
|
64
|
+
@audited(tool="list_symptom_categories")
|
|
65
65
|
def categories() -> None:
|
|
66
66
|
"""List the symptom categories debug recognises and what to check for each."""
|
|
67
67
|
table = Table(title="vmware-debug symptom categories")
|
|
@@ -74,7 +74,7 @@ def categories() -> None:
|
|
|
74
74
|
|
|
75
75
|
|
|
76
76
|
@app.command()
|
|
77
|
-
@
|
|
77
|
+
@audited(tool="incident_timeline")
|
|
78
78
|
def triage(
|
|
79
79
|
events_file: Optional[Path] = typer.Option(
|
|
80
80
|
None, "--events", "-e", help="JSON file of event envelopes; reads stdin if omitted."
|
|
@@ -4,10 +4,22 @@ Tool logic lives in vmware_debug.mcp.tools (the case tools' bodies in
|
|
|
4
4
|
ops/cases/api.py). This module wires them into a FastMCP server and provides
|
|
5
5
|
the stdio entry point.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
Every tool here goes through vmware_policy's ``@vmware_tool``, so each call —
|
|
8
|
+
a failed one included — writes one row to ~/.vmware/audit.db (HLD §8.1, I-10,
|
|
9
|
+
amended 2026-09-15b). Until then none did: this module said no audit was needed
|
|
10
|
+
because nothing here acts on a VMware target, and the case ledger was offered
|
|
11
|
+
as the record. The ledger records the investigation, not the calls — a live
|
|
12
|
+
session submitted evidence with invented fetch times and nothing could show
|
|
13
|
+
when the calls had really been made or which had failed. Large data the caller
|
|
14
|
+
hands over (``payload``, ``events``) is kept out of the row via
|
|
15
|
+
``sensitive_params``; it lives in the ledger or with the caller. The two tools
|
|
16
|
+
whose *result* quotes that event text — ``incident_timeline`` and
|
|
17
|
+
``case_timeline`` (``hypotheses[].sample_text``, ``unmatched_samples``,
|
|
18
|
+
``rejected``) — declare ``sensitive_result``, so their row keeps the call, the
|
|
19
|
+
arguments and the status but not the result body: event text fetched from
|
|
20
|
+
vCenter or a log can carry anything, credentials included (review D3,
|
|
21
|
+
2026-09-15). The other tools return ids, counts, grades and text the caller
|
|
22
|
+
wrote as an argument, which the row already holds.
|
|
11
23
|
|
|
12
24
|
Note: signatures here use typing.Optional, never PEP 604 ``X | None`` — FastMCP
|
|
13
25
|
reflects these at registration and ``X | None`` crashes on Python 3.10 + older
|
|
@@ -19,7 +31,7 @@ import sys
|
|
|
19
31
|
from typing import Optional, Union
|
|
20
32
|
|
|
21
33
|
from mcp.server.fastmcp import FastMCP
|
|
22
|
-
from vmware_policy import describe_tool_parameters, sanitize
|
|
34
|
+
from vmware_policy import describe_tool_parameters, sanitize, vmware_tool
|
|
23
35
|
|
|
24
36
|
from vmware_debug.mcp import tools as t
|
|
25
37
|
from vmware_debug import __version__
|
|
@@ -201,7 +213,8 @@ def build_server() -> FastMCP:
|
|
|
201
213
|
# bin_seconds and z_threshold are exactly the two knobs a wrong guess makes
|
|
202
214
|
# silently useless.
|
|
203
215
|
@server.tool(name="incident_timeline", annotations=_READ)
|
|
204
|
-
|
|
216
|
+
@vmware_tool(risk_level="low", sensitive_params=["events"], sensitive_result=True)
|
|
217
|
+
def incident_timeline(
|
|
205
218
|
events: list[dict],
|
|
206
219
|
bin_seconds: Optional[float] = None,
|
|
207
220
|
z_threshold: float = 2.0,
|
|
@@ -262,7 +275,8 @@ def build_server() -> FastMCP:
|
|
|
262
275
|
}
|
|
263
276
|
|
|
264
277
|
@server.tool(name="list_symptom_categories", annotations=_READ)
|
|
265
|
-
|
|
278
|
+
@vmware_tool(risk_level="low")
|
|
279
|
+
def list_symptom_categories() -> dict:
|
|
266
280
|
"""[READ] List the symptom categories vmware-debug recognises, each with
|
|
267
281
|
example keywords and a suggested next check (which skill/tool to run).
|
|
268
282
|
Takes no parameters. Use this when you don't yet know what to look
|
|
@@ -287,7 +301,8 @@ def build_server() -> FastMCP:
|
|
|
287
301
|
return {"error": _safe_error(exc, tool), "hint": _CASE_ERROR_HINT}
|
|
288
302
|
|
|
289
303
|
@server.tool(name="case_open", annotations=_WRITE_LOCAL)
|
|
290
|
-
|
|
304
|
+
@vmware_tool(risk_level="low")
|
|
305
|
+
def case_open(
|
|
291
306
|
summary: str,
|
|
292
307
|
determined_by: str,
|
|
293
308
|
objects: Optional[list[str]] = None,
|
|
@@ -339,7 +354,8 @@ def build_server() -> FastMCP:
|
|
|
339
354
|
return _case_error(exc, "case_open")
|
|
340
355
|
|
|
341
356
|
@server.tool(name="case_list", annotations=_READ)
|
|
342
|
-
|
|
357
|
+
@vmware_tool(risk_level="low")
|
|
358
|
+
def case_list(limit: int = 50) -> dict:
|
|
343
359
|
"""[READ] List investigation cases, newest first.
|
|
344
360
|
|
|
345
361
|
WHEN: to find the id of a case you or someone else opened earlier.
|
|
@@ -359,7 +375,8 @@ def build_server() -> FastMCP:
|
|
|
359
375
|
return _case_error(exc, "case_list")
|
|
360
376
|
|
|
361
377
|
@server.tool(name="case_get", annotations=_READ)
|
|
362
|
-
|
|
378
|
+
@vmware_tool(risk_level="low")
|
|
379
|
+
def case_get(case_id: str) -> dict:
|
|
363
380
|
"""[READ] One case: its scope, its ledger sizes, and its grade history.
|
|
364
381
|
|
|
365
382
|
WHEN: to pick up an investigation, or to see why a case sits at the
|
|
@@ -392,7 +409,8 @@ def build_server() -> FastMCP:
|
|
|
392
409
|
# what a model fills with invention. Trim the prose here and the cost stays
|
|
393
410
|
# while the explanation goes.
|
|
394
411
|
@server.tool(name="case_submit_evidence", annotations=_WRITE_LOCAL)
|
|
395
|
-
|
|
412
|
+
@vmware_tool(risk_level="low", sensitive_params=["payload"])
|
|
413
|
+
def case_submit_evidence(
|
|
396
414
|
case_id: str,
|
|
397
415
|
source_skill: str,
|
|
398
416
|
source_tool: str,
|
|
@@ -485,7 +503,8 @@ def build_server() -> FastMCP:
|
|
|
485
503
|
return _case_error(exc, "case_submit_evidence")
|
|
486
504
|
|
|
487
505
|
@server.tool(name="case_record_gap", annotations=_WRITE_LOCAL)
|
|
488
|
-
|
|
506
|
+
@vmware_tool(risk_level="low")
|
|
507
|
+
def case_record_gap(
|
|
489
508
|
case_id: str,
|
|
490
509
|
what: str,
|
|
491
510
|
why: str,
|
|
@@ -535,7 +554,8 @@ def build_server() -> FastMCP:
|
|
|
535
554
|
return _case_error(exc, "case_record_gap")
|
|
536
555
|
|
|
537
556
|
@server.tool(name="case_grade", annotations=_WRITE_LOCAL)
|
|
538
|
-
|
|
557
|
+
@vmware_tool(risk_level="low")
|
|
558
|
+
def case_grade(case_id: str) -> dict:
|
|
539
559
|
"""[WRITE] Compute and record the conclusion grade — steps 07/08.
|
|
540
560
|
|
|
541
561
|
WHEN: when you think the investigation has reached a conclusion, or to
|
|
@@ -574,7 +594,8 @@ def build_server() -> FastMCP:
|
|
|
574
594
|
return _case_error(exc, "case_grade")
|
|
575
595
|
|
|
576
596
|
@server.tool(name="case_readiness", annotations=_READ)
|
|
577
|
-
|
|
597
|
+
@vmware_tool(risk_level="low")
|
|
598
|
+
def case_readiness(available_skills: Optional[list[str]] = None) -> dict:
|
|
578
599
|
"""[READ] What strength of conclusion can this environment reach?
|
|
579
600
|
|
|
580
601
|
WHEN: before starting an investigation, or when a case will not go
|
|
@@ -614,7 +635,8 @@ def build_server() -> FastMCP:
|
|
|
614
635
|
return _case_error(exc, "case_readiness")
|
|
615
636
|
|
|
616
637
|
@server.tool(name="case_plan", annotations=_READ)
|
|
617
|
-
|
|
638
|
+
@vmware_tool(risk_level="low")
|
|
639
|
+
def case_plan(
|
|
618
640
|
case_id: str,
|
|
619
641
|
category: Optional[str] = None,
|
|
620
642
|
available_skills: Optional[list[str]] = None,
|
|
@@ -675,7 +697,8 @@ def build_server() -> FastMCP:
|
|
|
675
697
|
return _case_error(exc, "case_plan")
|
|
676
698
|
|
|
677
699
|
@server.tool(name="case_hypotheses", annotations=_WRITE_LOCAL)
|
|
678
|
-
|
|
700
|
+
@vmware_tool(risk_level="low")
|
|
701
|
+
def case_hypotheses(case_id: str, statement: Optional[str] = None) -> dict:
|
|
679
702
|
"""[WRITE] Register a candidate explanation, or read the ledger — step 06.
|
|
680
703
|
|
|
681
704
|
WHEN: as soon as you have a theory worth testing, and again to see where
|
|
@@ -712,7 +735,8 @@ def build_server() -> FastMCP:
|
|
|
712
735
|
return _case_error(exc, "case_hypotheses")
|
|
713
736
|
|
|
714
737
|
@server.tool(name="case_timeline", annotations=_WRITE_LOCAL)
|
|
715
|
-
|
|
738
|
+
@vmware_tool(risk_level="low", sensitive_result=True)
|
|
739
|
+
def case_timeline(
|
|
716
740
|
case_id: str,
|
|
717
741
|
bin_seconds: Optional[float] = None,
|
|
718
742
|
z_threshold: float = 2.0,
|
|
@@ -762,7 +786,8 @@ def build_server() -> FastMCP:
|
|
|
762
786
|
return _case_error(exc, "case_timeline")
|
|
763
787
|
|
|
764
788
|
@server.tool(name="case_close", annotations=_WRITE_LOCAL)
|
|
765
|
-
|
|
789
|
+
@vmware_tool(risk_level="low")
|
|
790
|
+
def case_close(case_id: str) -> dict:
|
|
766
791
|
"""[WRITE] Record the final grade and archive the case — step 08.
|
|
767
792
|
|
|
768
793
|
WHEN: when the investigation is finished, or is being handed over.
|
|
@@ -788,7 +813,8 @@ def build_server() -> FastMCP:
|
|
|
788
813
|
return _case_error(exc, "case_close")
|
|
789
814
|
|
|
790
815
|
@server.tool(name="case_knowledge", annotations=_READ)
|
|
791
|
-
|
|
816
|
+
@vmware_tool(risk_level="low")
|
|
817
|
+
def case_knowledge(case_id: Optional[str] = None) -> dict:
|
|
792
818
|
"""[READ] What the knowledge layer accepts, and what is mounted.
|
|
793
819
|
|
|
794
820
|
WHEN: when someone asks what can be added to make conclusions stronger,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{vmware_debug-1.12.1 → vmware_debug-1.12.2}/docs/plans/2026-08-28-investigation-layer-design.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{vmware_debug-1.12.1 → vmware_debug-1.12.2}/skills/vmware-debug/references/event-envelope.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/capability/test_entity_reachability.py
RENAMED
|
File without changes
|
{vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/capability/test_error_actionability.py
RENAMED
|
File without changes
|
{vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/capability/test_tool_description_quality.py
RENAMED
|
File without changes
|
{vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/capability/test_tool_manifest_budget.py
RENAMED
|
File without changes
|
|
File without changes
|
{vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/regression/_vsphere_event_vocabulary.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/regression/test_case_plan_max_steps.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/regression/test_evidence_payload_contract.py
RENAMED
|
File without changes
|
|
File without changes
|
{vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/regression/test_investigation_metrics.py
RENAMED
|
File without changes
|
{vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/regression/test_real_alert_titles_classify.py
RENAMED
|
File without changes
|
|
File without changes
|
{vmware_debug-1.12.1 → vmware_debug-1.12.2}/tests/eval/regression/test_skill_name_aliases.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|