vmware-debug 1.8.4__tar.gz → 1.8.5__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/PKG-INFO +2 -2
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/RELEASE_NOTES.md +88 -0
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/pyproject.toml +2 -2
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/server.json +2 -2
- vmware_debug-1.8.5/tests/eval/capability/_family.py +204 -0
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/tests/eval/capability/_scores.json +1 -1
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/tests/eval/capability/_scoring.py +26 -2
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/tests/eval/capability/test_error_actionability.py +132 -28
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/tests/eval/regression/test_capability_grader.py +136 -0
- vmware_debug-1.8.5/tests/test_safe_error_passthrough.py +193 -0
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/uv.lock +5 -5
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/vmware_debug/__init__.py +1 -1
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/vmware_debug/envelope.py +64 -10
- vmware_debug-1.8.4/tests/test_safe_error_passthrough.py +0 -84
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/.gitignore +0 -0
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/README-CN.md +0 -0
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/README.md +0 -0
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/SECURITY.md +0 -0
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/skills/vmware-debug/SKILL.md +0 -0
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/skills/vmware-debug/references/agent-guardrails.md +0 -0
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/skills/vmware-debug/references/capabilities.md +0 -0
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/skills/vmware-debug/references/cli-reference.md +0 -0
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/skills/vmware-debug/references/event-envelope.md +0 -0
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/skills/vmware-debug/references/routing.md +0 -0
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/skills/vmware-debug/references/setup-guide.md +0 -0
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/tests/eval/__init__.py +0 -0
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/tests/eval/capability/__init__.py +0 -0
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/tests/eval/capability/_skill.py +0 -0
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/tests/eval/capability/conftest.py +0 -0
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/tests/eval/capability/test_entity_reachability.py +0 -0
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/tests/eval/capability/test_tool_description_quality.py +0 -0
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/tests/eval/capability/test_tool_manifest_budget.py +0 -0
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/tests/eval/regression/__init__.py +0 -0
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/tests/eval/regression/test_debug_regressions.py +0 -0
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/tests/eval/regression/test_declared_environment.py +0 -0
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/tests/eval/regression/test_read_only_mode.py +0 -0
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/tests/eval/regression/test_result_envelope.py +0 -0
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/tests/test_timeline.py +0 -0
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/vmware_debug/cli.py +0 -0
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/vmware_debug/mcp/__init__.py +0 -0
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/vmware_debug/mcp/tools.py +0 -0
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/vmware_debug/mcp_server/__init__.py +0 -0
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/vmware_debug/mcp_server/server.py +0 -0
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/vmware_debug/ops/__init__.py +0 -0
- {vmware_debug-1.8.4 → vmware_debug-1.8.5}/vmware_debug/ops/timeline.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: vmware-debug
|
|
3
|
-
Version: 1.8.
|
|
3
|
+
Version: 1.8.5
|
|
4
4
|
Summary: VMware diagnostic brain — read-only incident triage, log/event correlation, and root-cause routing across the VMware skill family
|
|
5
5
|
Author-email: Wei Zhou <wei-wz.zhou@broadcom.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -13,7 +13,7 @@ Requires-Python: >=3.10
|
|
|
13
13
|
Requires-Dist: mcp[cli]<2.0,>=1.10
|
|
14
14
|
Requires-Dist: rich<15.0,>=13.0
|
|
15
15
|
Requires-Dist: typer<1.0,>=0.12
|
|
16
|
-
Requires-Dist: vmware-policy<2.0,>=1.8.
|
|
16
|
+
Requires-Dist: vmware-policy<2.0,>=1.8.5
|
|
17
17
|
Description-Content-Type: text/markdown
|
|
18
18
|
|
|
19
19
|
<!-- mcp-name: io.github.zw008/vmware-debug -->
|
|
@@ -1,3 +1,91 @@
|
|
|
1
|
+
## v1.8.5 (2026-07-20) — the two fixes v1.8.4 announced now actually work
|
|
2
|
+
|
|
3
|
+
Four adversarial reviews of v1.8.4 found that both of its headline fixes were
|
|
4
|
+
incomplete in ways the release notes did not reflect. This release makes them
|
|
5
|
+
real. If you are on 1.8.4, this is the one to take.
|
|
6
|
+
|
|
7
|
+
### Fixed — a failure that was *returned* was still audited as a success
|
|
8
|
+
|
|
9
|
+
vmware-policy 1.8.4 added `report_tool_failure()` for tools that catch an
|
|
10
|
+
exception and return an error payload instead of raising. **No skill called it.**
|
|
11
|
+
|
|
12
|
+
Every string-returning tool therefore kept doing exactly what 1.8.4 said it had
|
|
13
|
+
stopped doing: writing `status=ok` to `~/.vmware/audit.db` for an operation that
|
|
14
|
+
failed, recording an undo token for a change that never happened, and telling the
|
|
15
|
+
circuit breaker the call succeeded so repeated failures never tripped it.
|
|
16
|
+
|
|
17
|
+
The surface this covered is not marginal:
|
|
18
|
+
|
|
19
|
+
| Skill | What was mis-audited |
|
|
20
|
+
|---|---|
|
|
21
|
+
| vmware-aiops | 25 of 49 tools, including **every undo-bearing write** — a failed `vm_power_on` left an undo token saying "power it back off" |
|
|
22
|
+
| vmware-avi | all 28 tools, including `vs_toggle` and `ako_restart` |
|
|
23
|
+
| vmware-storage | all 4 write tools |
|
|
24
|
+
| vmware-nsx | the 5 delete tools |
|
|
25
|
+
|
|
26
|
+
vmware-avi is worth calling out: before 1.8.4 its exceptions propagated and the
|
|
27
|
+
audit was correct. 1.8.4 caught them and returned a string, so **that release made
|
|
28
|
+
its audit trail worse than it had been.**
|
|
29
|
+
|
|
30
|
+
Skills whose tools already return dict payloads (vmware-monitor, vmware-vks,
|
|
31
|
+
vmware-aria, vmware-log-insight, vmware-harden, vmware-debug, vmware-pilot) were
|
|
32
|
+
already detected correctly. They gained a test proving it rather than a redundant
|
|
33
|
+
call.
|
|
34
|
+
|
|
35
|
+
### Fixed — narrowing `OSError` did not close the leak it was meant to close
|
|
36
|
+
|
|
37
|
+
1.8.4 narrowed the `_safe_error` passthrough because bare `OSError` let TLS and
|
|
38
|
+
DNS failures reach the agent with hostnames and certificate subjects in them.
|
|
39
|
+
That narrowing had no effect on the error it was written for:
|
|
40
|
+
|
|
41
|
+
```
|
|
42
|
+
ssl.SSLCertVerificationError → ssl.SSLError → OSError, ValueError
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
`ValueError` has been on every allowlist since long before 1.8.4, so a
|
|
46
|
+
certificate failure kept passing through — the commonest self-signed-certificate
|
|
47
|
+
failure in this family, carrying the hostname it was checked against. An
|
|
48
|
+
allowlist structurally cannot express "not this one".
|
|
49
|
+
|
|
50
|
+
Where `ssl.SSLError` can actually surface — the pyVmomi skills — it is now
|
|
51
|
+
reduced *ahead* of the allowlist. In the httpx skills TLS arrives wrapped as
|
|
52
|
+
`httpx.ConnectError`, and in vmware-avi as `requests.exceptions.SSLError`, so the
|
|
53
|
+
guard cannot fire there; in those skills the leak was the raw exception
|
|
54
|
+
interpolated into an already-allowlisted `*ApiError`, and that is now authored
|
|
55
|
+
text naming the config target and `verify_ssl` instead of the exception.
|
|
56
|
+
|
|
57
|
+
The missing-password error — this family's most common first-run failure, whose
|
|
58
|
+
entire remedy is the environment variable name it carries — keeps its message
|
|
59
|
+
through a narrow `ConfigError(OSError)` rather than the base class. Connection
|
|
60
|
+
failures are translated at the connection layer into an authored remedy that
|
|
61
|
+
names the target and the setting to change, with the raw detail left on
|
|
62
|
+
`__cause__` for the server log.
|
|
63
|
+
|
|
64
|
+
### Also fixed
|
|
65
|
+
|
|
66
|
+
- **vmware-vks**: the quickstart documented a password variable the code never
|
|
67
|
+
reads — following `README.md` verbatim produced "Password not found". Five
|
|
68
|
+
places, plus six references to a `doctor` command this CLI has never had, two
|
|
69
|
+
descriptions promising fields the tools do not return, and eight teaching
|
|
70
|
+
messages that `RuntimeError` was masking.
|
|
71
|
+
- **vmware-nsx**: an error cited `--route-advertisement`; the flag is `--advertise`.
|
|
72
|
+
- **vmware-pilot**: `get_workflow_status` told the model to call `approve` — a
|
|
73
|
+
tool the read-only gate withholds — as the required next step; and a hint
|
|
74
|
+
pointed at a filename that could never appear in that message.
|
|
75
|
+
- **vmware-aiops**: `vm_task_status` polling a *failed task* returned
|
|
76
|
+
`{"state": "error", "error": ...}` from a successful read, which the new
|
|
77
|
+
detection read as the call itself failing. The field is now `task_error`.
|
|
78
|
+
**This is a breaking change for anything parsing that payload.**
|
|
79
|
+
- Several remedies that were still being cut by the 300-character cap the 1.8.4
|
|
80
|
+
notes claimed to have addressed.
|
|
81
|
+
|
|
82
|
+
### Known and not fixed
|
|
83
|
+
|
|
84
|
+
`ConnectionError` remains one type from two sources in several skills — a
|
|
85
|
+
skill's own authored message and urllib3's `HTTPSConnectionPool(host=..., port=...)`
|
|
86
|
+
share it, and an allowlist cannot separate them. vmware-vks is converted; the
|
|
87
|
+
rest need their own domain type and are deferred rather than half-done.
|
|
88
|
+
|
|
1
89
|
## v1.8.4 (2026-07-20) — errors that teach, and tool descriptions a small model can route from
|
|
2
90
|
|
|
3
91
|
A capability eval was rolled out across the family and asked two open questions:
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "vmware-debug"
|
|
7
|
-
version = "1.8.
|
|
7
|
+
version = "1.8.5"
|
|
8
8
|
description = "VMware diagnostic brain — read-only incident triage, log/event correlation, and root-cause routing across the VMware skill family"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = "MIT"
|
|
@@ -21,7 +21,7 @@ dependencies = [
|
|
|
21
21
|
"typer>=0.12,<1.0",
|
|
22
22
|
"rich>=13.0,<15.0",
|
|
23
23
|
"mcp[cli]>=1.10,<2.0",
|
|
24
|
-
"vmware-policy>=1.8.
|
|
24
|
+
"vmware-policy>=1.8.5,<2.0",
|
|
25
25
|
]
|
|
26
26
|
|
|
27
27
|
[project.scripts]
|
|
@@ -7,12 +7,12 @@
|
|
|
7
7
|
"url": "https://github.com/zw008/VMware-Debug",
|
|
8
8
|
"source": "github"
|
|
9
9
|
},
|
|
10
|
-
"version": "1.8.
|
|
10
|
+
"version": "1.8.5",
|
|
11
11
|
"packages": [
|
|
12
12
|
{
|
|
13
13
|
"registryType": "pypi",
|
|
14
14
|
"identifier": "vmware-debug",
|
|
15
|
-
"version": "1.8.
|
|
15
|
+
"version": "1.8.5",
|
|
16
16
|
"transport": {
|
|
17
17
|
"type": "stdio"
|
|
18
18
|
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
"""What every skill in the family exposes — generated, do not edit.
|
|
2
|
+
|
|
3
|
+
Written by scripts/install_capability_evals.sh, which is the only place
|
|
4
|
+
that can see all twelve repos at once. Error messages and tool
|
|
5
|
+
descriptions route across skills, so checking those citations needs the
|
|
6
|
+
sibling surfaces; deriving them here beats a hand-kept list that goes
|
|
7
|
+
stale without saying so.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
FAMILY_TOOLS: dict[str, frozenset[str]] = {
|
|
13
|
+
'vmware-aiops': frozenset({
|
|
14
|
+
'acknowledge_vcenter_alarm', 'attach_iso_to_vm', 'batch_clone_vms',
|
|
15
|
+
'batch_deploy_from_spec', 'batch_linked_clone_vms', 'browse_datastore',
|
|
16
|
+
'cluster_add_host', 'cluster_configure', 'cluster_create', 'cluster_delete',
|
|
17
|
+
'cluster_health_summary', 'cluster_info', 'cluster_remove_host',
|
|
18
|
+
'convert_vm_to_template', 'cross_vcenter_attention',
|
|
19
|
+
'datastore_investigation_bundle', 'deploy_linked_clone', 'deploy_vm_from_ova',
|
|
20
|
+
'deploy_vm_from_template', 'host_investigation_bundle', 'list_vcenter_alarms',
|
|
21
|
+
'reset_vcenter_alarm', 'scan_datastore_images', 'vm_apply_plan', 'vm_cancel_ttl',
|
|
22
|
+
'vm_clean_slate', 'vm_clone', 'vm_create', 'vm_create_plan', 'vm_create_snapshot',
|
|
23
|
+
'vm_delete', 'vm_delete_snapshot', 'vm_guest_download', 'vm_guest_exec',
|
|
24
|
+
'vm_guest_exec_output', 'vm_guest_provision', 'vm_guest_upload',
|
|
25
|
+
'vm_investigation_bundle', 'vm_list_plans', 'vm_list_snapshots', 'vm_list_ttl',
|
|
26
|
+
'vm_migrate', 'vm_power_off', 'vm_power_on', 'vm_reconfigure', 'vm_revert_snapshot',
|
|
27
|
+
'vm_rollback_plan', 'vm_set_ttl', 'vm_task_status',
|
|
28
|
+
}),
|
|
29
|
+
'vmware-aria': frozenset({
|
|
30
|
+
'acknowledge_alert', 'cancel_alert', 'create_alert_definition',
|
|
31
|
+
'delete_alert_definition', 'delete_report', 'generate_report', 'get_alert',
|
|
32
|
+
'get_aria_health', 'get_capacity_overview', 'get_remaining_capacity', 'get_report',
|
|
33
|
+
'get_resource', 'get_resource_health', 'get_resource_metrics',
|
|
34
|
+
'get_resource_riskbadge', 'get_time_remaining', 'get_top_consumers',
|
|
35
|
+
'investigate_alert', 'list_alert_definitions', 'list_alerts', 'list_anomalies',
|
|
36
|
+
'list_collector_groups', 'list_report_definitions', 'list_reports',
|
|
37
|
+
'list_resources', 'list_rightsizing_recommendations', 'list_symptom_definitions',
|
|
38
|
+
'set_alert_definition_state',
|
|
39
|
+
}),
|
|
40
|
+
'vmware-avi': frozenset({
|
|
41
|
+
'ako_amko_status', 'ako_clusters', 'ako_config_diff', 'ako_config_show',
|
|
42
|
+
'ako_config_upgrade', 'ako_ingress_check', 'ako_ingress_diagnose',
|
|
43
|
+
'ako_ingress_map', 'ako_logs', 'ako_restart', 'ako_status', 'ako_sync_diff',
|
|
44
|
+
'ako_sync_force', 'ako_sync_status', 'ako_version', 'pool_list',
|
|
45
|
+
'pool_member_disable', 'pool_member_enable', 'pool_members', 'se_health', 'se_list',
|
|
46
|
+
'ssl_expiry_check', 'ssl_list', 'vs_analytics', 'vs_error_logs', 'vs_list',
|
|
47
|
+
'vs_status', 'vs_toggle',
|
|
48
|
+
}),
|
|
49
|
+
'vmware-debug': frozenset({
|
|
50
|
+
'incident_timeline', 'list_symptom_categories',
|
|
51
|
+
}),
|
|
52
|
+
'vmware-harden': frozenset({
|
|
53
|
+
'get_baseline_rules', 'get_remediation', 'list_baselines', 'list_drift_events',
|
|
54
|
+
'list_violations', 'scan_target',
|
|
55
|
+
}),
|
|
56
|
+
'vmware-log-insight': frozenset({
|
|
57
|
+
'alert_get', 'alert_history', 'alert_list', 'log_aggregate', 'log_fields',
|
|
58
|
+
'log_search', 'log_version',
|
|
59
|
+
}),
|
|
60
|
+
'vmware-monitor': frozenset({
|
|
61
|
+
'active_sessions', 'active_tasks', 'certificate_status', 'cluster_health_summary',
|
|
62
|
+
'cross_vcenter_attention', 'datastore_capacity', 'datastore_investigation_bundle',
|
|
63
|
+
'get_alarms', 'get_events', 'get_host_sensors', 'get_host_services',
|
|
64
|
+
'host_investigation_bundle', 'host_log_scan', 'host_performance', 'license_status',
|
|
65
|
+
'list_all_clusters', 'list_all_datastores', 'list_all_networks', 'list_esxi_hosts',
|
|
66
|
+
'list_virtual_machines', 'ntp_status', 'resource_pool_usage', 'snapshot_aging',
|
|
67
|
+
'vm_info', 'vm_investigation_bundle', 'vm_list_snapshots', 'vm_performance',
|
|
68
|
+
}),
|
|
69
|
+
'vmware-nsx': frozenset({
|
|
70
|
+
'configure_tier0_bgp', 'create_ip_pool', 'create_nat_rule', 'create_segment',
|
|
71
|
+
'create_static_route', 'create_tier1_gateway', 'delete_ip_pool', 'delete_nat_rule',
|
|
72
|
+
'delete_segment', 'delete_static_route', 'delete_tier1_gateway',
|
|
73
|
+
'get_bgp_neighbors', 'get_edge_cluster_status', 'get_ip_pool_usage',
|
|
74
|
+
'get_logical_port_status', 'get_nsx_manager_status', 'get_segment',
|
|
75
|
+
'get_segment_port_for_vm', 'get_tier0_gateway', 'get_tier1_gateway',
|
|
76
|
+
'get_transport_node_status', 'list_edge_clusters', 'list_ip_pools',
|
|
77
|
+
'list_nat_rules', 'list_nsx_alarms', 'list_segments', 'list_static_routes',
|
|
78
|
+
'list_tier0_gateways', 'list_tier1_gateways', 'list_transport_nodes',
|
|
79
|
+
'list_transport_zones', 'update_segment', 'update_tier1_gateway',
|
|
80
|
+
}),
|
|
81
|
+
'vmware-nsx-security': frozenset({
|
|
82
|
+
'apply_vm_tag', 'create_dfw_policy', 'create_dfw_rule', 'create_group',
|
|
83
|
+
'delete_dfw_policy', 'delete_dfw_rule', 'delete_group', 'get_dfw_policy',
|
|
84
|
+
'get_dfw_rule_stats', 'get_group', 'get_idps_status', 'get_traceflow_result',
|
|
85
|
+
'list_dfw_policies', 'list_dfw_rules', 'list_groups', 'list_idps_profiles',
|
|
86
|
+
'list_vm_tags', 'remove_vm_tag', 'run_traceflow', 'update_dfw_policy',
|
|
87
|
+
'update_dfw_rule',
|
|
88
|
+
}),
|
|
89
|
+
'vmware-pilot': frozenset({
|
|
90
|
+
'approve', 'cancel_workflow', 'confirm_draft', 'create_workflow', 'design_workflow',
|
|
91
|
+
'get_skill_catalog', 'get_workflow_status', 'list_workflows', 'plan_workflow',
|
|
92
|
+
'review_workflow', 'rollback', 'run_workflow', 'update_draft',
|
|
93
|
+
}),
|
|
94
|
+
'vmware-storage': frozenset({
|
|
95
|
+
'browse_datastore', 'list_all_datastores', 'list_cached_images',
|
|
96
|
+
'scan_datastore_images', 'storage_iscsi_add_target', 'storage_iscsi_enable',
|
|
97
|
+
'storage_iscsi_remove_target', 'storage_iscsi_status', 'storage_rescan',
|
|
98
|
+
'vsan_capacity', 'vsan_health',
|
|
99
|
+
}),
|
|
100
|
+
'vmware-vks': frozenset({
|
|
101
|
+
'check_vks_compatibility', 'create_namespace', 'create_tkc_cluster',
|
|
102
|
+
'delete_namespace', 'delete_tkc_cluster', 'get_harbor_info', 'get_namespace',
|
|
103
|
+
'get_supervisor_kubeconfig', 'get_supervisor_status', 'get_tkc_available_versions',
|
|
104
|
+
'get_tkc_cluster', 'get_tkc_kubeconfig', 'list_namespace_storage_usage',
|
|
105
|
+
'list_namespaces', 'list_supervisor_storage_policies', 'list_tkc_clusters',
|
|
106
|
+
'list_vm_classes', 'scale_tkc_cluster', 'update_namespace', 'upgrade_tkc_cluster',
|
|
107
|
+
}),
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
FAMILY_COMMANDS: dict[str, frozenset[str]] = {
|
|
111
|
+
'vmware-aiops': frozenset({
|
|
112
|
+
'alarm acknowledge', 'alarm list', 'alarm reset', 'attention', 'cluster add-host',
|
|
113
|
+
'cluster configure', 'cluster create', 'cluster delete', 'cluster info',
|
|
114
|
+
'cluster remove-host', 'daemon start', 'daemon status', 'daemon stop',
|
|
115
|
+
'datastore browse', 'datastore scan-images', 'deploy batch', 'deploy batch-clone',
|
|
116
|
+
'deploy iso', 'deploy linked-clone', 'deploy mark-template', 'deploy ova',
|
|
117
|
+
'deploy template', 'doctor', 'hub status', 'init', 'investigate datastore',
|
|
118
|
+
'investigate host', 'investigate vm', 'mcp', 'mcp-config generate',
|
|
119
|
+
'mcp-config install', 'mcp-config list', 'plan list', 'scan now', 'summary',
|
|
120
|
+
'vm cancel-ttl', 'vm clean-slate', 'vm clone', 'vm create', 'vm delete',
|
|
121
|
+
'vm guest-download', 'vm guest-exec', 'vm guest-upload', 'vm list-ttl',
|
|
122
|
+
'vm migrate', 'vm power-off', 'vm power-on', 'vm reconfigure', 'vm set-ttl',
|
|
123
|
+
'vm snapshot-create', 'vm snapshot-delete', 'vm snapshot-list',
|
|
124
|
+
'vm snapshot-revert', 'vm task-status',
|
|
125
|
+
}),
|
|
126
|
+
'vmware-aria': frozenset({
|
|
127
|
+
'alert acknowledge', 'alert cancel', 'alert definitions', 'alert get', 'alert list',
|
|
128
|
+
'anomaly list', 'anomaly risk', 'capacity overview', 'capacity remaining',
|
|
129
|
+
'capacity rightsizing', 'capacity time-remaining', 'doctor', 'health collectors',
|
|
130
|
+
'health status', 'init', 'mcp', 'report definitions', 'report delete',
|
|
131
|
+
'report generate', 'report get', 'report list', 'resource get', 'resource health',
|
|
132
|
+
'resource list', 'resource metrics', 'resource top',
|
|
133
|
+
}),
|
|
134
|
+
'vmware-avi': frozenset({
|
|
135
|
+
'ako amko-status', 'ako clusters', 'ako config-diff', 'ako config-show',
|
|
136
|
+
'ako config-upgrade', 'ako ingress-check', 'ako ingress-diagnose',
|
|
137
|
+
'ako ingress-map', 'ako logs', 'ako restart', 'ako status', 'ako sync-diff',
|
|
138
|
+
'ako sync-force', 'ako sync-status', 'ako version', 'analytics', 'config', 'doctor',
|
|
139
|
+
'init', 'logs', 'mcp', 'pool disable', 'pool enable', 'pool members', 'se health',
|
|
140
|
+
'se list', 'ssl expiry', 'ssl list', 'vs disable', 'vs enable', 'vs list',
|
|
141
|
+
'vs status',
|
|
142
|
+
}),
|
|
143
|
+
'vmware-debug': frozenset({
|
|
144
|
+
'categories', 'mcp', 'triage', 'version',
|
|
145
|
+
}),
|
|
146
|
+
'vmware-harden': frozenset({
|
|
147
|
+
'advise', 'apply', 'baseline import', 'baseline list', 'baseline validate',
|
|
148
|
+
'doctor', 'drift', 'mcp', 'report', 'scan', 'web',
|
|
149
|
+
}),
|
|
150
|
+
'vmware-log-insight': frozenset({
|
|
151
|
+
'aggregate', 'alert get', 'alert history', 'alert list', 'doctor', 'fields', 'mcp',
|
|
152
|
+
'search', 'version',
|
|
153
|
+
}),
|
|
154
|
+
'vmware-monitor': frozenset({
|
|
155
|
+
'activity sessions', 'activity tasks', 'attention', 'capacity datastores',
|
|
156
|
+
'capacity pools', 'daemon start', 'daemon status', 'daemon stop', 'doctor',
|
|
157
|
+
'health alarms', 'health events', 'health sensors', 'health services',
|
|
158
|
+
'infra certs', 'infra licenses', 'infra ntp', 'init', 'inventory clusters',
|
|
159
|
+
'inventory datastores', 'inventory hosts', 'inventory networks', 'inventory vms',
|
|
160
|
+
'investigate datastore', 'investigate host', 'investigate vm', 'mcp',
|
|
161
|
+
'mcp-config generate', 'mcp-config list', 'perf hosts', 'perf vms', 'scan now',
|
|
162
|
+
'snapshots aging', 'summary', 'vm info', 'vm snapshot-list',
|
|
163
|
+
}),
|
|
164
|
+
'vmware-nsx': frozenset({
|
|
165
|
+
'doctor', 'gateway configure-tier0-bgp', 'gateway create-tier1',
|
|
166
|
+
'gateway delete-tier1', 'gateway update-tier1', 'health alarms',
|
|
167
|
+
'health edge-cluster-status', 'health manager-status',
|
|
168
|
+
'health transport-node-status', 'init', 'inventory get-segment',
|
|
169
|
+
'inventory get-tier0', 'inventory get-tier1', 'inventory list-edge-clusters',
|
|
170
|
+
'inventory list-segments', 'inventory list-tier0s', 'inventory list-tier1s',
|
|
171
|
+
'inventory list-transport-nodes', 'inventory list-transport-zones',
|
|
172
|
+
'ip-pool create', 'ip-pool delete', 'mcp', 'mcp-config generate',
|
|
173
|
+
'mcp-config install', 'mcp-config list', 'nat create-rule', 'nat delete-rule',
|
|
174
|
+
'networking bgp-neighbors', 'networking ip-pool-usage', 'networking list-ip-pools',
|
|
175
|
+
'networking list-nat-rules', 'networking list-static-routes', 'route create-static',
|
|
176
|
+
'route delete-static', 'segment create', 'segment delete', 'segment update',
|
|
177
|
+
'troubleshoot port-status', 'troubleshoot vm-segment',
|
|
178
|
+
}),
|
|
179
|
+
'vmware-nsx-security': frozenset({
|
|
180
|
+
'doctor', 'group delete', 'group get', 'group list', 'idps profiles', 'idps status',
|
|
181
|
+
'init', 'mcp', 'policy create', 'policy delete', 'policy get', 'policy list',
|
|
182
|
+
'rule delete', 'rule list', 'rule stats', 'tag apply', 'tag list', 'tag remove',
|
|
183
|
+
'traceflow run',
|
|
184
|
+
}),
|
|
185
|
+
'vmware-pilot': frozenset({
|
|
186
|
+
'mcp', 'version',
|
|
187
|
+
}),
|
|
188
|
+
'vmware-storage': frozenset({
|
|
189
|
+
'datastore browse', 'datastore list', 'datastore scan-images', 'doctor', 'init',
|
|
190
|
+
'iscsi add-target', 'iscsi enable', 'iscsi remove-target', 'iscsi rescan',
|
|
191
|
+
'iscsi status', 'mcp', 'vsan capacity', 'vsan health',
|
|
192
|
+
}),
|
|
193
|
+
'vmware-vks': frozenset({
|
|
194
|
+
'check', 'harbor', 'init', 'kubeconfig get', 'kubeconfig supervisor', 'mcp',
|
|
195
|
+
'namespace create', 'namespace delete', 'namespace get', 'namespace list',
|
|
196
|
+
'namespace update', 'namespace vm-classes', 'preflight-auth', 'storage',
|
|
197
|
+
'supervisor status', 'supervisor storage-policies', 'tkc create', 'tkc delete',
|
|
198
|
+
'tkc get', 'tkc list', 'tkc scale', 'tkc upgrade', 'tkc versions',
|
|
199
|
+
}),
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
#: Every tool name anywhere in the family, for citation checks that do
|
|
203
|
+
#: not care which skill owns the name.
|
|
204
|
+
ALL_TOOLS: frozenset[str] = frozenset().union(*FAMILY_TOOLS.values())
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"_comment": "Capability eval scores. Regenerate with: pytest -m capability. These are tracked trends, not pass/fail gates \u2014 see _scoring.py.",
|
|
2
|
+
"_comment": "Capability eval scores. Regenerate with: pytest -m capability. These are tracked trends, not pass/fail gates \u2014 see _scoring.py. Entries marked stale=true were carried over from an earlier run because this run did not measure them.",
|
|
3
3
|
"scores": {
|
|
4
4
|
"entity_reachability_full": {
|
|
5
5
|
"detail": {
|
|
@@ -99,14 +99,38 @@ class ScoreBoard:
|
|
|
99
99
|
return {s.name: s.as_dict() for s in sorted(self.records, key=lambda s: s.name)}
|
|
100
100
|
|
|
101
101
|
def write(self, path: Path = SCORES_PATH) -> None:
|
|
102
|
+
"""Persist this run's scores, never shrinking the recorded baseline.
|
|
103
|
+
|
|
104
|
+
A partial selection — ``pytest tests/eval/capability/test_x.py`` — used
|
|
105
|
+
to rewrite the file with only the metrics that run collected, silently
|
|
106
|
+
deleting the rest. Running one measurement across the family reduced all
|
|
107
|
+
twelve baselines from thirteen metrics to three, and one ``git add -A``
|
|
108
|
+
would have made that permanent: the next release would have had nothing
|
|
109
|
+
to diff against, which is the mixed-provenance corruption this file
|
|
110
|
+
exists to prevent.
|
|
111
|
+
|
|
112
|
+
Metrics from earlier runs are carried forward and marked ``stale`` so a
|
|
113
|
+
reader can tell a fresh measurement from an inherited one. Nothing is
|
|
114
|
+
lost, and nothing pretends to be newer than it is.
|
|
115
|
+
"""
|
|
102
116
|
if not self.records:
|
|
103
117
|
return
|
|
118
|
+
fresh = self.as_dict()
|
|
119
|
+
merged = dict(fresh)
|
|
120
|
+
for name, value in previous_scores(path).items():
|
|
121
|
+
if name not in merged:
|
|
122
|
+
carried = dict(value) if isinstance(value, dict) else value
|
|
123
|
+
if isinstance(carried, dict):
|
|
124
|
+
carried["stale"] = True
|
|
125
|
+
merged[name] = carried
|
|
104
126
|
payload = {
|
|
105
127
|
"_comment": (
|
|
106
128
|
"Capability eval scores. Regenerate with: pytest -m capability. "
|
|
107
|
-
"These are tracked trends, not pass/fail gates — see _scoring.py."
|
|
129
|
+
"These are tracked trends, not pass/fail gates — see _scoring.py. "
|
|
130
|
+
"Entries marked stale=true were carried over from an earlier run "
|
|
131
|
+
"because this run did not measure them."
|
|
108
132
|
),
|
|
109
|
-
"scores":
|
|
133
|
+
"scores": merged,
|
|
110
134
|
}
|
|
111
135
|
path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n")
|
|
112
136
|
|
|
@@ -65,6 +65,7 @@ from collections.abc import Callable
|
|
|
65
65
|
import pytest
|
|
66
66
|
|
|
67
67
|
from ._scoring import Score
|
|
68
|
+
from ._family import ALL_TOOLS, FAMILY_COMMANDS
|
|
68
69
|
from ._skill import CLI_NAME, COMPANION_SKILLS, PACKAGE, SERVER_MODULE
|
|
69
70
|
|
|
70
71
|
pytestmark = pytest.mark.capability
|
|
@@ -213,6 +214,49 @@ def _cli_commands() -> frozenset[str] | None:
|
|
|
213
214
|
return None
|
|
214
215
|
|
|
215
216
|
|
|
217
|
+
#: A token cited in an imperative position — "run `foo_bar`", "call foo_bar".
|
|
218
|
+
#: Only these are treated as claims about the surface. A snake_case word
|
|
219
|
+
#: elsewhere in a sentence is usually a parameter or a response field, and
|
|
220
|
+
#: flagging those would drown the real signal.
|
|
221
|
+
#: ``see`` and ``use`` are excluded: "see RELEASE_NOTES.md 'Known limitations'"
|
|
222
|
+
#: read as a call to a tool named ``release_notes``. The trailing guard keeps a
|
|
223
|
+
#: filename from matching at all — a citation ends at the word, not at a dot.
|
|
224
|
+
CITED_TOOL = re.compile(
|
|
225
|
+
r"\b(?:run|re-run|rerun|call|invoke)\s+['\"`]?([a-z][a-z0-9]*(?:_[a-z0-9]+)+)(?![.\w])"
|
|
226
|
+
)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _citable_tool_names(tools) -> frozenset[str]:
|
|
230
|
+
"""Every tool name a message in this repo may legitimately cite.
|
|
231
|
+
|
|
232
|
+
This repo's own surface plus every sibling's, because cross-skill routing is
|
|
233
|
+
the family's promoted pattern — "run vmware-monitor's ``list_esxi_hosts``"
|
|
234
|
+
is a documented hand-off, not a phantom. ``ALL_TOOLS`` is generated from all
|
|
235
|
+
twelve live registries by ``scripts/install_capability_evals.sh``.
|
|
236
|
+
|
|
237
|
+
A separate function so the union is testable on its own: driven only through
|
|
238
|
+
the live check, dropping the sibling half is invisible in any repo whose own
|
|
239
|
+
messages happen not to route outward.
|
|
240
|
+
"""
|
|
241
|
+
own = frozenset(t.name.lower() for t in tools)
|
|
242
|
+
assert own, "no tools on the surface — this check would pass vacuously"
|
|
243
|
+
assert ALL_TOOLS, "_family.py holds no tools — regenerate it before trusting this"
|
|
244
|
+
return own | frozenset(n.lower() for n in ALL_TOOLS)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _phantom_tool_citations(text: str, known: frozenset[str]) -> list[str]:
|
|
248
|
+
"""Tool-shaped names cited as callable that are not on the surface.
|
|
249
|
+
|
|
250
|
+
``names_artifact`` is an any-of predicate: one real artifact anywhere in the
|
|
251
|
+
message earns the point. So a message reading "Run list_vms to see VMs.
|
|
252
|
+
Also check .env." scored full marks *and* steered the model at a tool that
|
|
253
|
+
does not exist — the registry rebuild closed only the case where the phantom
|
|
254
|
+
was the sole artifact. Phantom citations are a regression-class property, not
|
|
255
|
+
a trend, so they are asserted rather than scored.
|
|
256
|
+
"""
|
|
257
|
+
return sorted({m.group(1) for m in CITED_TOOL.finditer(text)} - known)
|
|
258
|
+
|
|
259
|
+
|
|
216
260
|
_UNSET = object()
|
|
217
261
|
|
|
218
262
|
|
|
@@ -256,44 +300,60 @@ def _artifact_matcher(tools, cli_commands=_UNSET) -> Callable[[str], bool]:
|
|
|
256
300
|
#: tools are snake_case, so a token containing ``_`` is a tool reference,
|
|
257
301
|
#: not a subcommand claim — it is checked against the registry below rather
|
|
258
302
|
#: than being mistaken for a phantom command named "get".
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
+ re.escape(CLI_NAME.lower())
|
|
262
|
-
+ r"((?:\s+[a-z][a-z0-9_-]*){1,3})"
|
|
263
|
-
)
|
|
264
|
-
cli_mention = re.compile(re.escape(CLI_NAME.lower()))
|
|
303
|
+
def _cites_cli(low: str, cli: str, known_commands) -> bool:
|
|
304
|
+
"""``<cli> <subcommand>`` counts only when the subcommand is registered.
|
|
265
305
|
|
|
266
|
-
|
|
267
|
-
|
|
306
|
+
Used for this skill's own CLI *and* for every companion it routes to, so
|
|
307
|
+
the two rules cannot drift — the companion path was previously exempt
|
|
308
|
+
and credited any mention outright.
|
|
268
309
|
|
|
269
|
-
A prefix
|
|
270
|
-
rebuild it" cites
|
|
271
|
-
*group*
|
|
272
|
-
satisfies this even though
|
|
273
|
-
"""
|
|
274
|
-
return any(" ".join(tokens[: i + 1]) in commands for i in range(len(tokens)))
|
|
310
|
+
A prefix of the cited tokens is enough, because real messages trail
|
|
311
|
+
prose: "run 'vmware-nsx init' to rebuild it" cites ``init`` followed by
|
|
312
|
+
words that are not arguments. A *group* alone is not a runnable prefix,
|
|
313
|
+
so ``pool`` never satisfies this even though ``pool members`` does.
|
|
275
314
|
|
|
276
|
-
|
|
277
|
-
|
|
315
|
+
``known_commands is None`` means that CLI's tree could not be resolved —
|
|
316
|
+
unverifiable, which is not the same as refuted.
|
|
317
|
+
"""
|
|
318
|
+
if cli not in low:
|
|
278
319
|
return False
|
|
320
|
+
pattern = re.compile(
|
|
321
|
+
r"(?:['\"`]|\b(?:run|re-run|rerun|via|using|with)\s+['\"`]?)"
|
|
322
|
+
+ re.escape(cli)
|
|
323
|
+
+ r"((?:\s+[a-z][a-z0-9_-]*){1,3})"
|
|
324
|
+
)
|
|
279
325
|
claims = [
|
|
280
|
-
[t for t in m.group(1).split() if "_" not in t] for m in
|
|
326
|
+
[t for t in m.group(1).split() if "_" not in t] for m in pattern.finditer(low)
|
|
281
327
|
]
|
|
282
328
|
claims = [c for c in claims if c]
|
|
283
329
|
if not claims:
|
|
284
330
|
return True # names the executable, or a tool the registry check reads
|
|
285
|
-
if
|
|
286
|
-
return True #
|
|
287
|
-
return any(
|
|
331
|
+
if known_commands is None:
|
|
332
|
+
return True # tree not introspectable: unverifiable, not refuted
|
|
333
|
+
return any(
|
|
334
|
+
any(" ".join(c[: i + 1]) in known_commands for i in range(len(c)))
|
|
335
|
+
for c in claims
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
def _cites_this_cli(low: str) -> bool:
|
|
339
|
+
return _cites_cli(low, CLI_NAME.lower(), commands)
|
|
288
340
|
|
|
289
341
|
def names_artifact(text: str) -> bool:
|
|
290
342
|
low = text.lower()
|
|
291
343
|
if any(m in low for m in STATIC_ARTIFACT_MARKERS if m != CLI_NAME):
|
|
292
344
|
return True
|
|
293
|
-
# A companion hand-off counts
|
|
294
|
-
#
|
|
295
|
-
|
|
296
|
-
|
|
345
|
+
# A companion hand-off counts — but its subcommand is verified exactly
|
|
346
|
+
# like our own. Crediting any companion mention outright reopened the
|
|
347
|
+
# phantom-command hole one hop away: "run 'vmware-storage not-a-command'"
|
|
348
|
+
# scored full marks in every repo, which is the same defect the CLI
|
|
349
|
+
# check was built for. FAMILY_COMMANDS is generated from every repo's
|
|
350
|
+
# live Typer tree by scripts/install_capability_evals.sh.
|
|
351
|
+
for match in COMPANION_PATTERN.finditer(low):
|
|
352
|
+
companion = match.group(0)
|
|
353
|
+
if companion == CLI_NAME.lower():
|
|
354
|
+
continue
|
|
355
|
+
if _cites_cli(low, companion, FAMILY_COMMANDS.get(companion)):
|
|
356
|
+
return True
|
|
297
357
|
if _cites_this_cli(low):
|
|
298
358
|
return True
|
|
299
359
|
if FLAG_PATTERN.search(low) or ENV_VAR_PATTERN.search(text):
|
|
@@ -474,6 +534,29 @@ def test_error_actionability_index(board, graded_errors):
|
|
|
474
534
|
)
|
|
475
535
|
|
|
476
536
|
|
|
537
|
+
def test_no_error_message_cites_a_tool_that_does_not_exist(tools, graded_errors):
|
|
538
|
+
"""Every tool an error tells the model to run must be on the surface.
|
|
539
|
+
|
|
540
|
+
Separate from the scored dimensions on purpose. ``names_artifact`` asks "is
|
|
541
|
+
there something to act on?" and stops at the first yes; this asks "is
|
|
542
|
+
everything it points at real?", which no aggregate can express — a message
|
|
543
|
+
can be 3/3 actionable and still strand the model, as long as it names one
|
|
544
|
+
valid artifact alongside the phantom.
|
|
545
|
+
"""
|
|
546
|
+
known = _citable_tool_names(tools)
|
|
547
|
+
|
|
548
|
+
phantoms = [
|
|
549
|
+
{"where": f"{f}:{ln}", "cites": bad, "message": text[:120]}
|
|
550
|
+
for f, ln, text, _c, _g in graded_errors
|
|
551
|
+
for bad in [_phantom_tool_citations(text.lower(), known)]
|
|
552
|
+
if bad
|
|
553
|
+
]
|
|
554
|
+
assert not phantoms, (
|
|
555
|
+
f"{len(phantoms)} error message(s) tell the model to run a tool this "
|
|
556
|
+
f"surface does not expose — a remedy it cannot carry out: {phantoms[:5]}"
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
|
|
477
560
|
def test_teaching_error_rate(board, graded_errors):
|
|
478
561
|
"""Share of errors carrying *both* a remedy and something concrete to act on.
|
|
479
562
|
|
|
@@ -661,7 +744,7 @@ def test_remedies_survive_the_truncation_cap(board, graded_errors):
|
|
|
661
744
|
)
|
|
662
745
|
|
|
663
746
|
|
|
664
|
-
def _error_returns_in_server():
|
|
747
|
+
def _error_returns_in_server(server_dir=None, module=None):
|
|
665
748
|
"""Yield ``(lineno, is_dict, has_hint, hint_text)`` for each caught-error return.
|
|
666
749
|
|
|
667
750
|
Scanned statically out of the MCP server module rather than by calling a
|
|
@@ -671,8 +754,15 @@ def _error_returns_in_server():
|
|
|
671
754
|
*exhaustively* — which is the one that drifts, since each site is written
|
|
672
755
|
independently and nothing forces them to match.
|
|
673
756
|
"""
|
|
674
|
-
|
|
675
|
-
|
|
757
|
+
# Both parameters default to this repo's live server. They exist so the
|
|
758
|
+
# regression suite can point the scan at a fabricated source: driven only
|
|
759
|
+
# against whatever the package contains, a resolution bug is invisible until
|
|
760
|
+
# some repo happens to write its handler the unhandled way — which is
|
|
761
|
+
# exactly how a whole surface went unseen once already.
|
|
762
|
+
if module is None:
|
|
763
|
+
module = importlib.import_module(SERVER_MODULE)
|
|
764
|
+
if server_dir is None:
|
|
765
|
+
server_dir = pathlib.Path(module.__file__).parent
|
|
676
766
|
|
|
677
767
|
def _resolve(node) -> str:
|
|
678
768
|
"""Render a hint expression as text, following module-level constants.
|
|
@@ -705,7 +795,7 @@ def _error_returns_in_server():
|
|
|
705
795
|
# payload as having none.
|
|
706
796
|
if node.id in file_constants:
|
|
707
797
|
return file_constants[node.id]
|
|
708
|
-
return str(getattr(module, node.id, ""))
|
|
798
|
+
return str(getattr(module, node.id, "") if module is not None else "")
|
|
709
799
|
return ""
|
|
710
800
|
|
|
711
801
|
# Walk every module in the server package, not just server.py: skills split
|
|
@@ -765,10 +855,24 @@ def _error_returns_in_server():
|
|
|
765
855
|
)
|
|
766
856
|
|
|
767
857
|
for handler, file_constants in handlers:
|
|
858
|
+
# `msg = _render(...)` then `return msg` is ordinary style, and reading
|
|
859
|
+
# only the return expression made the whole surface invisible: one skill
|
|
860
|
+
# wrote its handler that way and this scan found zero error returns for
|
|
861
|
+
# a repo that has 28 tools' worth. Resolve a returned local back to what
|
|
862
|
+
# the handler assigned it.
|
|
863
|
+
locals_in_handler = {
|
|
864
|
+
t.id: n.value
|
|
865
|
+
for n in ast.walk(handler)
|
|
866
|
+
if isinstance(n, ast.Assign)
|
|
867
|
+
for t in n.targets
|
|
868
|
+
if isinstance(t, ast.Name)
|
|
869
|
+
}
|
|
768
870
|
for node in ast.walk(handler):
|
|
769
871
|
if not (isinstance(node, ast.Return) and node.value is not None):
|
|
770
872
|
continue
|
|
771
873
|
value = node.value
|
|
874
|
+
if isinstance(value, ast.Name) and value.id in locals_in_handler:
|
|
875
|
+
value = locals_in_handler[value.id]
|
|
772
876
|
folded = ""
|
|
773
877
|
if isinstance(value, ast.Call):
|
|
774
878
|
fn = value.func
|