vmware-storage 2.3.0__tar.gz → 2.4.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 (113) hide show
  1. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/.claude-plugin/plugin.json +1 -1
  2. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/.mcp.json +1 -1
  3. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/PKG-INFO +10 -2
  4. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/README-CN.md +7 -0
  5. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/README.md +8 -0
  6. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/RELEASE_NOTES.md +24 -0
  7. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/openclaw.plugin.json +1 -1
  8. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/package.json +1 -1
  9. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/pyproject.toml +2 -2
  10. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/server.json +2 -2
  11. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/skills/vmware-storage/SKILL.md +6 -2
  12. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/skills/vmware-storage/references/setup-guide.md +11 -3
  13. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_atexit_handlers_do_not_accumulate.py +12 -6
  14. vmware_storage-2.4.0/tests/eval/regression/test_login_without_a_role.py +215 -0
  15. vmware_storage-2.4.0/tests/test_policy_roles.py +55 -0
  16. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/uv.lock +5 -5
  17. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/__init__.py +1 -1
  18. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/cli.py +6 -2
  19. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/connection.py +29 -0
  20. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/mcp_server/server.py +40 -12
  21. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/policy_environment.py +4 -0
  22. vmware_storage-2.4.0/vmware_storage/policy_roles.py +52 -0
  23. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/.clawhubignore +0 -0
  24. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/.env.example +0 -0
  25. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/.gitignore +0 -0
  26. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/Dockerfile +0 -0
  27. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/LICENSE +0 -0
  28. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/SECURITY.md +0 -0
  29. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/config.example.yaml +0 -0
  30. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/docker-compose.yml +0 -0
  31. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/examples/mcp-configs/README.md +0 -0
  32. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/examples/mcp-configs/claude-code.json +0 -0
  33. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/examples/mcp-configs/continue.yaml +0 -0
  34. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/examples/mcp-configs/cursor.json +0 -0
  35. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/examples/mcp-configs/goose.json +0 -0
  36. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/examples/mcp-configs/localcowork.json +0 -0
  37. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/examples/mcp-configs/mcp-agent.yaml +0 -0
  38. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/examples/mcp-configs/vscode-copilot.json +0 -0
  39. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/patterns/README.md +0 -0
  40. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/patterns/iscsi-target-stale-rescan.yaml +0 -0
  41. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/skills/vmware-storage/evals/evals.json +0 -0
  42. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/skills/vmware-storage/references/agent-guardrails.md +0 -0
  43. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/skills/vmware-storage/references/capabilities.md +0 -0
  44. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/skills/vmware-storage/references/cli-reference.md +0 -0
  45. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/smithery.yaml +0 -0
  46. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/__init__.py +0 -0
  47. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/conftest.py +0 -0
  48. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/__init__.py +0 -0
  49. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/capability/__init__.py +0 -0
  50. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/capability/_family.py +0 -0
  51. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/capability/_scores.json +0 -0
  52. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/capability/_scoring.py +0 -0
  53. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/capability/_skill.py +0 -0
  54. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/capability/conftest.py +0 -0
  55. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/capability/test_entity_reachability.py +0 -0
  56. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/capability/test_error_actionability.py +0 -0
  57. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/capability/test_tool_description_quality.py +0 -0
  58. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/capability/test_tool_manifest_budget.py +0 -0
  59. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/__init__.py +0 -0
  60. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_audit_isolation.py +0 -0
  61. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_cached_loader_agrees_with_the_resolver.py +0 -0
  62. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_capability_grader.py +0 -0
  63. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_cli_writes_guarded.py +0 -0
  64. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_credential_env_override.py +0 -0
  65. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_datastore_list_renders.py +0 -0
  66. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_doctor_authenticates_every_target.py +0 -0
  67. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_doctor_uses_the_config_the_tools_use.py +0 -0
  68. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_env_secret_obfuscation.py +0 -0
  69. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_fc_multipath.py +0 -0
  70. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_instructions_name_targets.py +0 -0
  71. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_inventory_scale_n1.py +0 -0
  72. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_iscsi_unreachable_host.py +0 -0
  73. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_release_blockers.py +0 -0
  74. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_remedy_survives_a_long_home.py +0 -0
  75. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_result_envelope.py +0 -0
  76. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_returned_failure_is_audited.py +0 -0
  77. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_session_probe_eviction.py +0 -0
  78. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_storage_hardening_fixes.py +0 -0
  79. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_storage_ops_behavior.py +0 -0
  80. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_tool_names_2_0.py +0 -0
  81. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_vim_attribute_conformance.py +0 -0
  82. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_vsan_efficiency.py +0 -0
  83. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_vsan_health_is_actually_queried.py +0 -0
  84. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/regression/test_vsan_reports_what_it_could_not_read.py +0 -0
  85. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/spec/__init__.py +0 -0
  86. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/spec/vsan_efficiency_endpoints.py +0 -0
  87. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/eval/spec/vsan_health_endpoints.py +0 -0
  88. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/test_access_info.py +0 -0
  89. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/test_init_and_onboarding.py +0 -0
  90. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/test_iscsi_gate.py +0 -0
  91. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/test_no_destructive_ops.py +0 -0
  92. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/test_no_destructive_vm_code.py +0 -0
  93. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/test_safe_error_passthrough.py +0 -0
  94. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/test_session_pool.py +0 -0
  95. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/tests/test_stop_signal_runs_atexit.py +0 -0
  96. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/access_requirements.py +0 -0
  97. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/config.py +0 -0
  98. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/doctor.py +0 -0
  99. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/init_wizard.py +0 -0
  100. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/mcp_server/__init__.py +0 -0
  101. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/mcp_server/__main__.py +0 -0
  102. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/notify/__init__.py +0 -0
  103. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/notify/audit.py +0 -0
  104. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/ops/__init__.py +0 -0
  105. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/ops/datastore_browser.py +0 -0
  106. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/ops/inventory.py +0 -0
  107. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/ops/iscsi_config.py +0 -0
  108. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/ops/iscsi_gate.py +0 -0
  109. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/ops/multipath.py +0 -0
  110. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/ops/storage_paths.py +0 -0
  111. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/ops/vsan.py +0 -0
  112. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/ops/vsan_efficiency.py +0 -0
  113. {vmware_storage-2.3.0 → vmware_storage-2.4.0}/vmware_storage/ops/vsan_sdk.py +0 -0
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://anthropic.com/claude-code/plugin.schema.json",
3
3
  "name": "vmware-storage",
