vmware-privateai 2.3.2__tar.gz → 2.4.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.
Files changed (87) hide show
  1. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/.claude-plugin/plugin.json +1 -1
  2. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/.mcp.json +1 -1
  3. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/PKG-INFO +3 -2
  4. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/README-CN.md +1 -0
  5. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/README.md +1 -0
  6. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/RELEASE_NOTES.md +21 -0
  7. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/openclaw.plugin.json +1 -1
  8. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/package.json +1 -1
  9. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/pyproject.toml +2 -2
  10. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/server.json +2 -2
  11. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/skills/vmware-privateai/SKILL.md +1 -1
  12. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/skills/vmware-privateai/references/setup-guide.md +3 -2
  13. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_login_without_a_role.py +65 -0
  14. vmware_privateai-2.4.2/tests/test_policy_roles.py +55 -0
  15. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/uv.lock +8 -8
  16. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/__init__.py +1 -1
  17. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/connection.py +37 -0
  18. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/policy_environment.py +4 -0
  19. vmware_privateai-2.4.2/vmware_privateai/policy_roles.py +52 -0
  20. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/.clawhubignore +0 -0
  21. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/.gitignore +0 -0
  22. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/LICENSE +0 -0
  23. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/SECURITY.md +0 -0
  24. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/skills/vmware-privateai/references/capabilities.md +0 -0
  25. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/skills/vmware-privateai/references/cli-reference.md +0 -0
  26. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/conftest.py +0 -0
  27. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_assign.py +0 -0
  28. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_atexit_handlers_do_not_accumulate.py +0 -0
  29. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_audit_isolation.py +0 -0
  30. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_bundle.py +0 -0
  31. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_cli_writes_guarded.py +0 -0
  32. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_config.py +0 -0
  33. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_config_path_is_shared.py +0 -0
  34. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_doctor.py +0 -0
  35. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_endpoint_gate.py +0 -0
  36. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_gpu.py +0 -0
  37. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_host_paths_protected.py +0 -0
  38. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_instructions_name_targets.py +0 -0
  39. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_monitoring.py +0 -0
  40. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_pais.py +0 -0
  41. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_readiness.py +0 -0
  42. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_sanitize.py +0 -0
  43. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_sizing.py +0 -0
  44. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_utilization.py +0 -0
  45. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_validate.py +0 -0
  46. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_vgpu_assign_blast_radius.py +0 -0
  47. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_vgpu_profiles.py +0 -0
  48. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/spec/privateai_endpoints.py +0 -0
  49. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/test_access_info.py +0 -0
  50. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/test_session_pool.py +0 -0
  51. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/test_stop_signal_runs_atexit.py +0 -0
  52. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/access_requirements.py +0 -0
  53. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/cli/__init__.py +0 -0
  54. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/cli/_common.py +0 -0
  55. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/cli/gpu.py +0 -0
  56. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/cli/pais.py +0 -0
  57. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/cli/vgpu.py +0 -0
  58. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/config.py +0 -0
  59. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/doctor.py +0 -0
  60. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/mcp_server/__init__.py +0 -0
  61. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/mcp_server/_shared.py +0 -0
  62. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/mcp_server/server.py +0 -0
  63. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/mcp_server/tools/__init__.py +0 -0
  64. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/mcp_server/tools/access.py +0 -0
  65. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/mcp_server/tools/advisor.py +0 -0
  66. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/mcp_server/tools/gpu.py +0 -0
  67. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/mcp_server/tools/pais.py +0 -0
  68. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/mcp_server/tools/vgpu_profiles.py +0 -0
  69. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/notify/__init__.py +0 -0
  70. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/notify/audit.py +0 -0
  71. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/ops/__init__.py +0 -0
  72. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/ops/_errors.py +0 -0
  73. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/ops/_paging.py +0 -0
  74. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/ops/_sanitize.py +0 -0
  75. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/ops/assign.py +0 -0
  76. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/ops/bundle.py +0 -0
  77. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/ops/gpu.py +0 -0
  78. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/ops/monitoring.py +0 -0
  79. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/ops/pais.py +0 -0
  80. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/ops/readiness.py +0 -0
  81. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/ops/sizing.py +0 -0
  82. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/ops/utilization.py +0 -0
  83. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/ops/validate.py +0 -0
  84. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/ops/vgpu_profiles.py +0 -0
  85. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/pais/__init__.py +0 -0
  86. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/pais/client.py +0 -0
  87. {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/pais/config.py +0 -0
@@ -2,7 +2,7 @@
2
2
  "$schema": "https://anthropic.com/claude-code/plugin.schema.json",
3
3
  "name": "vmware-privateai",
4
4
  "displayName": "VMware PrivateAI",
5
- "version": "2.3.2",
5
+ "version": "2.4.2",
6
6
  "description": "VMware Private AI (NVIDIA): GPU inventory, vGPU profiles, utilization, model serving — 18 tools",
7
7
  "author": {
8
8
  "name": "Wei Zhou",
@@ -5,7 +5,7 @@
5
5
  "command": "uvx",
6
6
  "args": [
7
7
  "--from",
8
- "vmware-privateai==2.3.2",
8
+ "vmware-privateai==2.4.2",
9
9
  "vmware-privateai-mcp"
10
10
  ]
11
11
  }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: vmware-privateai
3
- Version: 2.3.2
3
+ Version: 2.4.2
4
4
  Summary: VMware Private AI Foundation with NVIDIA ops — GPU inventory, vGPU/DirectPath profiles, utilization, model-serving (MCP + CLI)
5
5
  Project-URL: Homepage, https://github.com/vmware-skills/VMware-PrivateAI
6
6
  Project-URL: Repository, https://github.com/vmware-skills/VMware-PrivateAI
@@ -16,7 +16,7 @@ Classifier: Programming Language :: Python :: 3
16
16
  Classifier: Topic :: System :: Systems Administration
17
17
  Requires-Python: >=3.11
18
18
  Requires-Dist: httpx<1.0,>=0.27
19
- Requires-Dist: mcp[cli]<2.0,>=1.28
19
+ Requires-Dist: mcp[cli]<2.0,>=1.30
20
20
  Requires-Dist: python-dotenv<2.0,>=1.0
21
21
  Requires-Dist: pyvmomi<10.0,>=8.0.3.0
22
22
  Requires-Dist: pyyaml<7.0,>=6.0
@@ -188,6 +188,7 @@ claude mcp add --transport http vmware-privateai https://mcp.example.com:8443/mc
188
188
  - 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).
