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.
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/.claude-plugin/plugin.json +1 -1
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/.mcp.json +1 -1
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/PKG-INFO +3 -2
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/README-CN.md +1 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/README.md +1 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/RELEASE_NOTES.md +21 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/openclaw.plugin.json +1 -1
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/package.json +1 -1
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/pyproject.toml +2 -2
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/server.json +2 -2
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/skills/vmware-privateai/SKILL.md +1 -1
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/skills/vmware-privateai/references/setup-guide.md +3 -2
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_login_without_a_role.py +65 -0
- vmware_privateai-2.4.2/tests/test_policy_roles.py +55 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/uv.lock +8 -8
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/__init__.py +1 -1
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/connection.py +37 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/policy_environment.py +4 -0
- vmware_privateai-2.4.2/vmware_privateai/policy_roles.py +52 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/.clawhubignore +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/.gitignore +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/LICENSE +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/SECURITY.md +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/skills/vmware-privateai/references/capabilities.md +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/skills/vmware-privateai/references/cli-reference.md +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/conftest.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_assign.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_atexit_handlers_do_not_accumulate.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_audit_isolation.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_bundle.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_cli_writes_guarded.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_config.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_config_path_is_shared.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_doctor.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_endpoint_gate.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_gpu.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_host_paths_protected.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_instructions_name_targets.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_monitoring.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_pais.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_readiness.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_sanitize.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_sizing.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_utilization.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_validate.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_vgpu_assign_blast_radius.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_vgpu_profiles.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/spec/privateai_endpoints.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/test_access_info.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/test_session_pool.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/test_stop_signal_runs_atexit.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/access_requirements.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/cli/__init__.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/cli/_common.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/cli/gpu.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/cli/pais.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/cli/vgpu.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/config.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/doctor.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/mcp_server/__init__.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/mcp_server/_shared.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/mcp_server/server.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/mcp_server/tools/__init__.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/mcp_server/tools/access.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/mcp_server/tools/advisor.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/mcp_server/tools/gpu.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/mcp_server/tools/pais.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/mcp_server/tools/vgpu_profiles.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/notify/__init__.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/notify/audit.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/ops/__init__.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/ops/_errors.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/ops/_paging.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/ops/_sanitize.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/ops/assign.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/ops/bundle.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/ops/gpu.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/ops/monitoring.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/ops/pais.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/ops/readiness.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/ops/sizing.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/ops/utilization.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/ops/validate.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/ops/vgpu_profiles.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/pais/__init__.py +0 -0
- {vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/pais/client.py +0 -0
- {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.
|
|
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",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: vmware-privateai
|
|
3
|
-
Version: 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.
|
|
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
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vmware-privateai"
|
|
7
|
-
version = "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.
|
|
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.
|
|
10
|
+
"version": "2.4.2",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "pypi",
|
|
14
14
|
"identifier": "vmware-privateai",
|
|
15
|
-
"version": "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.
|
|
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
|
```
|
{vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/skills/vmware-privateai/references/setup-guide.md
RENAMED
|
@@ -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.
|
|
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.
|
|
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
|
|
{vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_login_without_a_role.py
RENAMED
|
@@ -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.
|
|
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/
|
|
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/
|
|
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.
|
|
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/
|
|
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/
|
|
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.
|
|
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.
|
|
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" },
|
|
@@ -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
|
+
)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/skills/vmware-privateai/references/capabilities.md
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_audit_isolation.py
RENAMED
|
File without changes
|
|
File without changes
|
{vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_cli_writes_guarded.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_endpoint_gate.py
RENAMED
|
File without changes
|
|
File without changes
|
{vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_host_paths_protected.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
|
{vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/tests/eval/regression/test_vgpu_profiles.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
|
{vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/mcp_server/tools/__init__.py
RENAMED
|
File without changes
|
{vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/mcp_server/tools/access.py
RENAMED
|
File without changes
|
{vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/mcp_server/tools/advisor.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{vmware_privateai-2.3.2 → vmware_privateai-2.4.2}/vmware_privateai/mcp_server/tools/vgpu_profiles.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
|