4
4
  "displayName": "VMware Storage",
5
- "version": "2.3.0",
5
+ "version": "2.4.0",
6
6
  "description": "VMware vSphere storage with 15 MCP tools: datastores, iSCSI, vSAN, FC multipath.",
7
7
  "author": {
8
8
  "name": "Wei Zhou",
@@ -5,7 +5,7 @@
5
5
  "command": "uvx",
6
6
  "args": [
7
7
  "--from",
8
- "vmware-storage==2.3.0",
8
+ "vmware-storage==2.4.0",
9
9
  "vmware-storage-mcp"
10
10
  ]
11
11
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: vmware-storage
3
- Version: 2.3.0
3
+ Version: 2.4.0
4
4
  Summary: VMware vSphere storage management: datastores, iSCSI, vSAN. Domain-focused MCP skill.
5
5
  Project-URL: Homepage, https://github.com/vmware-skills/VMware-Storage
6
6
  Project-URL: Repository, https://github.com/vmware-skills/VMware-Storage
@@ -16,7 +16,7 @@ Requires-Dist: python-dotenv<2.0,>=1.0
16
16
  Requires-Dist: pyvmomi<10.0,>=8.0.3.0
17
17
  Requires-Dist: rich<15.0,>=13.0
18
18
  Requires-Dist: typer<1.0,>=0.12
19
- Requires-Dist: vmware-policy<2.0,>=1.22.0
19
+ Requires-Dist: vmware-policy<2.0,>=1.23.0
20
20
  Description-Content-Type: text/markdown
21
21
 
22
22
  <!-- mcp-name: io.github.vmware-skills/vmware-storage -->
@@ -276,6 +276,7 @@ vmware-storage-mcp
276
276
  | `get_vsan_health` returns `overall_health: null` | `null` = not asked (see `health_not_queried_reason`), never a measurement; a string is always vSAN's own answer, including its own `"unknown"`. Usual cause: connected to standalone ESXi, but the health service runs in **vCenter**. |
277
277
  | Rescan doesn't discover new LUNs | Wait 15-30 seconds after adding targets, then rescan again. Verify target IP is reachable from ESXi. |
278
278
  | "Password not found" error | Variable names follow `VMWARE_<TARGET_UPPER>_PASSWORD` (hyphens → underscores). Check `~/.vmware-storage/.env`. |
279
+ | `doctor` fails with "no vSphere role", or every command fails with "session is not authenticated" | The login works but the account has no vSphere role. Assign one (Read-Only or higher) on the vCenter root folder (Administration > Access Control > Global Permissions). Mid-run, the same fault can be an expired session: run the command again. |
279
280
  | Connection timeout to vCenter | Use `vmware-storage doctor --skip-auth` to bypass auth checks on high-latency networks. |
280
281
  | "Datastore browse did not finish within Ns" | The datastore is very large or busy. Narrow the search with a sub-path and a specific pattern (e.g. `datastore browse ds01 --path templates --pattern "*.ova"`) instead of browsing the root — do not just retry the same broad browse. |
281
282
 
@@ -319,9 +320,16 @@ claude mcp add --transport http vmware-storage https://mcp.example.com:8443/mcp
319
320
  before it binds that unauthenticated requests get 401. Use a certificate your clients trust for those names.
320
321
  - Behind a TLS-terminating reverse proxy on the same machine use `--transport http --host 127.0.0.1
321
322
  --public-name mcp.example.com` and pass the `Authorization` header through.
323
+ - Behind a load balancer or gateway on another machine, keep `--transport https` (a self-signed certificate is
324
+ fine), list in `--public-name` the host name the balancer sends as `Host`, and have it forward `Authorization`.
325
+ Its health probe can use `GET /healthz`, which needs no token and returns only `{"status": "ok"}`
326
+ (vmware-policy ≥ 1.23.0).
327
+ - Send `traceparent` or `X-Request-ID` with each request to have it stored on the audit row;
328
+ `vmware-audit log --correlation-id ID` then finds the calls behind one platform request.
322
329
  - The audit log records each caller as `token:<name>`. Calls run side by side, also on the same target: the server keeps up to `VMWARE_MCP_POOL_SIZE` (default 2) sessions per target, each one a login on that target, and a call waits at most 300 s for a free session (vmware-policy ≥ 1.22.0).
323
330
  - **Everyone who can reach the endpoint acts with this server's account.** Tokens identify callers; they grant
324
331
  nothing on the platform. Run one endpoint per privilege level. `vmware-storage access-info` shows which account that is and what it may do.
332
+ - **Several people or roles on one endpoint:** run `vmware-audit roles enable` once, then give each token a role (`vmware-audit token create NAME --role reader`, or a role in `roles.yaml` naming tools and targets; vmware-policy ≥ 1.24.0). A role limits what the token may call on this endpoint; it cannot grant more than the account has. See vmware-policy's README, "Roles for network callers".
325
333
 
326
334
  **Offer only read tools, plus the writes you choose:**
327
335
 
@@ -290,6 +290,7 @@ vmware-storage-mcp
290
290
  | `get_vsan_health` 返回 `overall_health: null` | `null` 表示**没有查询**(原因见 `health_not_queried_reason`),不是测量结果;只要是字符串就一定是 vSAN 自己给的答案,包括它自己的 `"unknown"`。常见原因:连的是独立 ESXi,而健康服务跑在 **vCenter** 上。 |
291
291
  | 重扫后未发现新 LUN | 添加目标后等待 15-30 秒再重扫。确认 ESXi 能访问目标 IP。 |
292
292
  | "Password not found" 错误 | 变量名规则:`VMWARE_<目标名大写>_PASSWORD`(连字符→下划线)。检查 `~/.vmware-storage/.env`。 |
293
+ | `doctor` 报 "no vSphere role",或每个命令都报 "session is not authenticated" | 能登录,但账号没有 vSphere 角色。在 vCenter 根文件夹上给它分配一个角色(Read-Only 或更高):Administration > Access Control > Global Permissions。运行中出现同一错误也可能是会话过期:重跑命令即可。 |
293
294
  | 连接 vCenter 超时 | 使用 `vmware-storage doctor --skip-auth` 跳过高延迟网络的认证检查。 |
294
295
 
295
296
  <!-- usage: remote-and-exposure -->
@@ -332,9 +333,15 @@ claude mcp add --transport http vmware-storage https://mcp.example.com:8443/mcp
332
333
  证书要用客户端信任、且覆盖这些名字的证书。
333
334
  - 在同一台机器上由反向代理终结 TLS 时,用 `--transport http --host 127.0.0.1 --public-name mcp.example.com`,
334
335
  并让代理透传 `Authorization` 头。
336
+ - 负载均衡或网关在另一台机器上时,保持 `--transport https`(自签证书也可以),在 `--public-name` 里写上负载均衡
337
+ 转发时带的 `Host` 名,并让它透传 `Authorization`。健康检查可以用 `GET /healthz`:不需要 token,只返回
338
+ `{"status": "ok"}`(需 vmware-policy ≥ 1.23.0)。
339
+ - 每个请求带上 `traceparent` 或 `X-Request-ID`,它会记进审计记录;用 `vmware-audit log --correlation-id ID`
340
+ 就能查到平台上某一次请求对应的调用。
335
341
  - 审计日志把每个调用者记为 `token:<名字>`。调用并行执行,同一 target 上也一样:server 为每个 target 最多保持 `VMWARE_MCP_POOL_SIZE`(默认 2)个会话,每个会话都是在该 target 上的一次登录;一个调用最多等 300 秒拿到空闲会话(vmware-policy ≥ 1.22.0)。
336
342
  - **能连到这个端点的人,都以该 server 配置的账号操作。** token 只用来识别调用者,不在平台上授予任何权限。
337
343
  每个权限级别单独起一个端点。`vmware-storage access-info` 可查看是哪个账号、它能做什么。
344
+ - **一个端点给多人或多种角色用:** 先运行一次 `vmware-audit roles enable`,再给每个 token 指定角色(`vmware-audit token create 名字 --role reader`,或在 `roles.yaml` 里定义可用的工具和 target;需 vmware-policy ≥ 1.24.0)。角色只能限定这个 token 在本端点上能调用什么,不能超出账号本身的权限。详见 vmware-policy README 的「网络调用者的角色」一节。
338
345
 
339
346
  **只提供读工具,加上你选定的写工具:**
340
347
 
@@ -255,6 +255,7 @@ vmware-storage-mcp
255
255
  | `get_vsan_health` returns `overall_health: null` | `null` = not asked (see `health_not_queried_reason`), never a measurement; a string is always vSAN's own answer, including its own `"unknown"`. Usual cause: connected to standalone ESXi, but the health service runs in **vCenter**. |
256
256
  | Rescan doesn't discover new LUNs | Wait 15-30 seconds after adding targets, then rescan again. Verify target IP is reachable from ESXi. |
257
257
  | "Password not found" error | Variable names follow `VMWARE_<TARGET_UPPER>_PASSWORD` (hyphens → underscores). Check `~/.vmware-storage/.env`. |
258
+ | `doctor` fails with "no vSphere role", or every command fails with "session is not authenticated" | The login works but the account has no vSphere role. Assign one (Read-Only or higher) on the vCenter root folder (Administration > Access Control > Global Permissions). Mid-run, the same fault can be an expired session: run the command again. |
258
259
  | Connection timeout to vCenter | Use `vmware-storage doctor --skip-auth` to bypass auth checks on high-latency networks. |
259
260
  | "Datastore browse did not finish within Ns" | The datastore is very large or busy. Narrow the search with a sub-path and a specific pattern (e.g. `datastore browse ds01 --path templates --pattern "*.ova"`) instead of browsing the root — do not just retry the same broad browse. |
260
261
 
@@ -298,9 +299,16 @@ claude mcp add --transport http vmware-storage https://mcp.example.com:8443/mcp
298
299
  before it binds that unauthenticated requests get 401. Use a certificate your clients trust for those names.
299
300
  - Behind a TLS-terminating reverse proxy on the same machine use `--transport http --host 127.0.0.1
300
301
  --public-name mcp.example.com` and pass the `Authorization` header through.
302
+ - Behind a load balancer or gateway on another machine, keep `--transport https` (a self-signed certificate is
303
+ fine), list in `--public-name` the host name the balancer sends as `Host`, and have it forward `Authorization`.
304
+ Its health probe can use `GET /healthz`, which needs no token and returns only `{"status": "ok"}`
305
+ (vmware-policy ≥ 1.23.0).
306
+ - Send `traceparent` or `X-Request-ID` with each request to have it stored on the audit row;
307
+ `vmware-audit log --correlation-id ID` then finds the calls behind one platform request.
301
308
  - The audit log records each caller as `token:<name>`. Calls run side by side, also on the same target: the server keeps up to `VMWARE_MCP_POOL_SIZE` (default 2) sessions per target, each one a login on that target, and a call waits at most 300 s for a free session (vmware-policy ≥ 1.22.0).
302
309
  - **Everyone who can reach the endpoint acts with this server's account.** Tokens identify callers; they grant
303
310
  nothing on the platform. Run one endpoint per privilege level. `vmware-storage access-info` shows which account that is and what it may do.
311
+ - **Several people or roles on one endpoint:** run `vmware-audit roles enable` once, then give each token a role (`vmware-audit token create NAME --role reader`, or a role in `roles.yaml` naming tools and targets; vmware-policy ≥ 1.24.0). A role limits what the token may call on this endpoint; it cannot grant more than the account has. See vmware-policy's README, "Roles for network callers".
304
312
 
305
313
  **Offer only read tools, plus the writes you choose:**
306
314
 
@@ -1,3 +1,27 @@
1
+ ## v2.4.0 — roles for network callers (HLD §18)
2
+
3
+ * Declares the role metadata vmware-policy ≥ 1.24.0 needs to apply **roles** to this server over HTTPS
4
+ (HLD §18): how a missing target resolves. With an older vmware-policy nothing changes. Once an operator runs
5
+ `vmware-audit roles enable`, a token's role decides which tools and targets it may use here; a refused call is
6
+ audited as `denied`. See vmware-policy's README, "Roles for network callers".
7
+ * A target-limited role is checked against the same config the connection uses: the copy the server's connection manager loaded on its first connection. Edit `config.yaml` and restart for both to see the change.
8
+ * README and setup guide: one endpoint for several people or roles.
9
+
10
+ ## v2.3.1 — a login with no vSphere role is named as such (AIops#47)
11
+
12
+ * An account that logs in but has no vSphere role (vCenter shows no session for it) is now refused at connect time
13
+ with a message naming the missing role (assign Read-Only or higher), on the `doctor` auth row, the CLI and every
14
+ MCP tool. The session is logged out and never cached. Before, `doctor` passed and every call then failed with
15
+ "the session is not authenticated".
16
+ * Refused only when two signals agree: no session, **and** vCenter refuses a view of the root folder
17
+ (`NotAuthenticated` / `NoPermission`). An account scoped to one folder can also show no session, and still
18
+ connects; anything the check cannot read lets the login through.
19
+ * The MCP hint beside these two errors (tool errors and `get_access_info`) points at `doctor`'s role check
20
+ instead of "verify connectivity".
21
+ * `NotAuthenticated` raised later (session expired, or a role removed while running) now says both causes, on the
22
+ CLI (it was a raw traceback) and over MCP (it was `NotAuthenticated: operation failed.`).
23
+ * Requires vmware-policy 1.23.0.
24
+
1
25
  ## v2.3.0 — calls run side by side on one target; `get_access_info`
2
26
 
3
27
  **Concurrent calls (HLD §16 D-13(2), issue #18).** On a shared HTTPS server, calls no longer wait for each other:
@@ -11,7 +11,7 @@
11
11
  "command": "uvx",
12
12
  "args": [
13
13
  "--from",
14
- "vmware-storage==2.3.0",
14
+ "vmware-storage==2.4.0",
15
15
  "vmware-storage-mcp"
16
16
  ]
17
17
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zw008/vmware-storage",
3
- "version": "2.3.0",
3
+ "version": "2.4.0",
4
4
  "description": "VMware vSphere storage with 15 MCP tools: datastores, iSCSI, vSAN, FC multipath.",
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-storage"
7
- version = "2.3.0"
7
+ version = "2.4.0"
8
8
  description = "VMware vSphere storage management: datastores, iSCSI, vSAN. Domain-focused MCP skill."
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -17,7 +17,7 @@ dependencies = [
17
17
  "rich>=13.0,<15.0",
18
18
  "python-dotenv>=1.0,<2.0",
19
19
  "mcp[cli]>=1.28,<2.0",
20
- "vmware-policy>=1.22.0,<2.0",
20
+ "vmware-policy>=1.23.0,<2.0",
21
21
  ]
22
22
 
23
23
 
@@ -7,12 +7,12 @@
7
7
  "url": "https://github.com/vmware-skills/VMware-Storage",
8
8
  "source": "github"
9
9
  },
10
- "version": "2.3.0",
10
+ "version": "2.4.0",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "pypi",
14
14
  "identifier": "vmware-storage",
15
- "version": "2.3.0",
15
+ "version": "2.4.0",
16
16
  "runtimeHint": "uvx",
17
17
  "packageArguments": [
18
18
  {
@@ -40,7 +40,7 @@ VMware vSphere storage management — 15 MCP tools for datastores, iSCSI, vSAN,
40
40
  ## Quick Install
41
41
 
42
42
  ```bash
43
- uv tool install vmware-storage==2.3.0
43
+ uv tool install vmware-storage==2.4.0
44
44
  vmware-storage init # guided setup: writes config + .env (chmod 600, password grep-safe), then verifies
45
45
  vmware-storage doctor
46
46
  ```
@@ -234,6 +234,10 @@ After adding iSCSI targets, the storage subsystem may need 10-30 seconds to enum
234
234
 
235
235
  The password environment variable is missing. Variable names follow the pattern `VMWARE_<TARGET_NAME_UPPER>_PASSWORD` where hyphens become underscores. Example: target `my-vcenter` needs `VMWARE_MY_VCENTER_PASSWORD`. Check your `~/.vmware-storage/.env` file.
236
236
 
237
+ ### "Session is not authenticated" or "no vSphere role" right after login
238
+
239
+ The password is right; the account has no vSphere role, so vCenter shows no session for it and every call fails. `doctor` now reports this on the auth row. Assign the account a role (Read-Only or higher) on the vCenter root folder under Administration > Access Control > Global Permissions. Seen mid-run, the same fault can also be an expired session: run the command again.
240
+
237
241
  ### Doctor reports ".env permissions too open"
238
242
 
239
243
  The `.env` file contains passwords and must have owner-only permissions:
@@ -267,7 +271,7 @@ Corporate TLS proxies inject certificates that uv's bundled CA store doesn't tru
267
271
  ## Setup
268
272
 
269
273
  ```bash
270
- uv tool install vmware-storage==2.3.0
274
+ uv tool install vmware-storage==2.4.0
271
275
  mkdir -p ~/.vmware-storage
272
276
  cp config.example.yaml ~/.vmware-storage/config.yaml
273
277
  # Edit config.yaml with your vCenter/ESXi targets
@@ -13,19 +13,19 @@ Complete setup and security guide for `vmware-storage`.
13
13
  ### Via uv (recommended)
14
14
 
15
15
  ```bash
16
- uv tool install vmware-storage==2.3.0
16
+ uv tool install vmware-storage==2.4.0
17
17
  ```
18
18
 
19
19
  ### Via pip
20
20
 
21
21
  ```bash
22
- pip install vmware-storage==2.3.0
22
+ pip install vmware-storage==2.4.0
23
23
  ```
24
24
 
25
25
  ### From source
26
26
 
27
27
  ```bash
28
- git clone --branch v2.3.0 https://github.com/vmware-skills/VMware-Storage.git
28
+ git clone --branch v2.4.0 https://github.com/vmware-skills/VMware-Storage.git
29
29
  cd VMware-Storage
30
30
  pip install -e .
31
31
  ```
@@ -235,6 +235,13 @@ vmware-storage mcp --transport https --host 0.0.0.0 --port 8443 \
235
235
  - Behind a reverse proxy that terminates TLS (Nginx, HAProxy) on the same machine, use
236
236
  `--transport http --host 127.0.0.1 --public-name mcp.example.com` (the names the proxy forwards in `Host`), and
237
237
  have the proxy pass the `Authorization` header through.
238
+ - Behind a load balancer or gateway on another machine, keep `--transport https` (a self-signed certificate is
239
+ fine: the balancer is the client that has to trust it), list in `--public-name` the host name the balancer sends
240
+ as `Host`, and have it forward `Authorization`. Point its health probe at `GET /healthz`: it needs no token, returns
241
+ only `{"status": "ok"}` and accepts any `Host` (vmware-policy ≥ 1.23.0). Every other path still needs the token.
242
+ - Correlation: each audit row stores the caller's `traceparent` trace-id, else its `X-Request-ID` /
243
+ `X-Correlation-ID`. Have the agent platform send one, and `vmware-audit log --correlation-id ID` lists the calls
244
+ behind one platform request.
238
245
  - `vmware-audit token list` / `token revoke NAME` manage callers; a revoked token is refused from its next request.
239
246
  Replace a leaked token under a new name.
240
247
  - The server will not start without at least one token, and checks before it binds that unauthenticated requests
@@ -243,6 +250,7 @@ vmware-storage mcp --transport https --host 0.0.0.0 --port 8443 \
243
250
  - **Everyone who can reach the endpoint acts with this server's account.** Tokens identify callers; they grant
244
251
  nothing on the platform. Run one endpoint per privilege level (for example a read-only account behind
245
252
  `--expose read`, and a separate endpoint for operators).
253
+ - **Several people or roles on one endpoint:** run `vmware-audit roles enable` once, then give each token a role (`vmware-audit token create NAME --role reader`, or a role in `roles.yaml` naming tools and targets; vmware-policy ≥ 1.24.0). A role limits what the token may call on this endpoint; it cannot grant more than the account has. See vmware-policy's README, "Roles for network callers".
246
254
 
247
255
  ## Security Details
248
256
 
@@ -43,9 +43,15 @@ def _target(name: str = "vcenter-prod") -> TargetConfig:
43
43
  return TargetConfig(name=name, host="vc.example.com", config_username="svc")
44
44
 
45
45
 
46
- def _dead_si(*_a, **_kw):
47
- """An SI whose liveness probe says dead, so the next connect() evicts it."""
46
+ def _live_si(*_a, **_kw):
47
+ """An SI with a session; a login without one is refused (AIops#47)."""
48
48
  si = MagicMock()
49
+ si.content.sessionManager.currentSession = MagicMock()
50
+ return si
51
+
52
+
53
+ def _expire(si: MagicMock) -> MagicMock:
54
+ """The session expires after login, so the next connect() evicts it."""
49
55
  si.content.sessionManager.currentSession = None
50
56
  return si
51
57
 
@@ -53,7 +59,7 @@ def _dead_si(*_a, **_kw):
53
59
  @pytest.mark.unit
54
60
  def test_a_released_connection_is_collectable() -> None:
55
61
  """The mechanism: what connect() registers, _release_si takes back."""
56
- with patch("pyVim.connect.SmartConnect", side_effect=_dead_si):
62
+ with patch("pyVim.connect.SmartConnect", side_effect=_live_si):
57
63
  si = ConnectionManager._create_connection(_target())
58
64
  ref = weakref.ref(si)
59
65
 
@@ -68,7 +74,7 @@ def test_a_connection_not_released_is_pinned() -> None:
68
74
  """The negative half. Without this, the test above could pass because
69
75
  nothing ever held the object -- and would then keep passing with the whole
70
76
  registration deleted."""
71
- with patch("pyVim.connect.SmartConnect", side_effect=_dead_si):
77
+ with patch("pyVim.connect.SmartConnect", side_effect=_live_si):
72
78
  si = ConnectionManager._create_connection(_target())
73
79
  ref = weakref.ref(si)
74
80
 
@@ -87,10 +93,10 @@ def test_repeated_eviction_does_not_pin_every_session() -> None:
87
93
  mgr = ConnectionManager(cfg)
88
94
 
89
95
  seen: list[weakref.ref] = []
90
- with patch("pyVim.connect.SmartConnect", side_effect=_dead_si), \
96
+ with patch("pyVim.connect.SmartConnect", side_effect=_live_si), \
91
97
  patch("pyVim.connect.Disconnect"):
92
98
  for _ in range(20):
93
- seen.append(weakref.ref(mgr.connect("vcenter-prod")))
99
+ seen.append(weakref.ref(_expire(mgr.connect("vcenter-prod"))))
94
100
  # Drop the last, still-cached session the way production does.
95
101
  mgr.disconnect_all()
96
102
  gc.collect()
@@ -0,0 +1,215 @@
1
+ """A login that works but has no vSphere role (AIops#47).
2
+
3
+ Reported on vCenter 8.0.3: an account with no vSphere role logs in, vCenter
4
+ shows no session for it (``currentSession`` is ``None``), and every inventory
5
+ call then fails with ``vim.fault.NotAuthenticated``. ``doctor`` passed, and the
6
+ error told the user to check credentials, which were fine.
7
+
8
+ Driven through the real ConnectionManager, doctor, CLI handler and MCP tool;
9
+ only ``SmartConnect`` / ``Disconnect`` are fakes, so nothing touches a network.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from unittest.mock import MagicMock, patch
15
+
16
+ import pytest
17
+ import typer
18
+ from pyVmomi import vim
19
+
20
+ from vmware_storage import doctor
21
+ from vmware_storage.config import AppConfig, TargetConfig
22
+ from vmware_storage.connection import _SI_ATEXIT, ConnectionManager, NoVsphereRoleError
23
+ from vmware_storage.mcp_server import server
24
+
25
+ ROLE_WORDS = ("no vSphere role", "Read-Only")
26
+
27
+
28
+ def _config() -> AppConfig:
29
+ return AppConfig(
30
+ targets=(
31
+ TargetConfig(name="lab", host="vc.example.com", config_username="u@vsphere.local"),
32
+ )
33
+ )
34
+
35
+
36
+ def _si(has_session: bool, *, root_readable: bool = False) -> MagicMock:
37
+ """A login as vCenter answers it.
38
+
39
+ No role (AIops#47) takes both signals: ``currentSession`` reads ``None`` and
40
+ a container view on the root folder is refused with NotAuthenticated.
41
+ ``root_readable`` is the scoped account (one folder, or Supervisor namespace
42
+ permissions only): no session either, but the root view opens.
43
+ """
44
+ si = MagicMock(name="si")
45
+ si.content.sessionManager.currentSession = MagicMock() if has_session else None
46
+ if not has_session and not root_readable:
47
+ si.content.viewManager.CreateContainerView.side_effect = vim.fault.NotAuthenticated()
48
+ return si
49
+
50
+
51
+ @pytest.fixture
52
+ def fake_login(monkeypatch):
53
+ monkeypatch.setenv("VMWARE_LAB_PASSWORD", "pw")
54
+
55
+ def install(has_session: bool, root_readable: bool = False):
56
+ si = _si(has_session, root_readable=root_readable)
57
+ return si, patch("pyVim.connect.SmartConnect", return_value=si), patch(
58
+ "pyVim.connect.Disconnect"
59
+ )
60
+
61
+ return install
62
+
63
+
64
+ @pytest.mark.unit
65
+ def test_role_less_login_is_refused_closed_and_not_cached(fake_login):
66
+ si, connect, disconnect = fake_login(has_session=False)
67
+ mgr = ConnectionManager(_config())
68
+ with connect as smart, disconnect as dis:
69
+ with pytest.raises(NoVsphereRoleError) as excinfo:
70
+ mgr.connect("lab")
71
+ msg = str(excinfo.value)
72
+ for word in ROLE_WORDS:
73
+ assert word in msg
74
+ assert "'lab'" in msg
75
+ dis.assert_called_once_with(si) # the session is logged out
76
+ assert id(si) not in _SI_ATEXIT # and its exit handler unpinned
77
+ assert mgr.list_connected() == [] # nothing cached
78
+ with pytest.raises(NoVsphereRoleError):
79
+ mgr.connect("lab")
80
+ assert smart.call_count == 2 # the second call logged in again
81
+
82
+
83
+ @pytest.mark.unit
84
+ def test_normal_login_is_unaffected(fake_login):
85
+ si, connect, disconnect = fake_login(has_session=True)
86
+ mgr = ConnectionManager(_config())
87
+ with connect, disconnect as dis:
88
+ assert mgr.connect("lab") is si
89
+ assert mgr.list_connected() == ["lab"]
90
+ dis.assert_not_called()
91
+ mgr.disconnect_all()
92
+
93
+
94
+ @pytest.mark.unit
95
+ def test_scoped_account_without_a_session_still_connects(fake_login):
96
+ """``currentSession`` alone is not a verdict: a readable root view lets it through."""
97
+ si, connect, disconnect = fake_login(has_session=False, root_readable=True)
98
+ mgr = ConnectionManager(_config())
99
+ with connect, disconnect as dis:
100
+ assert mgr.connect("lab") is si # no NoVsphereRoleError
101
+ assert mgr.list_connected() == ["lab"] # cached like any session
102
+ dis.assert_not_called()
103
+ view = si.content.viewManager.CreateContainerView.return_value
104
+ view.Destroy.assert_called_once_with() # the probe leaves no view behind
105
+ mgr.disconnect_all()
106
+
107
+
108
+ @pytest.mark.unit
109
+ def test_doctor_auth_row_names_the_missing_role(fake_login, monkeypatch):
110
+ _, connect, disconnect = fake_login(has_session=False)
111
+ monkeypatch.setattr(type(doctor.resolve_config_path()), "exists", lambda self: True)
112
+ monkeypatch.setattr("vmware_storage.config.load_config", _config)
113
+ with connect, disconnect:
114
+ ok, row = doctor._check_auth()
115
+ assert ok is False
116
+ for word in ROLE_WORDS:
117
+ assert word in row
118
+
119
+
120
+ @pytest.mark.unit
121
+ def test_mcp_tool_returns_the_role_message_whole(fake_login, monkeypatch):
122
+ _, connect, disconnect = fake_login(has_session=False)
123
+ mgr = ConnectionManager(_config())
124
+ monkeypatch.setattr(server, "_get_conn_mgr", lambda: mgr)
125
+ with connect, disconnect:
126
+ out = server.list_all_datastores(target="lab")
127
+ for word in ROLE_WORDS:
128
+ assert word in out["error"]
129
+ # The remedy sits at the end of the message; the 300-character cap cut it.
130
+ assert "vmware-storage doctor" in out["error"]
131
+ # The hint points at the role check, not at connectivity, which is fine.
132
+ assert "session" in out["hint"] and "verify connectivity" not in out["hint"]
133
+
134
+
135
+ @pytest.mark.unit
136
+ def test_cli_handler_prints_the_role_message(fake_login, monkeypatch, capsys):
137
+ from vmware_storage import cli
138
+
139
+ _, connect, disconnect = fake_login(has_session=False)
140
+ monkeypatch.setattr(cli, "load_config", lambda path=None: _config())
141
+
142
+ @cli.handle_cli_errors
143
+ def run():
144
+ cli._get_connection("lab")
145
+
146
+ with connect, disconnect, pytest.raises(typer.Exit):
147
+ run()
148
+ out = " ".join(capsys.readouterr().out.split())
149
+ for word in ROLE_WORDS:
150
+ assert word in out
151
+
152
+
153
+ # ── NotAuthenticated raised later: expired session, or a role removed ─────────
154
+
155
+
156
+ def _says_both(text: str) -> None:
157
+ text = " ".join(text.split())
158
+ assert "expired" in text and "run the command again" in text
159
+ assert "no vSphere role" in text and "Read-Only" in text
160
+
161
+
162
+ @pytest.mark.unit
163
+ def test_cli_not_authenticated_says_both_causes(capsys):
164
+ from vmware_storage.cli import handle_cli_errors
165
+
166
+ @handle_cli_errors
167
+ def boom():
168
+ raise vim.fault.NotAuthenticated()
169
+
170
+ with pytest.raises(typer.Exit):
171
+ boom()
172
+ _says_both(capsys.readouterr().out)
173
+
174
+
175
+ @pytest.mark.unit
176
+ def test_mcp_not_authenticated_says_both_causes(monkeypatch):
177
+ def _boom(target=None):
178
+ raise vim.fault.NotAuthenticated()
179
+
180
+ monkeypatch.setattr(server, "_get_connection", _boom)
181
+ out = server.list_all_datastores(target="lab")
182
+ _says_both(out["error"])
183
+ assert "operation failed" not in out["error"]
184
+ assert "session" in out["hint"] and "verify connectivity" not in out["hint"]
185
+
186
+
187
+ @pytest.mark.unit
188
+ def test_get_access_info_hint_names_the_role_check(fake_login, monkeypatch):
189
+ _, connect, disconnect = fake_login(has_session=False)
190
+ mgr = ConnectionManager(_config())
191
+ monkeypatch.setattr(server, "_get_conn_mgr", lambda: mgr)
192
+ with connect, disconnect:
193
+ out = server.get_access_info(target="lab")
194
+ for word in ROLE_WORDS:
195
+ assert word in out["error"]
196
+ assert "session" in out["hint"] and "verify connectivity" not in out["hint"]
197
+
198
+
199
+ @pytest.mark.unit
200
+ def test_write_tool_error_reply_hint_names_the_role_check():
201
+ out = server._error_reply(vim.fault.NotAuthenticated(), "storage")
202
+ assert "session" in out["hint"] and "verify connectivity" not in out["hint"]
203
+ other = server._error_reply(TimeoutError("slow"), "storage")
204
+ assert "verify connectivity" in other["hint"]
205
+
206
+
207
+ @pytest.mark.unit
208
+ def test_doctor_row_is_not_a_class_name(fake_login, monkeypatch):
209
+ """The row must carry the text, not 'NoVsphereRoleError'."""
210
+ _, connect, disconnect = fake_login(has_session=False)
211
+ monkeypatch.setattr(type(doctor.resolve_config_path()), "exists", lambda self: True)
212
+ monkeypatch.setattr("vmware_storage.config.load_config", _config)
213
+ with connect, disconnect:
214
+ _, row = doctor._check_auth()
215
+ assert "NoVsphereRoleError" not in row
@@ -0,0 +1,55 @@
1
+ """HLD §18: the role metadata this skill declares matches its real tools."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+
7
+ import pytest
8
+
9
+ from vmware_storage import policy_roles
10
+
11
+
12
+ def _registry() -> set[str]:
13
+ from vmware_storage.mcp_server.server import mcp
14
+
15
+ return {t.name for t in asyncio.run(mcp.list_tools())}
16
+
17
+
18
+ @pytest.mark.unit
19
+ def test_every_declared_name_is_a_real_tool():
20
+ named = set(policy_roles.ESCAPES) | set(getattr(policy_roles, "AKO_TOOLS", ()))
21
+ assert named - _registry() == set(), sorted(named - _registry())
22
+
23
+
24
+ @pytest.mark.unit
25
+ def test_metadata_is_declared_when_policy_has_roles():
26
+ roles = pytest.importorskip("vmware_policy.roles")
27
+ import vmware_storage.mcp_server.server # noqa: F401 (registers the metadata on import)
28
+
29
+ meta = roles.metadata("storage")
30
+ assert meta is not None and meta.version == 1
31
+ assert meta.escapes == policy_roles.ESCAPES
32
+ assert (meta.resolver is not None) is True
33
+
34
+
35
+ @pytest.mark.unit
36
+ def test_the_resolver_uses_the_config_the_connection_manager_holds(monkeypatch):
37
+ """Review H1/H-1: once the server's connection manager exists, a config edit must
38
+ not move the role check off the config it connects with; before, read the file."""
39
+ import importlib
40
+ import types
41
+
42
+ from vmware_storage import policy_roles as pr
43
+
44
+ server = importlib.import_module("vmware_storage.mcp_server.server")
45
+ assert hasattr(server, "_conn_mgr"), "policy_roles looks up a global the server does not have"
46
+ edited, loads = object(), []
47
+ monkeypatch.setattr(pr, "load_config", lambda: loads.append(1) or edited)
48
+
49
+ monkeypatch.setattr(server, "_conn_mgr", None)
50
+ assert pr._connection_config() is edited and pr._connection_config() is edited
51
+ assert len(loads) == 2 # no manager yet: the file as it is now, every time
52
+
53
+ held = object()
54
+ monkeypatch.setattr(server, "_conn_mgr", types.SimpleNamespace(_config=held))
55
+ assert pr._connection_config() is held # the manager's copy, not the edited file
@@ -1162,21 +1162,21 @@ wheels = [
1162
1162
 
1163
1163
  [[package]]
1164
1164
  name = "vmware-policy"
1165
- version = "1.22.0"
1165
+ version = "1.24.0"
1166
1166
  source = { registry = "https://pypi.org/simple" }
1167
1167
  dependencies = [
1168
1168
  { name = "pyyaml" },
1169
1169
  { name = "rich" },
1170
1170
  { name = "typer" },
1171
1171
  ]
1172
- sdist = { url = "https://files.pythonhosted.org/packages/d7/52/405a9d5e88d79649f4d7bddade8248f38ab6a508007a7500a3cdcf81897a/vmware_policy-1.22.0.tar.gz", hash = "sha256:e1a96b44ddf3c4d00fb8bdcaae4e1a059b03f343a84df9de23f865122d3dc280", size = 319890, upload-time = "2026-10-15T14:39:45.757Z" }
1172
+ sdist = { url = "https://files.pythonhosted.org/packages/4a/62/3a3cb83af77e3dd3c8fc92cb3b7968bfda04a2f71bf0d6f9b72ee3198441/vmware_policy-1.24.0.tar.gz", hash = "sha256:fba12732ccbd4371232c15d9fd014f9f5d697497210aedff2515d1d205ed3577", size = 361302, upload-time = "2026-10-16T07:18:28.317Z" }
1173
1173
  wheels = [
1174
- { url = "https://files.pythonhosted.org/packages/73/b5/2536cc8750c3ecb0a57c573def664989642ccae1a4f4a8b4283e34877721/vmware_policy-1.22.0-py3-none-any.whl", hash = "sha256:ca8afc5731e926b5eb1b93e26e14763b477bd45c33d3808138fb72903e2af664", size = 108585, upload-time = "2026-10-15T14:39:43.888Z" },
1174
+ { url = "https://files.pythonhosted.org/packages/3d/f2/fe3d9d384712023916e2cacddc385f14b095509c5e00aea417d1a9550e98/vmware_policy-1.24.0-py3-none-any.whl", hash = "sha256:cf615d0256134f01fb84c39b083649281a66d2c064c2eaa28786a0fa95ec6e58", size = 131460, upload-time = "2026-10-16T07:18:26.907Z" },
1175
1175
  ]
1176
1176
 
1177
1177
  [[package]]
1178
1178
  name = "vmware-storage"
1179
- version = "2.3.0"
1179
+ version = "2.4.0"
1180
1180
  source = { editable = "." }
1181
1181
  dependencies = [
1182
1182
  { name = "mcp", extra = ["cli"] },
@@ -1203,7 +1203,7 @@ requires-dist = [
1203
1203
  { name = "pyvmomi", specifier = ">=8.0.3.0,<10.0" },
1204
1204
  { name = "rich", specifier = ">=13.0,<15.0" },
1205
1205
  { name = "typer", specifier = ">=0.12,<1.0" },
1206
- { name = "vmware-policy", specifier = ">=1.22.0,<2.0" },
1206
+ { name = "vmware-policy", specifier = ">=1.23.0,<2.0" },
1207
1207
  ]
1208
1208
 
1209
1209
  [package.metadata.requires-dev]
@@ -1,3 +1,3 @@
1
1
  """VMware Storage — vSphere datastore, iSCSI, and vSAN management."""
2
2
 
3
- __version__ = "2.3.0"
3
+ __version__ = "2.4.0"