189
189
  - **Everyone who can reach the endpoint acts with this server's account.** Tokens identify callers; they grant
190
190
  nothing on the platform. Run one endpoint per privilege level. `vmware-privateai access-info` shows which account that is and what it may do.
191
+ - **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".
191
192
 
192
193
  **Offer only read tools, plus the writes you choose:**
193
194
 
@@ -148,6 +148,7 @@ claude mcp add --transport http vmware-privateai https://mcp.example.com:8443/mc
148
148
  - 审计日志把每个调用者记为 `token:<名字>`。调用并行执行,同一 target 上也一样:server 为每个 target 最多保持 `VMWARE_MCP_POOL_SIZE`(默认 2)个会话,每个会话都是在该 target 上的一次登录;一个调用最多等 300 秒拿到空闲会话(vmware-policy ≥ 1.22.0)。
149
149
  - **能连到这个端点的人,都以该 server 配置的账号操作。** token 只用来识别调用者,不在平台上授予任何权限。
150
150
  每个权限级别单独起一个端点。`vmware-privateai access-info` 可查看是哪个账号、它能做什么。
151
+ - **一个端点给多人或多种角色用:** 先运行一次 `vmware-audit roles enable`,再给每个 token 指定角色(`vmware-audit token create 名字 --role reader`,或在 `roles.yaml` 里定义可用的工具和 target;需 vmware-policy ≥ 1.24.0)。角色只能限定这个 token 在本端点上能调用什么,不能超出账号本身的权限。详见 vmware-policy README 的「网络调用者的角色」一节。
151
152
 
152
153
  **只提供读工具,加上你选定的写工具:**
153
154
 
@@ -161,6 +161,7 @@ claude mcp add --transport http vmware-privateai https://mcp.example.com:8443/mc
161
161
  - 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).
162
162
  - **Everyone who can reach the endpoint acts with this server's account.** Tokens identify callers; they grant
163
163
  nothing on the platform. Run one endpoint per privilege level. `vmware-privateai access-info` shows which account that is and what it may do.
164
+ - **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".
164
165
 
165
166
  **Offer only read tools, plus the writes you choose:**
166
167
 
@@ -1,3 +1,24 @@
1
+ ## v2.4.2 — a login refused for lack of a vSphere role says so (2026-10-19)
2
+
3
+ (2.4.1 was not used for this package; this release follows 2.4.0.)
4
+
5
+ * **A login vCenter refuses with `NoPermission` gets the role error, not the raw fault.** Found on a vCenter 8.0.3
6
+ lab (follow-up to AIops#47): for an SSO account with no permission anywhere, vCenter refuses the login itself
7
+ (`System.View` on the root folder), so the post-login no-role check never ran and `doctor` and every tool showed
8
+ the raw fault with a connectivity hint. It now raises the no-role error: the "no vSphere role / Global
9
+ Permissions" remedy only for a vCenter target and `System.View`; an ESXi target or another privilege gets a
10
+ neutral text naming the (sanitized) privilege. A wrong password (`InvalidLogin`) is unchanged. Same text as
11
+ vmware-monitor.
12
+
13
+ ## v2.4.0 — roles for network callers (HLD §18)
14
+
15
+ * Declares the role metadata vmware-policy ≥ 1.24.0 needs to apply **roles** to this server over HTTPS
16
+ (HLD §18): how a missing target resolves. With an older vmware-policy nothing changes. Once an operator runs
17
+ `vmware-audit roles enable`, a token's role decides which tools and targets it may use here; a refused call is
18
+ audited as `denied`. See vmware-policy's README, "Roles for network callers".
19
+ * 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.
20
+ * README and setup guide: one endpoint for several people or roles.
21
+
1
22
  ## v2.3.2 — tools may not touch this server's own control files
2
23
 
3
24
  * **Security fix (HLD §18 step 0).** the `verify_pais_bundle` manifest path could name this server's own files, so over HTTPS a token holder could
@@ -11,7 +11,7 @@
11
11
  "command": "uvx",
12
12
  "args": [
13
13
  "--from",
14
- "vmware-privateai==2.3.2",
14
+ "vmware-privateai==2.4.2",
15
15
  "vmware-privateai-mcp"
16
16
  ]
17
17
  }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zw008/vmware-privateai",
3
- "version": "2.3.2",
3
+ "version": "2.4.2",
4
4
  "description": "VMware Private AI (NVIDIA): GPU inventory, vGPU profiles, utilization, model serving — 18 tools",
5
5
  "private": true,
6
6
  "license": "MIT",
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "vmware-privateai"
7
- version = "2.3.2"
7
+ version = "2.4.2"
8
8
  description = "VMware Private AI Foundation with NVIDIA ops — GPU inventory, vGPU/DirectPath profiles, utilization, model-serving (MCP + CLI)"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -24,7 +24,7 @@ dependencies = [
24
24
  "rich>=13.0,<15.0",
25
25
  "pyyaml>=6.0,<7.0",
26
26
  "python-dotenv>=1.0,<2.0",
27
- "mcp[cli]>=1.28,<2.0",
27
+ "mcp[cli]>=1.30,<2.0",
28
28
  "vmware-policy>=1.23.1,<2.0",
29
29
  ]
30
30
 
@@ -7,12 +7,12 @@
7
7
  "url": "https://github.com/vmware-skills/VMware-PrivateAI",
8
8
  "source": "github"
9
9
  },
10
- "version": "2.3.2",
10
+ "version": "2.4.2",
11
11
  "packages": [
12
12
  {
13
13
  "registryType": "pypi",
14
14
  "identifier": "vmware-privateai",
15
- "version": "2.3.2",
15
+ "version": "2.4.2",
16
16
  "runtimeHint": "uvx",
17
17
  "packageArguments": [
18
18
  {
@@ -66,7 +66,7 @@ double-confirmed at the CLI, and is audit-logged. Pre-flight the write with `val
66
66
  ## Quick Install
67
67
 
68
68
  ```bash
69
- uv tool install vmware-privateai==2.3.2
69
+ uv tool install vmware-privateai==2.4.2
70
70
  vmware-privateai version
71
71
  vmware-privateai gpu host-list # first read — lists hosts that have a GPU
72
72
  ```
@@ -10,7 +10,7 @@ Install, credential, and MCP-client configuration for vmware-privateai, plus the
10
10
  ## 1. Install
11
11
 
12
12
  ```bash
13
- uv tool install vmware-privateai==2.3.2 # isolated tool env; puts vmware-privateai on PATH
13
+ uv tool install vmware-privateai==2.4.2 # isolated tool env; puts vmware-privateai on PATH
14
14
  vmware-privateai version
15
15
  ```
16
16
 
@@ -91,7 +91,7 @@ Fallback (`uvx` — re-resolves from PyPI each start; if your network runs a TLS
91
91
  "mcpServers": {
92
92
  "vmware-privateai": {
93
93
  "command": "uvx",
94
- "args": ["--from", "vmware-privateai==2.3.2", "vmware-privateai-mcp"],
94
+ "args": ["--from", "vmware-privateai==2.4.2", "vmware-privateai-mcp"],
95
95
  "env": { "VMWARE_PRIVATEAI_CONFIG": "~/.vmware-privateai/config.yaml" }
96
96
  }
97
97
  }
@@ -163,6 +163,7 @@ vmware-privateai mcp --transport https --host 0.0.0.0 --port 8443 \
163
163
  - **Everyone who can reach the endpoint acts with this server's account.** Tokens identify callers; they grant
164
164
  nothing on the platform. Run one endpoint per privilege level (for example a read-only account behind
165
165
  `--expose read`, and a separate endpoint for operators).
166
+ - **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".
166
167
 
167
168
  ## 5. Verify
168
169
 
@@ -182,3 +182,68 @@ def test_mcp_not_authenticated_says_both_causes(monkeypatch):
182
182
  out = gpu_tools.list_gpu_hosts(target="lab")
183
183
  _says_both(out["error"])
184
184
  assert "unexpected error" not in out["error"]
185
+
186
+
187
+ # 2026-10-18, lab vCenter 8.0.3, SSO account with no permission anywhere:
188
+ # vCenter refuses the *login itself* with NoPermission (System.View on the root
189
+ # folder). Before the fix that raw fault reached doctor and every tool.
190
+ def _login_refused(exc: Exception, monkeypatch: pytest.MonkeyPatch):
191
+ monkeypatch.setenv("VMWARE_PRIVATEAI_LAB_PASSWORD", "pw")
192
+ mgr = ConnectionManager(_config())
193
+ with patch("pyVim.connect.SmartConnect", side_effect=exc):
194
+ try:
195
+ mgr.connect("lab")
196
+ except Exception as e: # noqa: BLE001 — the test inspects whatever is raised
197
+ return e
198
+ raise AssertionError("connect did not raise")
199
+
200
+
201
+ def test_login_refused_with_no_permission_names_the_missing_role(
202
+ monkeypatch: pytest.MonkeyPatch,
203
+ ) -> None:
204
+ fault = vim.fault.NoPermission(
205
+ privilegeId="System.View", msg="Permission to perform this operation was denied."
206
+ )
207
+ err = _login_refused(fault, monkeypatch)
208
+ assert isinstance(err, NoVsphereRoleError)
209
+ msg = str(err)
210
+ assert "no vSphere role" in msg and "Read-Only" in msg and "'lab'" in msg
211
+ assert "dynamicType" not in msg and "operation failed" not in msg
212
+ assert len(msg) <= 300
213
+ assert err.__cause__ is fault
214
+
215
+
216
+ def test_wrong_password_is_not_reported_as_a_missing_role(
217
+ monkeypatch: pytest.MonkeyPatch,
218
+ ) -> None:
219
+ err = _login_refused(vim.fault.InvalidLogin(), monkeypatch)
220
+ assert not isinstance(err, NoVsphereRoleError)
221
+ assert isinstance(err, vim.fault.InvalidLogin)
222
+
223
+
224
+ @pytest.mark.parametrize(
225
+ ("privilege", "esxi"),
226
+ [("System.View", True), ("Sessions.ValidateSession", False), (None, False)],
227
+ )
228
+ def test_other_login_refusals_get_a_neutral_remedy(
229
+ privilege, esxi, monkeypatch: pytest.MonkeyPatch
230
+ ) -> None:
231
+ # Review 2026-10-18 (M3): an ESXi host (lockdown mode, host permissions) or
232
+ # another privilege must not be sent to vCenter Global Permissions.
233
+ from dataclasses import replace
234
+
235
+ fault = vim.fault.NoPermission(privilegeId=privilege)
236
+ mgr = ConnectionManager(_config())
237
+ if esxi:
238
+ esxi_targets = tuple(replace(t, type="esxi") for t in mgr._config.targets)
239
+ mgr._config = replace(mgr._config, targets=esxi_targets)
240
+ monkeypatch.setenv("VMWARE_LAB_PASSWORD", "pw")
241
+ monkeypatch.setenv("VMWARE_PRIVATEAI_LAB_PASSWORD", "pw")
242
+ with patch("pyVim.connect.SmartConnect", side_effect=fault):
243
+ with pytest.raises(Exception) as info:
244
+ mgr.connect("lab")
245
+ msg = str(info.value)
246
+ assert "Global Permissions" not in msg and "lockdown mode" in msg
247
+ if privilege:
248
+ assert f"({privilege})" in msg
249
+ assert info.value.__cause__ is fault and len(msg) <= 300
@@ -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_privateai import policy_roles
10
+
11
+
12
+ def _registry() -> set[str]:
13
+ from vmware_privateai.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_privateai.mcp_server.server # noqa: F401 (registers the metadata on import)
28
+
29
+ meta = roles.metadata("privateai")
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_privateai import policy_roles as pr
43
+
44
+ server = importlib.import_module("vmware_privateai.mcp_server._shared")
45
+ assert hasattr(server, "_manager"), "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, "_manager", 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, "_manager", types.SimpleNamespace(_config=held))
55
+ assert pr._connection_config() is held # the manager's copy, not the edited file
@@ -426,7 +426,7 @@ wheels = [
426
426
 
427
427
  [[package]]
428
428
  name = "mcp"
429
- version = "1.29.0"
429
+ version = "1.30.0"
430
430
  source = { registry = "https://pypi.org/simple" }
431
431
  dependencies = [
432
432
  { name = "anyio" },
@@ -444,9 +444,9 @@ dependencies = [
444
444
  { name = "typing-inspection" },
445
445
  { name = "uvicorn", marker = "sys_platform != 'emscripten'" },
446
446
  ]
447
- sdist = { url = "https://files.pythonhosted.org/packages/30/d3/f9acc21dfc886e4f78e2add1a47db46ce16884346afde53f8a064c02c891/mcp-1.29.0.tar.gz", hash = "sha256:52d01f334de1868cc3bb2d6604931126a67631f99a6c5d3b82ba47290315ec36", size = 643148, upload-time = "2026-07-28T13:41:41.939Z" }
447
+ sdist = { url = "https://files.pythonhosted.org/packages/ba/93/0142dc84a666daf8ad51a34268f34c12fd6fda4f3810c4be2504eecc8212/mcp-1.30.0.tar.gz", hash = "sha256:445414625fce5c295faa505bb11bacece661ab6f4028d57c935db57820b7a3e4", size = 680511, upload-time = "2026-09-07T14:34:15.845Z" }
448
448
  wheels = [
449
- { url = "https://files.pythonhosted.org/packages/01/c8/248b201f6d753d69fd5d6506011abbb35a946d9142b2ae311a948fd0be3d/mcp-1.29.0-py3-none-any.whl", hash = "sha256:f5a075bb611f23d6f4d080c6a1699fa62772eebc562ba9e66b306ddde1c755f7", size = 223436, upload-time = "2026-07-28T13:41:40.337Z" },
449
+ { url = "https://files.pythonhosted.org/packages/f5/f4/e58bc33317c92a0203664daaf00bf6f41166cc0149e5d6870a03f7cd004a/mcp-1.30.0-py3-none-any.whl", hash = "sha256:666edb5009503e1047c9d60346a756f94b261f05cc2625f23d41c728ffc484d0", size = 234581, upload-time = "2026-09-07T14:34:14.266Z" },
450
450
  ]
451
451
 
452
452
  [package.optional-dependencies]
@@ -1093,21 +1093,21 @@ wheels = [
1093
1093
 
1094
1094
  [[package]]
1095
1095
  name = "vmware-policy"
1096
- version = "1.23.1"
1096
+ version = "1.24.2"
1097
1097
  source = { registry = "https://pypi.org/simple" }
1098
1098
  dependencies = [
1099
1099
  { name = "pyyaml" },
1100
1100
  { name = "rich" },
1101
1101
  { name = "typer" },
1102
1102
  ]
1103
- sdist = { url = "https://files.pythonhosted.org/packages/ec/2c/03d2327478838ed3d407c7fc30ebbd6a2cf3b3d2c58285ba6653697224b0/vmware_policy-1.23.1.tar.gz", hash = "sha256:6fe55a99fc71f9591bedd3c5dcc00042559953f02a7bf7da9d2b9b4638877c08", size = 332522, upload-time = "2026-10-16T02:55:48.081Z" }
1103
+ sdist = { url = "https://files.pythonhosted.org/packages/15/8d/b9867ea1c0112caa481841c290c3fe718c0dbd738757db7877836f46267a/vmware_policy-1.24.2.tar.gz", hash = "sha256:6c816174fcd0b490bcd1f9ebaaa18b80fcb26b0eccb614e938938bb2e8452a40", size = 369315, upload-time = "2026-10-18T03:43:52.753Z" }
1104
1104
  wheels = [
1105
- { url = "https://files.pythonhosted.org/packages/b7/22/94fc30b3e273fd0e9739fb11ac0bfa8232352cefcaf95ee25229bb277733/vmware_policy-1.23.1-py3-none-any.whl", hash = "sha256:4afe95894e8a05556dfeb9e2c6a37186cf8c4bf3017131dedfb8c23478514cec", size = 114484, upload-time = "2026-10-16T02:55:46.591Z" },
1105
+ { url = "https://files.pythonhosted.org/packages/20/01/bc5f984a420d22514d8c3a627f332891f2008213d455c2f682f311e355f1/vmware_policy-1.24.2-py3-none-any.whl", hash = "sha256:2734ab49a76474fd74beb221a2d6535caab1f91a477668484c145dc2802ae072", size = 134565, upload-time = "2026-10-18T03:43:50.856Z" },
1106
1106
  ]
1107
1107
 
1108
1108
  [[package]]
1109
1109
  name = "vmware-privateai"
1110
- version = "2.3.2"
1110
+ version = "2.4.2"
1111
1111
  source = { editable = "." }
1112
1112
  dependencies = [
1113
1113
  { name = "httpx" },
@@ -1130,7 +1130,7 @@ dev = [
1130
1130
  [package.metadata]
1131
1131
  requires-dist = [
1132
1132
  { name = "httpx", specifier = ">=0.27,<1.0" },
1133
- { name = "mcp", extras = ["cli"], specifier = ">=1.28,<2.0" },
1133
+ { name = "mcp", extras = ["cli"], specifier = ">=1.30,<2.0" },
1134
1134
  { name = "python-dotenv", specifier = ">=1.0,<2.0" },
1135
1135
  { name = "pyvmomi", specifier = ">=8.0.3.0,<10.0" },
1136
1136
  { name = "pyyaml", specifier = ">=6.0,<7.0" },
@@ -5,4 +5,4 @@ assignment, and Private AI Service (PAIS) model-serving — over vSphere 9.x /
5
5
  VCF 9.1, with the family's governed-ops harness (audit + policy + teaching errors).
6
6
  """
7
7
 
8
- __version__ = "2.3.2"
8
+ __version__ = "2.4.2"
@@ -17,6 +17,7 @@ from pyVmomi import vim
17
17
  if TYPE_CHECKING:
18
18
  from pyVmomi.vim import ServiceInstance
19
19
 
20
+ from vmware_policy import sanitize
20
21
  from vmware_policy.access import login_has_no_session, no_role_message
21
22
  from vmware_policy.pool import SessionPool
22
23
 
@@ -61,6 +62,32 @@ class NoVsphereRoleError(ConfigError):
61
62
  """
62
63
 
63
64
 
65
+ def login_refused_message(target: str, cli: str, *, privilege: str | None = None, esxi: bool = False) -> str:
66
+ """The teaching error when the login itself is refused with NoPermission.
67
+
68
+ vCenter 8.0.3 refuses the login, with NoPermission for System.View on the
69
+ root folder, for an SSO account holding no vSphere permission anywhere (lab,
70
+ 2026-10-18); an account with a permission lower down still logs in. Only
71
+ that case gets the "no vSphere role" remedy. An ESXi host (lockdown mode,
72
+ host permissions) or another privilege gets a neutral text naming the
73
+ privilege vCenter returned (sanitized, capped at 48). About 300 characters.
74
+ """
75
+ if not esxi and privilege == "System.View":
76
+ return (
77
+ f"'{target}' refused the login: the password is right but the account has "
78
+ "no vSphere role (no System.View on the root). Assign Read-Only or higher "
79
+ f"(vCenter: Global Permissions), then run '{cli} doctor'. A new password "
80
+ "will not help."
81
+ )
82
+ # vCenter's own string: sanitized and bounded so the text stays short.
83
+ need = f" ({sanitize(privilege, 48)})" if privilege else ""
84
+ return (
85
+ f"'{target}' refused the login with NoPermission{need}: the password is right "
86
+ "but this account may not log in there. On ESXi check the host's Permissions "
87
+ f"and lockdown mode; on vCenter assign a role. Then run '{cli} doctor'."
88
+ )
89
+
90
+
64
91
  NOT_AUTHENTICATED_HINT = (
65
92
  "vCenter answered 'the session is not authenticated'. Either the session "
66
93
  "expired (run the command again; a new session is opened) or the account "
@@ -190,6 +217,16 @@ class ConnectionManager:
190
217
  f"vCenter/ESXi host is up and that its 'host' and 'port' in "
191
218
  f"{CONFIG_FILE} are reachable from this machine."
192
219
  ) from exc
220
+ except vim.fault.NoPermission as exc:
221
+ # A wrong password is InvalidLogin, not this; it passes through.
222
+ raise NoVsphereRoleError(
223
+ login_refused_message(
224
+ target.name,
225
+ "vmware-privateai",
226
+ privilege=getattr(exc, "privilegeId", None),
227
+ esxi=target.type == "esxi",
228
+ )
229
+ ) from exc
193
230
 
194
231
  def _cleanup(_si: ServiceInstance = si) -> None:
195
232
  try:
@@ -53,3 +53,7 @@ def _environment_for(target: str | None) -> str:
53
53
  # to ALLOW. Keyed, a resolver only ever answers for its own skill, so
54
54
  # registering at import time is safe again.
55
55
  set_environment_resolver(_environment_for, skill=skill_name(__name__))
56
+
57
+
58
+ # Role metadata (HLD §18) is declared beside the environment resolver, on every surface.
59
+ import vmware_privateai.policy_roles # noqa: E402,F401
@@ -0,0 +1,52 @@
1
+ """What vmware-policy needs to apply roles to this skill (HLD §18 D-21/D-22).
2
+
3
+ Policy cannot see which tools escape a role or how a missing target resolves; this
4
+ module declares them. Imported by ``policy_environment``, so the MCP server and
5
+ the CLI both register it. With a vmware-policy that has no roles module (< 1.24)
6
+ nothing is declared.
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import sys
12
+
13
+ from vmware_policy import skill_name
14
+
15
+ from vmware_privateai.config import load_config
16
+
17
+ #: Tools that give their holder more than a role. Host-path tools are added by
18
+ #: policy while a file root is set.
19
+ ESCAPES = frozenset()
20
+
21
+
22
+ def _connection_config():
23
+ """The config the connection layer uses for this call (review H1, then H-1).
24
+
25
+ The server's connection manager loads the config once, on its first connection,
26
+ and keeps it. Once it exists, the role is checked against that same object, so
27
+ an edit to config.yaml cannot move the check off the target the call connects
28
+ to. Before it exists, the file as it is now is what the manager is about to load.
29
+ Looked up in ``sys.modules`` so the CLI never imports the MCP server.
30
+ """
31
+ server = sys.modules.get("vmware_privateai.mcp_server._shared")
32
+ manager = getattr(server, "_manager", None) if server is not None else None
33
+ return manager._config if manager is not None else load_config()
34
+
35
+
36
+ def _target_name(target: str | None) -> str:
37
+ """The target the connection layer would use for ``target`` (or the default)."""
38
+ config = _connection_config()
39
+ return (config.get_target(target) if target else config.default_target).name
40
+
41
+
42
+ try:
43
+ from vmware_policy.roles import declare_role_metadata
44
+ except ImportError: # vmware-policy without roles: nothing to declare
45
+ declare_role_metadata = None
46
+
47
+ if declare_role_metadata is not None:
48
+ declare_role_metadata(
49
+ skill_name(__name__),
50
+ escapes=ESCAPES,
51
+ resolver=_target_name,
52
+ )