packet-tracer-skill 0.2.1 → 0.2.3
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.
- package/CHANGELOG.md +53 -2
- package/README.md +443 -124
- package/docs/automation-controller-proof.md +35 -0
- package/docs/campus-donor-proof.md +103 -0
- package/docs/curated-donor-registry.md +37 -0
- package/docs/generate-ready-pilot-design.md +30 -0
- package/docs/github-launch-ops-0.2.1.md +45 -0
- package/docs/github-launch-ops-0.2.2.md +42 -0
- package/docs/github-metadata.md +23 -6
- package/docs/home-iot-donor-proof.md +64 -0
- package/docs/industrial-programming-proof.md +48 -0
- package/docs/ipv4-routing-management-proof.md +37 -0
- package/docs/l2-resiliency-bgp-proof.md +60 -0
- package/docs/l2-security-qos-proof.md +59 -0
- package/docs/launch-announcement-0.2.2.md +15 -0
- package/docs/packet-tracer-feature-gap-atlas.md +244 -0
- package/docs/post-launch-follow-up.md +39 -0
- package/docs/publish-preview-roadmap.md +20 -17
- package/docs/release-checklist.md +32 -17
- package/docs/release-notes-0.2.1.md +7 -6
- package/docs/release-notes-0.2.2.md +26 -0
- package/docs/release-notes-0.2.3.md +59 -0
- package/docs/runtime-truth.md +31 -0
- package/docs/security-edge-deepening-proof.md +65 -0
- package/docs/voice-collaboration-proof.md +38 -0
- package/docs/wan-security-donor-proof.md +58 -0
- package/docs/wireless-advanced-proof.md +60 -0
- package/examples/README.md +11 -8
- package/examples/gallery.md +9 -3
- package/examples/index.json +3 -3
- package/package.json +20 -1
- package/references/curated-donor-registry.json +5 -2
- package/references/packettracer-feature-atlas.json +183 -0
- package/references/scenario-fixture-corpus.json +1 -0
- package/scripts/build_examples_index.py +26 -15
- package/scripts/coverage_matrix.py +1054 -25
- package/scripts/feature_atlas.py +293 -0
- package/scripts/generate_pkt.py +491 -34
- package/scripts/intent_parser.py +798 -8
- package/scripts/pkt_editor.py +651 -0
- package/scripts/remote_search.py +197 -21
- package/scripts/runtime_doctor.py +83 -10
- package/scripts/sample_catalog.py +266 -6
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import re
|
|
3
4
|
from dataclasses import asdict, dataclass, field
|
|
4
5
|
from typing import Any
|
|
5
6
|
|
|
@@ -26,6 +27,9 @@ DEVICE_FAMILY_MAP = {
|
|
|
26
27
|
"Tablet": "end devices",
|
|
27
28
|
"Smartphone": "end devices",
|
|
28
29
|
"Printer": "end devices",
|
|
30
|
+
"IpPhone": "end devices",
|
|
31
|
+
"HomeVoip": "end devices",
|
|
32
|
+
"AnalogPhone": "end devices",
|
|
29
33
|
"LightWeightAccessPoint": "access points",
|
|
30
34
|
"WirelessRouter": "home/wireless routers",
|
|
31
35
|
"WirelessRouterNewGeneration": "home/wireless routers",
|
|
@@ -37,6 +41,9 @@ DEVICE_FAMILY_MAP = {
|
|
|
37
41
|
"Dsl Modem": "wan/cloud/dsl/cable devices",
|
|
38
42
|
"Security Appliance": "security devices",
|
|
39
43
|
"ASA": "security devices",
|
|
44
|
+
"NetworkController": "network controllers",
|
|
45
|
+
"CentralOfficeServer": "wan/cloud/dsl/cable devices",
|
|
46
|
+
"CellTower": "wan/cloud/dsl/cable devices",
|
|
40
47
|
"IoT": "iot devices",
|
|
41
48
|
"Board": "iot devices",
|
|
42
49
|
"Sensor": "iot devices",
|
|
@@ -52,6 +59,201 @@ DEFAULT_FAMILY_LIMITATIONS = {
|
|
|
52
59
|
"security devices": ["advanced security devices remain donor-limited"],
|
|
53
60
|
}
|
|
54
61
|
|
|
62
|
+
REPORT_ONLY_CAPABILITIES = {
|
|
63
|
+
"ipv6_slaac",
|
|
64
|
+
"dhcpv6_stateful",
|
|
65
|
+
"dhcpv6_stateless",
|
|
66
|
+
"ipv6_prefix_delegation",
|
|
67
|
+
"ipv6_dns_aaaa",
|
|
68
|
+
"ipv6_tunneling",
|
|
69
|
+
"isatap",
|
|
70
|
+
"ospfv3",
|
|
71
|
+
"eigrp_ipv6",
|
|
72
|
+
"ripng",
|
|
73
|
+
"hsrp",
|
|
74
|
+
"dhcp_snooping",
|
|
75
|
+
"dai",
|
|
76
|
+
"dot1x",
|
|
77
|
+
"lldp",
|
|
78
|
+
"rep",
|
|
79
|
+
"snmp",
|
|
80
|
+
"netflow",
|
|
81
|
+
"span",
|
|
82
|
+
"qos",
|
|
83
|
+
"port_security",
|
|
84
|
+
"bgp",
|
|
85
|
+
"stp",
|
|
86
|
+
"rstp",
|
|
87
|
+
"etherchannel",
|
|
88
|
+
"lacp",
|
|
89
|
+
"pagp",
|
|
90
|
+
"vtp",
|
|
91
|
+
"dtp",
|
|
92
|
+
"ospfv2",
|
|
93
|
+
"eigrp_ipv4",
|
|
94
|
+
"ripv2",
|
|
95
|
+
"static_route",
|
|
96
|
+
"default_route",
|
|
97
|
+
"dhcp_relay",
|
|
98
|
+
"nat_static",
|
|
99
|
+
"nat_dynamic",
|
|
100
|
+
"pat",
|
|
101
|
+
"ssh_ios",
|
|
102
|
+
"ntp_ios",
|
|
103
|
+
"syslog_ios",
|
|
104
|
+
"asa_acl_nat",
|
|
105
|
+
"asa_service_policy",
|
|
106
|
+
"clientless_vpn",
|
|
107
|
+
"cbac",
|
|
108
|
+
"zfw",
|
|
109
|
+
"sniffer",
|
|
110
|
+
"wlc",
|
|
111
|
+
"wpa_enterprise",
|
|
112
|
+
"wep",
|
|
113
|
+
"guest_wifi",
|
|
114
|
+
"beamforming",
|
|
115
|
+
"meraki",
|
|
116
|
+
"cellular_5g",
|
|
117
|
+
"bluetooth",
|
|
118
|
+
"network_controller",
|
|
119
|
+
"python_programming",
|
|
120
|
+
"javascript_programming",
|
|
121
|
+
"blockly_programming",
|
|
122
|
+
"tcp_udp_app",
|
|
123
|
+
"vm_iox",
|
|
124
|
+
"voip",
|
|
125
|
+
"ip_phone",
|
|
126
|
+
"call_manager",
|
|
127
|
+
"linksys_voice",
|
|
128
|
+
"mqtt",
|
|
129
|
+
"real_http",
|
|
130
|
+
"real_websocket",
|
|
131
|
+
"visual_scripting",
|
|
132
|
+
"ptp",
|
|
133
|
+
"profinet",
|
|
134
|
+
"l2nat",
|
|
135
|
+
"cyberobserver",
|
|
136
|
+
"industrial_firewall",
|
|
137
|
+
"coaxial",
|
|
138
|
+
"cable_dsl",
|
|
139
|
+
"central_office",
|
|
140
|
+
"cell_tower",
|
|
141
|
+
"power_distribution",
|
|
142
|
+
"hot_swappable",
|
|
143
|
+
"ios_license",
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
IPV6_DONOR_BACKED_CAPABILITIES = {
|
|
147
|
+
"ipv6_slaac",
|
|
148
|
+
"dhcpv6_stateful",
|
|
149
|
+
"ospfv3",
|
|
150
|
+
"eigrp_ipv6",
|
|
151
|
+
"ripng",
|
|
152
|
+
"hsrp",
|
|
153
|
+
}
|
|
154
|
+
IPV6_DONOR_BACKED_READY_EDIT_CAPABILITIES = {
|
|
155
|
+
"ospfv3",
|
|
156
|
+
"eigrp_ipv6",
|
|
157
|
+
"ripng",
|
|
158
|
+
"hsrp",
|
|
159
|
+
}
|
|
160
|
+
L2_SECURITY_MONITORING_EDIT_PROVEN_CAPABILITIES = {
|
|
161
|
+
"dhcp_snooping",
|
|
162
|
+
"dai",
|
|
163
|
+
"dot1x",
|
|
164
|
+
"lldp",
|
|
165
|
+
"rep",
|
|
166
|
+
"snmp",
|
|
167
|
+
"netflow",
|
|
168
|
+
"span",
|
|
169
|
+
"qos",
|
|
170
|
+
"port_security",
|
|
171
|
+
}
|
|
172
|
+
WIRELESS_ADVANCED_EDIT_PROVEN_CAPABILITIES = {
|
|
173
|
+
"wep",
|
|
174
|
+
"wpa_enterprise",
|
|
175
|
+
}
|
|
176
|
+
PROGRAMMING_EDIT_PROVEN_CAPABILITIES = {
|
|
177
|
+
"real_http",
|
|
178
|
+
"real_websocket",
|
|
179
|
+
"python_programming",
|
|
180
|
+
"javascript_programming",
|
|
181
|
+
"tcp_udp_app",
|
|
182
|
+
}
|
|
183
|
+
AUTOMATION_CONTROLLER_EDIT_PROVEN_CAPABILITIES = {
|
|
184
|
+
"python_programming",
|
|
185
|
+
"javascript_programming",
|
|
186
|
+
"tcp_udp_app",
|
|
187
|
+
}
|
|
188
|
+
PROGRAMMING_DONOR_BACKED_READY_CAPABILITIES = {
|
|
189
|
+
"real_http",
|
|
190
|
+
"real_websocket",
|
|
191
|
+
"python_programming",
|
|
192
|
+
"javascript_programming",
|
|
193
|
+
"tcp_udp_app",
|
|
194
|
+
}
|
|
195
|
+
L2_SECURITY_MONITORING_DONOR_BACKED_READY_CAPABILITIES = {
|
|
196
|
+
"dot1x",
|
|
197
|
+
}
|
|
198
|
+
WAN_SECURITY_DONOR_BACKED_READY_EDIT_CAPABILITIES = {
|
|
199
|
+
"zfw",
|
|
200
|
+
}
|
|
201
|
+
VOICE_COLLABORATION_DONOR_BACKED_READY_CAPABILITIES = {
|
|
202
|
+
"voip",
|
|
203
|
+
"ip_phone",
|
|
204
|
+
"call_manager",
|
|
205
|
+
}
|
|
206
|
+
VOICE_COLLABORATION_EDIT_PROVEN_CAPABILITIES = {
|
|
207
|
+
"voip",
|
|
208
|
+
"ip_phone",
|
|
209
|
+
"call_manager",
|
|
210
|
+
}
|
|
211
|
+
WAN_SECURITY_EDIT_PROVEN_CAPABILITIES = {
|
|
212
|
+
"gre",
|
|
213
|
+
"ppp",
|
|
214
|
+
"ipsec",
|
|
215
|
+
"vpn",
|
|
216
|
+
"cbac",
|
|
217
|
+
"zfw",
|
|
218
|
+
}
|
|
219
|
+
L2_RESILIENCY_ROUTING_EDIT_PROVEN_CAPABILITIES = {
|
|
220
|
+
"bgp",
|
|
221
|
+
"stp",
|
|
222
|
+
"rstp",
|
|
223
|
+
"etherchannel",
|
|
224
|
+
"lacp",
|
|
225
|
+
"pagp",
|
|
226
|
+
"vtp",
|
|
227
|
+
"dtp",
|
|
228
|
+
}
|
|
229
|
+
IPV4_ROUTING_MANAGEMENT_EDIT_PROVEN_CAPABILITIES = {
|
|
230
|
+
"ospfv2",
|
|
231
|
+
"eigrp_ipv4",
|
|
232
|
+
"ripv2",
|
|
233
|
+
"static_route",
|
|
234
|
+
"default_route",
|
|
235
|
+
"dhcp_relay",
|
|
236
|
+
"nat_static",
|
|
237
|
+
"nat_dynamic",
|
|
238
|
+
"pat",
|
|
239
|
+
"ssh_ios",
|
|
240
|
+
"ntp_ios",
|
|
241
|
+
"syslog_ios",
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
WAN_SECURITY_DONOR_LIMITED_CAPABILITIES = {
|
|
245
|
+
"gre",
|
|
246
|
+
"ppp",
|
|
247
|
+
"ipsec",
|
|
248
|
+
"vpn",
|
|
249
|
+
"security_edge",
|
|
250
|
+
"multilayer_switching",
|
|
251
|
+
}
|
|
252
|
+
ALWAYS_EDIT_PROVEN_REPORT_CAPABILITIES = IPV6_DONOR_BACKED_CAPABILITIES | (
|
|
253
|
+
L2_SECURITY_MONITORING_EDIT_PROVEN_CAPABILITIES - {"dot1x", "qos"}
|
|
254
|
+
)
|
|
255
|
+
EDIT_PROVEN_REPORT_CAPABILITIES = ALWAYS_EDIT_PROVEN_REPORT_CAPABILITIES
|
|
256
|
+
|
|
55
257
|
CAPABILITY_PROVIDER_FAMILIES = {
|
|
56
258
|
"access_port": {"switches", "multilayer switches"},
|
|
57
259
|
"trunk": {"switches", "multilayer switches"},
|
|
@@ -70,6 +272,25 @@ CAPABILITY_PROVIDER_FAMILIES = {
|
|
|
70
272
|
"ipsec": {"routers", "security devices", "wan/cloud/dsl/cable devices"},
|
|
71
273
|
"gre": {"routers", "wan/cloud/dsl/cable devices"},
|
|
72
274
|
"ppp": {"routers", "wan/cloud/dsl/cable devices"},
|
|
275
|
+
"bgp": {"routers", "multilayer switches"},
|
|
276
|
+
"stp": {"switches", "multilayer switches"},
|
|
277
|
+
"rstp": {"switches", "multilayer switches"},
|
|
278
|
+
"etherchannel": {"switches", "multilayer switches"},
|
|
279
|
+
"lacp": {"switches", "multilayer switches"},
|
|
280
|
+
"pagp": {"switches", "multilayer switches"},
|
|
281
|
+
"vtp": {"switches", "multilayer switches"},
|
|
282
|
+
"dtp": {"switches", "multilayer switches"},
|
|
283
|
+
"ospfv2": {"routers", "multilayer switches"},
|
|
284
|
+
"eigrp_ipv4": {"routers", "multilayer switches"},
|
|
285
|
+
"ripv2": {"routers", "multilayer switches"},
|
|
286
|
+
"static_route": {"routers", "multilayer switches", "security devices"},
|
|
287
|
+
"default_route": {"routers", "multilayer switches", "security devices"},
|
|
288
|
+
"dhcp_relay": {"routers", "multilayer switches"},
|
|
289
|
+
"nat_static": {"routers", "security devices"},
|
|
290
|
+
"nat_dynamic": {"routers", "security devices"},
|
|
291
|
+
"ssh_ios": {"routers", "switches", "multilayer switches"},
|
|
292
|
+
"ntp_ios": {"routers", "switches", "multilayer switches"},
|
|
293
|
+
"syslog_ios": {"routers", "switches", "multilayer switches"},
|
|
73
294
|
"multilayer_switching": {"multilayer switches"},
|
|
74
295
|
"wan": {"wan/cloud/dsl/cable devices", "routers"},
|
|
75
296
|
"security_edge": {"security devices", "routers"},
|
|
@@ -99,6 +320,67 @@ CAPABILITY_PROVIDER_FAMILIES = {
|
|
|
99
320
|
"tablet": {"end devices"},
|
|
100
321
|
"printer": {"end devices"},
|
|
101
322
|
"verification": {"routers", "switches", "multilayer switches", "servers", "end devices", "access points"},
|
|
323
|
+
"ipv6_slaac": {"routers", "switches", "end devices", "pt-specific edge/utility devices"},
|
|
324
|
+
"dhcpv6_stateful": {"routers", "servers", "end devices"},
|
|
325
|
+
"dhcpv6_stateless": {"routers", "servers", "end devices"},
|
|
326
|
+
"ipv6_prefix_delegation": {"routers", "wan/cloud/dsl/cable devices"},
|
|
327
|
+
"ipv6_dns_aaaa": {"servers", "routers"},
|
|
328
|
+
"ipv6_tunneling": {"routers"},
|
|
329
|
+
"isatap": {"routers", "end devices"},
|
|
330
|
+
"ospfv3": {"routers"},
|
|
331
|
+
"eigrp_ipv6": {"routers"},
|
|
332
|
+
"ripng": {"routers"},
|
|
333
|
+
"hsrp": {"routers", "switches", "multilayer switches"},
|
|
334
|
+
"dhcp_snooping": {"switches", "multilayer switches"},
|
|
335
|
+
"dai": {"switches", "multilayer switches"},
|
|
336
|
+
"dot1x": {"switches", "servers", "end devices"},
|
|
337
|
+
"lldp": {"routers", "switches", "multilayer switches"},
|
|
338
|
+
"rep": {"switches", "multilayer switches"},
|
|
339
|
+
"snmp": {"routers", "switches", "multilayer switches", "access points", "servers"},
|
|
340
|
+
"netflow": {"routers"},
|
|
341
|
+
"span": {"switches", "multilayer switches"},
|
|
342
|
+
"qos": {"routers", "switches", "multilayer switches"},
|
|
343
|
+
"port_security": {"switches", "multilayer switches"},
|
|
344
|
+
"asa_acl_nat": {"security devices"},
|
|
345
|
+
"asa_service_policy": {"security devices"},
|
|
346
|
+
"clientless_vpn": {"security devices"},
|
|
347
|
+
"cbac": {"routers", "security devices"},
|
|
348
|
+
"zfw": {"routers", "security devices"},
|
|
349
|
+
"sniffer": {"end devices", "pt-specific edge/utility devices"},
|
|
350
|
+
"wlc": {"access points"},
|
|
351
|
+
"wpa_enterprise": {"access points", "home/wireless routers", "servers"},
|
|
352
|
+
"wep": {"access points", "home/wireless routers"},
|
|
353
|
+
"guest_wifi": {"home/wireless routers", "access points"},
|
|
354
|
+
"beamforming": {"home/wireless routers"},
|
|
355
|
+
"meraki": {"security devices", "home/wireless routers", "access points"},
|
|
356
|
+
"cellular_5g": {"wan/cloud/dsl/cable devices", "end devices"},
|
|
357
|
+
"bluetooth": {"iot devices", "end devices"},
|
|
358
|
+
"network_controller": {"pt-specific edge/utility devices", "routers", "switches"},
|
|
359
|
+
"python_programming": {"end devices", "pt-specific edge/utility devices"},
|
|
360
|
+
"javascript_programming": {"end devices", "pt-specific edge/utility devices"},
|
|
361
|
+
"blockly_programming": {"end devices", "pt-specific edge/utility devices"},
|
|
362
|
+
"tcp_udp_app": {"end devices", "servers"},
|
|
363
|
+
"vm_iox": {"routers", "pt-specific edge/utility devices"},
|
|
364
|
+
"voip": {"end devices", "switches", "routers"},
|
|
365
|
+
"ip_phone": {"end devices"},
|
|
366
|
+
"call_manager": {"routers", "servers", "end devices", "switches"},
|
|
367
|
+
"linksys_voice": {"home/wireless routers", "end devices"},
|
|
368
|
+
"mqtt": {"iot devices", "servers", "home/wireless routers", "pt-specific edge/utility devices"},
|
|
369
|
+
"real_http": {"iot devices", "servers", "pt-specific edge/utility devices"},
|
|
370
|
+
"real_websocket": {"iot devices", "servers", "pt-specific edge/utility devices"},
|
|
371
|
+
"visual_scripting": {"iot devices"},
|
|
372
|
+
"ptp": {"switches", "multilayer switches", "routers"},
|
|
373
|
+
"profinet": {"iot devices", "routers", "switches"},
|
|
374
|
+
"l2nat": {"switches", "multilayer switches"},
|
|
375
|
+
"cyberobserver": {"servers", "switches", "routers"},
|
|
376
|
+
"industrial_firewall": {"security devices", "routers"},
|
|
377
|
+
"coaxial": {"pt-specific edge/utility devices"},
|
|
378
|
+
"cable_dsl": {"wan/cloud/dsl/cable devices"},
|
|
379
|
+
"central_office": {"pt-specific edge/utility devices"},
|
|
380
|
+
"cell_tower": {"pt-specific edge/utility devices", "wan/cloud/dsl/cable devices"},
|
|
381
|
+
"power_distribution": {"pt-specific edge/utility devices"},
|
|
382
|
+
"hot_swappable": {"multilayer switches", "pt-specific edge/utility devices"},
|
|
383
|
+
"ios_license": {"routers", "switches", "multilayer switches"},
|
|
102
384
|
}
|
|
103
385
|
|
|
104
386
|
|
|
@@ -126,6 +408,33 @@ class CoverageGapReport:
|
|
|
126
408
|
scenario_generate_readiness: dict[str, object] = field(default_factory=dict)
|
|
127
409
|
recommended_next_actions: list[str] = field(default_factory=list)
|
|
128
410
|
|
|
411
|
+
@property
|
|
412
|
+
def critical_capability_parity(self) -> list[dict[str, object]]:
|
|
413
|
+
critical = set(self.scenario_generate_readiness.get("critical_capabilities") or [])
|
|
414
|
+
if not critical:
|
|
415
|
+
return []
|
|
416
|
+
return [entry for entry in self.capability_parity if entry.get("capability") in critical]
|
|
417
|
+
|
|
418
|
+
@property
|
|
419
|
+
def critical_parity_supported_count(self) -> int:
|
|
420
|
+
return sum(1 for entry in self.critical_capability_parity if entry.get("inventory_supported") or entry.get("edit_supported"))
|
|
421
|
+
|
|
422
|
+
@property
|
|
423
|
+
def critical_parity_generate_ready_count(self) -> int:
|
|
424
|
+
return sum(1 for entry in self.critical_capability_parity if entry.get("generate_supported"))
|
|
425
|
+
|
|
426
|
+
@property
|
|
427
|
+
def critical_parity_acceptance_verified_count(self) -> int:
|
|
428
|
+
return sum(1 for entry in self.critical_capability_parity if entry.get("acceptance_verified"))
|
|
429
|
+
|
|
430
|
+
@property
|
|
431
|
+
def critical_parity_donor_backed_ready_count(self) -> int:
|
|
432
|
+
return sum(1 for entry in self.critical_capability_parity if entry.get("donor_backed_ready"))
|
|
433
|
+
|
|
434
|
+
@property
|
|
435
|
+
def critical_parity_mismatch_count(self) -> int:
|
|
436
|
+
return sum(1 for entry in self.critical_capability_parity if entry.get("generate_mismatch_reason"))
|
|
437
|
+
|
|
129
438
|
|
|
130
439
|
@dataclass
|
|
131
440
|
class BlueprintPlan:
|
|
@@ -277,26 +586,78 @@ def _requested_device_families(plan: IntentPlan) -> list[str]:
|
|
|
277
586
|
def _scenario_family_for_plan(plan: IntentPlan, requested_families: list[str]) -> str | None:
|
|
278
587
|
capabilities = set(plan.capabilities)
|
|
279
588
|
requested_services = {str(service) for service in plan.service_requirements.get("services", []) if service}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
589
|
+
requested_family_set = set(requested_families)
|
|
590
|
+
prompt_lower = str(plan.prompt or "").lower()
|
|
591
|
+
campus_capabilities = {"vlan", "router_on_a_stick", "management_vlan", "telnet", "acl", "nat", "pat", "trunk", "access_port"}
|
|
592
|
+
service_capabilities = {"server_dns", "server_dhcp", "server_http", "server_https", "server_ftp", "server_tftp", "server_email", "server_syslog", "server_aaa"}
|
|
593
|
+
explicit_campus_style = plan.network_style in {"campus", "department_lan"}
|
|
594
|
+
explicit_service_style = plan.network_style == "service_heavy"
|
|
595
|
+
explicit_home_style = plan.network_style in {"home_iot", "smart_home"}
|
|
596
|
+
explicit_home_gateway_prompt = any(
|
|
597
|
+
token in prompt_lower
|
|
598
|
+
for token in ["home gateway", "homegateway", "smart home", "iot "]
|
|
599
|
+
)
|
|
600
|
+
campus_signal = (
|
|
601
|
+
explicit_campus_style
|
|
602
|
+
or bool(plan.department_groups)
|
|
603
|
+
or "campus" in prompt_lower
|
|
604
|
+
or "kampus" in prompt_lower
|
|
605
|
+
or (requested_family_set & {"switches", "multilayer switches"} and capabilities & campus_capabilities)
|
|
606
|
+
or len(capabilities & campus_capabilities) >= 3
|
|
607
|
+
)
|
|
608
|
+
home_iot_signal = (
|
|
609
|
+
explicit_home_style
|
|
610
|
+
or explicit_home_gateway_prompt
|
|
611
|
+
or plan.wireless_mode == "home_router_edge"
|
|
612
|
+
or capabilities & {"iot", "iot_registration", "iot_control"}
|
|
613
|
+
or {"iot devices", "home/wireless routers"} & requested_family_set
|
|
614
|
+
)
|
|
615
|
+
wan_signal = (
|
|
283
616
|
plan.network_style == "wan_security"
|
|
284
|
-
or capabilities & {"vpn", "ipsec", "gre", "ppp", "multilayer_switching", "security_edge"}
|
|
285
|
-
or {"wan/cloud/dsl/cable devices", "security devices"} &
|
|
286
|
-
)
|
|
617
|
+
or capabilities & {"vpn", "ipsec", "gre", "ppp", "multilayer_switching", "security_edge", "cbac", "zfw"}
|
|
618
|
+
or {"wan/cloud/dsl/cable devices", "security devices"} & requested_family_set
|
|
619
|
+
)
|
|
620
|
+
ipv6_routing_signal = plan.network_style == "ipv6_routing" or capabilities & {"ipv6_slaac", "dhcpv6_stateful", "dhcpv6_stateless", "ipv6_prefix_delegation", "ipv6_dns_aaaa", "ipv6_tunneling", "isatap", "ospfv3", "eigrp_ipv6", "ripng", "hsrp"}
|
|
621
|
+
ipv4_routing_management_signal = plan.network_style == "ipv4_routing_management" or capabilities & {"ospfv2", "eigrp_ipv4", "ripv2", "static_route", "default_route", "dhcp_relay", "nat_static", "nat_dynamic", "pat", "ssh_ios", "ntp_ios", "syslog_ios"}
|
|
622
|
+
l2_resiliency_routing_signal = plan.network_style == "l2_resiliency_routing" or capabilities & {"bgp", "stp", "rstp", "etherchannel", "lacp", "pagp", "vtp", "dtp"}
|
|
623
|
+
l2_security_monitoring_signal = plan.network_style == "l2_security_monitoring" or capabilities & {"dhcp_snooping", "dai", "dot1x", "lldp", "rep", "snmp", "netflow", "span", "qos", "port_security"}
|
|
624
|
+
wireless_advanced_signal = plan.network_style == "wireless_advanced" or capabilities & {"wlc", "wpa_enterprise", "wep", "guest_wifi", "beamforming", "meraki", "cellular_5g", "bluetooth"}
|
|
625
|
+
automation_controller_signal = plan.network_style == "automation_controller" or capabilities & {"network_controller", "python_programming", "javascript_programming", "blockly_programming", "tcp_udp_app", "vm_iox"}
|
|
626
|
+
voice_collaboration_signal = plan.network_style == "voice_collaboration" or capabilities & {"voip", "ip_phone", "call_manager", "linksys_voice"}
|
|
627
|
+
industrial_iot_signal = plan.network_style == "industrial_iot" or capabilities & {"mqtt", "real_http", "real_websocket", "visual_scripting", "ptp", "profinet", "l2nat", "cyberobserver", "industrial_firewall"}
|
|
628
|
+
physical_media_device_signal = plan.network_style == "physical_media_device" or capabilities & {"coaxial", "cable_dsl", "central_office", "cell_tower", "power_distribution", "hot_swappable", "ios_license"}
|
|
629
|
+
service_signal = (
|
|
630
|
+
explicit_service_style
|
|
631
|
+
or requested_services & {"dns", "dhcp", "http", "https", "ftp", "tftp", "email", "syslog", "aaa", "ntp"}
|
|
632
|
+
or capabilities & service_capabilities
|
|
633
|
+
or "servers" in requested_family_set
|
|
634
|
+
)
|
|
635
|
+
if industrial_iot_signal:
|
|
636
|
+
return "industrial_iot"
|
|
637
|
+
if automation_controller_signal:
|
|
638
|
+
return "automation_controller"
|
|
639
|
+
if voice_collaboration_signal:
|
|
640
|
+
return "voice_collaboration"
|
|
641
|
+
if physical_media_device_signal:
|
|
642
|
+
return "physical_media_device"
|
|
643
|
+
if wireless_advanced_signal:
|
|
644
|
+
return "wireless_advanced"
|
|
645
|
+
if wan_signal:
|
|
287
646
|
return "wan_security_edge"
|
|
288
|
-
if
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
return "
|
|
294
|
-
if
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
647
|
+
if l2_resiliency_routing_signal:
|
|
648
|
+
return "l2_resiliency_routing"
|
|
649
|
+
if ipv4_routing_management_signal:
|
|
650
|
+
return "ipv4_routing_management"
|
|
651
|
+
if l2_security_monitoring_signal:
|
|
652
|
+
return "l2_security_monitoring"
|
|
653
|
+
if ipv6_routing_signal:
|
|
654
|
+
return "ipv6_routing"
|
|
655
|
+
if home_iot_signal:
|
|
656
|
+
return "home_iot"
|
|
657
|
+
if campus_signal:
|
|
299
658
|
return "campus"
|
|
659
|
+
if service_signal:
|
|
660
|
+
return "service_heavy"
|
|
300
661
|
return None
|
|
301
662
|
|
|
302
663
|
|
|
@@ -304,7 +665,16 @@ SCENARIO_CAPABILITY_SETS = {
|
|
|
304
665
|
"campus": {"router_on_a_stick", "trunk", "access_port", "management_vlan", "telnet", "verification", "acl", "nat"},
|
|
305
666
|
"service_heavy": {"server_dns", "server_dhcp", "server_http", "server_https", "server_ftp", "server_tftp", "server_email", "server_syslog", "server_aaa", "ntp"},
|
|
306
667
|
"home_iot": {"iot", "iot_registration", "iot_control", "wireless_ap", "wireless_mutation"},
|
|
307
|
-
"wan_security_edge": {"vpn", "ipsec", "gre", "ppp", "acl", "nat"},
|
|
668
|
+
"wan_security_edge": {"vpn", "ipsec", "gre", "ppp", "acl", "nat", "cbac", "zfw"},
|
|
669
|
+
"ipv6_routing": {"ipv6_slaac", "dhcpv6_stateful", "dhcpv6_stateless", "ipv6_prefix_delegation", "ipv6_dns_aaaa", "ipv6_tunneling", "isatap", "ospfv3", "eigrp_ipv6", "ripng", "hsrp"},
|
|
670
|
+
"l2_resiliency_routing": {"bgp", "stp", "rstp", "etherchannel", "lacp", "pagp", "vtp", "dtp"},
|
|
671
|
+
"ipv4_routing_management": {"ospfv2", "eigrp_ipv4", "ripv2", "static_route", "default_route", "dhcp_relay", "nat_static", "nat_dynamic", "pat", "ssh_ios", "ntp_ios", "syslog_ios"},
|
|
672
|
+
"l2_security_monitoring": {"dhcp_snooping", "dai", "dot1x", "lldp", "rep", "snmp", "netflow", "span", "qos", "port_security"},
|
|
673
|
+
"wireless_advanced": {"wlc", "wpa_enterprise", "wep", "guest_wifi", "beamforming", "meraki", "cellular_5g", "bluetooth"},
|
|
674
|
+
"automation_controller": {"network_controller", "python_programming", "javascript_programming", "blockly_programming", "tcp_udp_app", "vm_iox"},
|
|
675
|
+
"voice_collaboration": {"voip", "ip_phone", "call_manager", "linksys_voice"},
|
|
676
|
+
"industrial_iot": {"mqtt", "real_http", "real_websocket", "visual_scripting", "ptp", "profinet", "l2nat", "cyberobserver", "industrial_firewall"},
|
|
677
|
+
"physical_media_device": {"coaxial", "cable_dsl", "central_office", "cell_tower", "power_distribution", "hot_swappable", "ios_license"},
|
|
308
678
|
}
|
|
309
679
|
|
|
310
680
|
CAPABILITY_REQUIRED_ARCHETYPES = {
|
|
@@ -330,6 +700,76 @@ CAPABILITY_REQUIRED_ARCHETYPES = {
|
|
|
330
700
|
"ppp": ["WAN/security edge"],
|
|
331
701
|
"multilayer_switching": ["WAN/security edge", "campus/core"],
|
|
332
702
|
"security_edge": ["WAN/security edge"],
|
|
703
|
+
"cbac": ["WAN/security edge"],
|
|
704
|
+
"zfw": ["WAN/security edge"],
|
|
705
|
+
"ipv6_slaac": ["IPv6/routing"],
|
|
706
|
+
"dhcpv6_stateful": ["IPv6/routing"],
|
|
707
|
+
"dhcpv6_stateless": ["IPv6/routing"],
|
|
708
|
+
"ipv6_prefix_delegation": ["IPv6/routing"],
|
|
709
|
+
"ipv6_dns_aaaa": ["IPv6/routing"],
|
|
710
|
+
"ipv6_tunneling": ["IPv6/routing"],
|
|
711
|
+
"isatap": ["IPv6/routing"],
|
|
712
|
+
"ospfv3": ["IPv6/routing"],
|
|
713
|
+
"eigrp_ipv6": ["IPv6/routing"],
|
|
714
|
+
"ripng": ["IPv6/routing"],
|
|
715
|
+
"hsrp": ["IPv6/routing"],
|
|
716
|
+
"dhcp_snooping": ["L2 security/monitoring"],
|
|
717
|
+
"dai": ["L2 security/monitoring"],
|
|
718
|
+
"dot1x": ["L2 security/monitoring"],
|
|
719
|
+
"lldp": ["L2 security/monitoring"],
|
|
720
|
+
"rep": ["L2 security/monitoring"],
|
|
721
|
+
"snmp": ["L2 security/monitoring"],
|
|
722
|
+
"netflow": ["L2 security/monitoring"],
|
|
723
|
+
"span": ["L2 security/monitoring"],
|
|
724
|
+
"qos": ["L2 security/monitoring"],
|
|
725
|
+
"port_security": ["L2 security/monitoring"],
|
|
726
|
+
"bgp": ["L2 resiliency/routing"],
|
|
727
|
+
"stp": ["L2 resiliency/routing"],
|
|
728
|
+
"rstp": ["L2 resiliency/routing"],
|
|
729
|
+
"etherchannel": ["L2 resiliency/routing"],
|
|
730
|
+
"lacp": ["L2 resiliency/routing"],
|
|
731
|
+
"pagp": ["L2 resiliency/routing"],
|
|
732
|
+
"vtp": ["L2 resiliency/routing"],
|
|
733
|
+
"dtp": ["L2 resiliency/routing"],
|
|
734
|
+
"ospfv2": ["IPv4 routing/management"],
|
|
735
|
+
"eigrp_ipv4": ["IPv4 routing/management"],
|
|
736
|
+
"ripv2": ["IPv4 routing/management"],
|
|
737
|
+
"static_route": ["IPv4 routing/management"],
|
|
738
|
+
"default_route": ["IPv4 routing/management"],
|
|
739
|
+
"dhcp_relay": ["IPv4 routing/management"],
|
|
740
|
+
"nat_static": ["IPv4 routing/management"],
|
|
741
|
+
"nat_dynamic": ["IPv4 routing/management"],
|
|
742
|
+
"pat": ["IPv4 routing/management"],
|
|
743
|
+
"ssh_ios": ["IPv4 routing/management"],
|
|
744
|
+
"ntp_ios": ["IPv4 routing/management"],
|
|
745
|
+
"syslog_ios": ["IPv4 routing/management"],
|
|
746
|
+
"wlc": ["advanced wireless"],
|
|
747
|
+
"wpa_enterprise": ["advanced wireless"],
|
|
748
|
+
"wep": ["advanced wireless"],
|
|
749
|
+
"guest_wifi": ["advanced wireless"],
|
|
750
|
+
"beamforming": ["advanced wireless"],
|
|
751
|
+
"meraki": ["advanced wireless"],
|
|
752
|
+
"cellular_5g": ["advanced wireless"],
|
|
753
|
+
"bluetooth": ["advanced wireless"],
|
|
754
|
+
"network_controller": ["automation/controller"],
|
|
755
|
+
"python_programming": ["automation/controller"],
|
|
756
|
+
"javascript_programming": ["automation/controller"],
|
|
757
|
+
"blockly_programming": ["automation/controller"],
|
|
758
|
+
"tcp_udp_app": ["automation/controller"],
|
|
759
|
+
"vm_iox": ["automation/controller"],
|
|
760
|
+
"voip": ["voice/collaboration"],
|
|
761
|
+
"ip_phone": ["voice/collaboration"],
|
|
762
|
+
"call_manager": ["voice/collaboration"],
|
|
763
|
+
"linksys_voice": ["voice/collaboration"],
|
|
764
|
+
"mqtt": ["industrial IoT"],
|
|
765
|
+
"real_http": ["industrial IoT"],
|
|
766
|
+
"real_websocket": ["industrial IoT"],
|
|
767
|
+
"visual_scripting": ["industrial IoT"],
|
|
768
|
+
"ptp": ["industrial IoT"],
|
|
769
|
+
"profinet": ["industrial IoT"],
|
|
770
|
+
"l2nat": ["industrial IoT"],
|
|
771
|
+
"cyberobserver": ["industrial IoT"],
|
|
772
|
+
"industrial_firewall": ["industrial IoT"],
|
|
333
773
|
}
|
|
334
774
|
|
|
335
775
|
CAPABILITY_REQUIRED_RUNTIME_FEATURES = {
|
|
@@ -346,12 +786,44 @@ CAPABILITY_REQUIRED_RUNTIME_FEATURES = {
|
|
|
346
786
|
"wireless_client_association": ["workspace_validated", "wireless_runtime"],
|
|
347
787
|
"iot_registration": ["workspace_validated", "iot_runtime"],
|
|
348
788
|
"iot_control": ["workspace_validated", "iot_runtime"],
|
|
789
|
+
"vpn": ["workspace_validated", "tunnel_runtime"],
|
|
790
|
+
"ipsec": ["workspace_validated", "tunnel_runtime"],
|
|
791
|
+
"gre": ["workspace_validated", "tunnel_runtime"],
|
|
792
|
+
"ppp": ["workspace_validated", "wan_runtime"],
|
|
793
|
+
"security_edge": ["workspace_validated", "security_runtime"],
|
|
794
|
+
"cbac": ["workspace_validated", "security_runtime"],
|
|
795
|
+
"zfw": ["workspace_validated", "security_runtime"],
|
|
796
|
+
"multilayer_switching": ["workspace_validated", "multilayer_runtime"],
|
|
797
|
+
"ipv6_slaac": ["workspace_validated", "ipv6_runtime"],
|
|
798
|
+
"dhcpv6_stateful": ["workspace_validated", "ipv6_runtime"],
|
|
799
|
+
"ospfv3": ["workspace_validated", "ipv6_runtime"],
|
|
800
|
+
"eigrp_ipv6": ["workspace_validated", "ipv6_runtime"],
|
|
801
|
+
"ripng": ["workspace_validated", "ipv6_runtime"],
|
|
802
|
+
"hsrp": ["workspace_validated", "ipv6_runtime"],
|
|
803
|
+
"wep": ["workspace_validated", "wireless_runtime"],
|
|
804
|
+
"wpa_enterprise": ["workspace_validated", "wireless_runtime"],
|
|
805
|
+
"ospfv2": ["workspace_validated", "ipv4_routing_management_runtime"],
|
|
806
|
+
"eigrp_ipv4": ["workspace_validated", "ipv4_routing_management_runtime"],
|
|
807
|
+
"ripv2": ["workspace_validated", "ipv4_routing_management_runtime"],
|
|
808
|
+
"static_route": ["workspace_validated", "ipv4_routing_management_runtime"],
|
|
809
|
+
"default_route": ["workspace_validated", "ipv4_routing_management_runtime"],
|
|
810
|
+
"dhcp_relay": ["workspace_validated", "ipv4_routing_management_runtime"],
|
|
811
|
+
"nat_static": ["workspace_validated", "ipv4_routing_management_runtime"],
|
|
812
|
+
"nat_dynamic": ["workspace_validated", "ipv4_routing_management_runtime"],
|
|
813
|
+
"pat": ["workspace_validated", "ipv4_routing_management_runtime"],
|
|
814
|
+
"ssh_ios": ["workspace_validated", "ipv4_routing_management_runtime"],
|
|
815
|
+
"ntp_ios": ["workspace_validated", "ipv4_routing_management_runtime"],
|
|
816
|
+
"syslog_ios": ["workspace_validated", "ipv4_routing_management_runtime"],
|
|
349
817
|
}
|
|
350
818
|
|
|
351
819
|
|
|
352
820
|
def _generate_mismatch_reason(status: dict[str, object]) -> str | None:
|
|
821
|
+
if bool(status.get("report_only")):
|
|
822
|
+
return "report_only"
|
|
353
823
|
if not bool(status.get("inventory_supported")):
|
|
354
824
|
return "unsupported"
|
|
825
|
+
if bool(status.get("donor_limited_generate")):
|
|
826
|
+
return "supported_but_donor_limited"
|
|
355
827
|
if bool(status.get("edit_supported")) and not bool(status.get("safe_open_generate_supported")):
|
|
356
828
|
return "supported_in_edit_only"
|
|
357
829
|
if bool(status.get("safe_open_generate_supported")) and bool(status.get("requires_curated_donor")):
|
|
@@ -363,6 +835,327 @@ def _generate_mismatch_reason(status: dict[str, object]) -> str | None:
|
|
|
363
835
|
return "unsupported"
|
|
364
836
|
|
|
365
837
|
|
|
838
|
+
def _donor_backed_ready(status: dict[str, object]) -> bool:
|
|
839
|
+
return bool(status.get("donor_backed_ready"))
|
|
840
|
+
|
|
841
|
+
|
|
842
|
+
def _selected_sample_for_donor(
|
|
843
|
+
samples: list[SampleDescriptor],
|
|
844
|
+
selected_donor: str | None,
|
|
845
|
+
) -> SampleDescriptor | None:
|
|
846
|
+
if not selected_donor:
|
|
847
|
+
return None
|
|
848
|
+
needle = str(selected_donor).replace("\\", "/").lower()
|
|
849
|
+
for sample in samples:
|
|
850
|
+
candidates = {
|
|
851
|
+
str(sample.path).replace("\\", "/").lower(),
|
|
852
|
+
str(sample.relative_path).replace("\\", "/").lower(),
|
|
853
|
+
str(sample.path).replace("\\", "/").split("/")[-1].lower(),
|
|
854
|
+
str(sample.relative_path).replace("\\", "/").split("/")[-1].lower(),
|
|
855
|
+
}
|
|
856
|
+
if needle in candidates:
|
|
857
|
+
return sample
|
|
858
|
+
return None
|
|
859
|
+
|
|
860
|
+
|
|
861
|
+
def _has_explicit_iot_registration(plan: IntentPlan) -> bool:
|
|
862
|
+
return any(
|
|
863
|
+
op.get("op") == "set_iot_registration"
|
|
864
|
+
and str(op.get("device", "")).strip()
|
|
865
|
+
and str(op.get("target", "")).strip()
|
|
866
|
+
for op in plan.iot_ops
|
|
867
|
+
)
|
|
868
|
+
|
|
869
|
+
|
|
870
|
+
def _has_explicit_iot_control(plan: IntentPlan) -> bool:
|
|
871
|
+
return any(
|
|
872
|
+
op.get("op") == "set_iot_rule_state"
|
|
873
|
+
and str(op.get("device", "")).strip()
|
|
874
|
+
and str(op.get("rule_name", "")).strip()
|
|
875
|
+
for op in plan.iot_ops
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
def _has_explicit_wireless_association(plan: IntentPlan) -> bool:
|
|
880
|
+
return any(
|
|
881
|
+
op.get("op") == "associate_wireless_client"
|
|
882
|
+
and str(op.get("device", "")).strip()
|
|
883
|
+
and str(op.get("ap", "")).strip()
|
|
884
|
+
and str(op.get("ssid", "")).strip()
|
|
885
|
+
for op in plan.wireless_ops
|
|
886
|
+
)
|
|
887
|
+
|
|
888
|
+
|
|
889
|
+
def _has_explicit_advanced_wireless_edit(plan: IntentPlan, capability: str) -> bool:
|
|
890
|
+
for op in plan.wireless_ops:
|
|
891
|
+
if op.get("op") != "set_wireless_ssid":
|
|
892
|
+
continue
|
|
893
|
+
security = str(op.get("security") or "").strip().lower()
|
|
894
|
+
if capability == "wep" and security == "wep" and str(op.get("device") or "").strip() and str(op.get("ssid") or "").strip():
|
|
895
|
+
return True
|
|
896
|
+
if capability == "wpa_enterprise" and security in {"wpa-enterprise", "wpa2-enterprise", "802.1x"} and str(op.get("device") or "").strip() and str(op.get("ssid") or "").strip():
|
|
897
|
+
return True
|
|
898
|
+
return False
|
|
899
|
+
|
|
900
|
+
|
|
901
|
+
def _has_explicit_programming_edit(plan: IntentPlan, capability: str) -> bool:
|
|
902
|
+
for op in plan.programming_ops:
|
|
903
|
+
if op.get("op") != "set_script_file_content":
|
|
904
|
+
continue
|
|
905
|
+
if not (
|
|
906
|
+
str(op.get("device") or "").strip()
|
|
907
|
+
and str(op.get("app_name") or "").strip()
|
|
908
|
+
and str(op.get("file_name") or "").strip()
|
|
909
|
+
):
|
|
910
|
+
continue
|
|
911
|
+
file_name = str(op.get("file_name") or "").lower()
|
|
912
|
+
app_name = str(op.get("app_name") or "").lower()
|
|
913
|
+
content = str(op.get("content") or "").lower()
|
|
914
|
+
if capability == "python_programming" and (file_name.endswith(".py") or "python" in app_name):
|
|
915
|
+
return True
|
|
916
|
+
if capability == "javascript_programming" and (file_name.endswith(".js") or "javascript" in app_name):
|
|
917
|
+
return True
|
|
918
|
+
script_text = " ".join([file_name, app_name, content])
|
|
919
|
+
if capability == "tcp_udp_app" and ("tcp" in script_text or "udp" in script_text):
|
|
920
|
+
return True
|
|
921
|
+
if capability == "real_http" and ("realhttp" in content or "real http" in app_name):
|
|
922
|
+
return True
|
|
923
|
+
if capability == "real_websocket" and ("realws" in content or "websocket" in content or "websocket" in app_name):
|
|
924
|
+
return True
|
|
925
|
+
return False
|
|
926
|
+
|
|
927
|
+
|
|
928
|
+
def _has_explicit_l2_security_edit(plan: IntentPlan, capability: str) -> bool:
|
|
929
|
+
expected_ops = {
|
|
930
|
+
"dot1x": {"set_dot1x"},
|
|
931
|
+
"qos": {"set_qos_policy"},
|
|
932
|
+
}.get(capability, set())
|
|
933
|
+
if not expected_ops:
|
|
934
|
+
return False
|
|
935
|
+
for op in plan.switch_ops:
|
|
936
|
+
if str(op.get("op")) not in expected_ops:
|
|
937
|
+
continue
|
|
938
|
+
if not str(op.get("device") or "").strip():
|
|
939
|
+
continue
|
|
940
|
+
if capability == "dot1x":
|
|
941
|
+
return bool(str(op.get("interface") or "").strip() and str(op.get("mode") or "").strip())
|
|
942
|
+
if capability == "qos":
|
|
943
|
+
return bool(
|
|
944
|
+
str(op.get("interface") or "").strip()
|
|
945
|
+
and str(op.get("class_map") or "").strip()
|
|
946
|
+
and str(op.get("policy_map") or "").strip()
|
|
947
|
+
and str(op.get("direction") or "").strip()
|
|
948
|
+
)
|
|
949
|
+
return False
|
|
950
|
+
|
|
951
|
+
|
|
952
|
+
def _has_explicit_l2_resiliency_edit(plan: IntentPlan, capability: str) -> bool:
|
|
953
|
+
if capability == "bgp":
|
|
954
|
+
return any(
|
|
955
|
+
op.get("op") == "set_bgp_neighbor"
|
|
956
|
+
and str(op.get("device") or "").strip()
|
|
957
|
+
and str(op.get("neighbor") or "").strip()
|
|
958
|
+
and str(op.get("remote_as") or "").strip()
|
|
959
|
+
for op in plan.router_ops
|
|
960
|
+
)
|
|
961
|
+
for op in plan.switch_ops:
|
|
962
|
+
if not str(op.get("device") or "").strip():
|
|
963
|
+
continue
|
|
964
|
+
op_name = str(op.get("op"))
|
|
965
|
+
if capability == "stp" and op_name == "set_stp":
|
|
966
|
+
return bool(str(op.get("mode") or "").strip())
|
|
967
|
+
if capability == "rstp" and op_name == "set_stp":
|
|
968
|
+
return bool(re.search(r"(?:rapid|rstp)", str(op.get("mode") or ""), flags=re.IGNORECASE))
|
|
969
|
+
if capability == "etherchannel" and op_name == "set_etherchannel":
|
|
970
|
+
return bool(op.get("interfaces"))
|
|
971
|
+
if capability == "lacp" and op_name == "set_etherchannel":
|
|
972
|
+
if str(op.get("mode") or "").lower() in {"active", "passive"} and bool(op.get("interfaces")):
|
|
973
|
+
return True
|
|
974
|
+
continue
|
|
975
|
+
if capability == "pagp" and op_name == "set_etherchannel":
|
|
976
|
+
if str(op.get("mode") or "").lower() in {"desirable", "auto"} and bool(op.get("interfaces")):
|
|
977
|
+
return True
|
|
978
|
+
continue
|
|
979
|
+
if capability == "vtp" and op_name == "set_vtp":
|
|
980
|
+
return bool(str(op.get("domain") or "").strip() and str(op.get("mode") or "").strip())
|
|
981
|
+
if capability == "dtp" and op_name == "set_dtp":
|
|
982
|
+
return bool(str(op.get("interface") or "").strip() and str(op.get("mode") or "").strip())
|
|
983
|
+
return False
|
|
984
|
+
|
|
985
|
+
|
|
986
|
+
def _has_explicit_ipv4_routing_management_edit(plan: IntentPlan, capability: str) -> bool:
|
|
987
|
+
operation_map = {
|
|
988
|
+
"ospfv2": {"set_ospfv2_network"},
|
|
989
|
+
"eigrp_ipv4": {"set_eigrp_ipv4_network"},
|
|
990
|
+
"ripv2": {"set_ripv2_network"},
|
|
991
|
+
"static_route": {"set_static_route"},
|
|
992
|
+
"default_route": {"set_static_route"},
|
|
993
|
+
"dhcp_relay": {"set_dhcp_relay"},
|
|
994
|
+
"nat_static": {"set_nat_static"},
|
|
995
|
+
"nat_dynamic": {"set_nat_interface"},
|
|
996
|
+
"pat": {"set_pat_overload"},
|
|
997
|
+
"ssh_ios": {"set_ssh_ios"},
|
|
998
|
+
"ntp_ios": {"set_ntp_server"},
|
|
999
|
+
"syslog_ios": {"set_syslog_server"},
|
|
1000
|
+
}
|
|
1001
|
+
expected_ops = operation_map.get(capability, set())
|
|
1002
|
+
if not expected_ops:
|
|
1003
|
+
return False
|
|
1004
|
+
for op in plan.router_ops:
|
|
1005
|
+
if str(op.get("op")) not in expected_ops:
|
|
1006
|
+
continue
|
|
1007
|
+
if not str(op.get("device") or "").strip():
|
|
1008
|
+
continue
|
|
1009
|
+
if capability == "default_route":
|
|
1010
|
+
return op.get("network") == "0.0.0.0" and op.get("prefix") == 0
|
|
1011
|
+
if capability == "nat_dynamic":
|
|
1012
|
+
return str(op.get("role") or "").lower() in {"inside", "outside"} and str(op.get("interface") or "").strip()
|
|
1013
|
+
return True
|
|
1014
|
+
return False
|
|
1015
|
+
|
|
1016
|
+
|
|
1017
|
+
def _has_explicit_wan_security_edit(plan: IntentPlan, capability: str) -> bool:
|
|
1018
|
+
expected_ops = {
|
|
1019
|
+
"gre": {"set_gre_tunnel"},
|
|
1020
|
+
"ppp": {"set_ppp_interface"},
|
|
1021
|
+
"ipsec": {"set_ipsec_transform_set", "set_crypto_map"},
|
|
1022
|
+
"vpn": {"set_crypto_map"},
|
|
1023
|
+
"cbac": {"set_cbac_inspect"},
|
|
1024
|
+
"zfw": {"set_zfw_zone_interface", "set_zfw_zone_pair", "set_zfw_policy"},
|
|
1025
|
+
}.get(capability, set())
|
|
1026
|
+
if not expected_ops:
|
|
1027
|
+
return False
|
|
1028
|
+
for op in plan.router_ops:
|
|
1029
|
+
if str(op.get("op")) not in expected_ops:
|
|
1030
|
+
continue
|
|
1031
|
+
if not str(op.get("device") or "").strip():
|
|
1032
|
+
continue
|
|
1033
|
+
if capability == "gre":
|
|
1034
|
+
return bool(str(op.get("interface") or "").strip() and str(op.get("source") or "").strip() and str(op.get("destination") or "").strip())
|
|
1035
|
+
if capability == "ppp":
|
|
1036
|
+
return bool(str(op.get("interface") or "").strip())
|
|
1037
|
+
if capability == "ipsec":
|
|
1038
|
+
if op.get("op") == "set_ipsec_transform_set":
|
|
1039
|
+
return bool(str(op.get("name") or "").strip() and str(op.get("encryption") or "").strip() and str(op.get("integrity") or "").strip())
|
|
1040
|
+
return bool(str(op.get("map_name") or "").strip() and str(op.get("transform_set") or "").strip())
|
|
1041
|
+
if capability == "vpn":
|
|
1042
|
+
return bool(str(op.get("map_name") or "").strip() and str(op.get("peer") or "").strip())
|
|
1043
|
+
if capability == "cbac":
|
|
1044
|
+
return bool(str(op.get("name") or "").strip() and str(op.get("protocol") or "").strip() and str(op.get("interface") or "").strip())
|
|
1045
|
+
if capability == "zfw":
|
|
1046
|
+
if op.get("op") == "set_zfw_zone_interface":
|
|
1047
|
+
return bool(str(op.get("zone") or "").strip() and str(op.get("interface") or "").strip())
|
|
1048
|
+
if op.get("op") == "set_zfw_zone_pair":
|
|
1049
|
+
return bool(str(op.get("pair_name") or "").strip() and str(op.get("source") or "").strip() and str(op.get("destination") or "").strip())
|
|
1050
|
+
return bool(str(op.get("class_map") or "").strip() and str(op.get("policy_map") or "").strip())
|
|
1051
|
+
return False
|
|
1052
|
+
|
|
1053
|
+
|
|
1054
|
+
def _has_explicit_wan_security_intent(plan: IntentPlan) -> bool:
|
|
1055
|
+
capabilities = set(plan.capabilities)
|
|
1056
|
+
prompt_lower = str(plan.prompt or "").lower()
|
|
1057
|
+
return (
|
|
1058
|
+
plan.network_style == "wan_security"
|
|
1059
|
+
or bool(capabilities & {"vpn", "ipsec", "gre", "ppp", "security_edge", "multilayer_switching"})
|
|
1060
|
+
or any(token in prompt_lower for token in ["site-to-site", "firewall", "asa", "cloud", "multilayer"])
|
|
1061
|
+
)
|
|
1062
|
+
|
|
1063
|
+
|
|
1064
|
+
def _has_explicit_voice_edit(plan: IntentPlan, capability: str) -> bool:
|
|
1065
|
+
op_names = {str(op.get("op")) for op in plan.router_ops}
|
|
1066
|
+
if capability == "voip":
|
|
1067
|
+
return bool(op_names & {"set_telephony_service", "set_ephone_dn", "set_ephone", "set_dial_peer_voice"})
|
|
1068
|
+
if capability == "call_manager":
|
|
1069
|
+
return bool(op_names & {"set_telephony_service", "set_ephone_dn", "set_ephone"})
|
|
1070
|
+
if capability == "ip_phone":
|
|
1071
|
+
return bool(op_names & {"set_ephone_dn", "set_ephone"})
|
|
1072
|
+
return False
|
|
1073
|
+
|
|
1074
|
+
|
|
1075
|
+
def _has_explicit_ipv6_operation(plan: IntentPlan, capability: str) -> bool:
|
|
1076
|
+
operation_map = {
|
|
1077
|
+
"ipv6_slaac": {"enable_ipv6_unicast_routing", "set_ipv6_address", "set_ipv6_slaac"},
|
|
1078
|
+
"dhcpv6_stateful": {"set_dhcpv6_pool"},
|
|
1079
|
+
"ospfv3": {"set_ospfv3_interface"},
|
|
1080
|
+
"eigrp_ipv6": {"set_eigrp_ipv6_interface"},
|
|
1081
|
+
"ripng": {"set_ripng_interface"},
|
|
1082
|
+
"hsrp": {"set_hsrp_ipv6"},
|
|
1083
|
+
}
|
|
1084
|
+
expected_ops = operation_map.get(capability, set())
|
|
1085
|
+
return any(str(op.get("op")) in expected_ops for op in plan.router_ops)
|
|
1086
|
+
|
|
1087
|
+
|
|
1088
|
+
def _selected_donor_capability_override(
|
|
1089
|
+
plan: IntentPlan,
|
|
1090
|
+
capability: str,
|
|
1091
|
+
selected_sample: SampleDescriptor | None,
|
|
1092
|
+
) -> bool:
|
|
1093
|
+
if selected_sample is None:
|
|
1094
|
+
return False
|
|
1095
|
+
device_families = sorted({_device_family_for_type(device.get("type", "")) for device in selected_sample.devices if device.get("type")})
|
|
1096
|
+
sample_capabilities = set(_expanded_sample_capabilities(selected_sample, device_families))
|
|
1097
|
+
sample_capabilities.update(str(item) for item in selected_sample.validated_edit_capabilities if str(item).strip())
|
|
1098
|
+
archetypes = {str(item) for item in selected_sample.archetype_tags if str(item).strip()}
|
|
1099
|
+
runtime_features = {str(item) for item in selected_sample.runtime_features if str(item).strip()}
|
|
1100
|
+
|
|
1101
|
+
if capability == "iot":
|
|
1102
|
+
return (
|
|
1103
|
+
bool(archetypes & {"IoT/home gateway"})
|
|
1104
|
+
and bool(sample_capabilities & {"iot", "iot_registration", "iot_control"})
|
|
1105
|
+
and bool(runtime_features & {"iot_runtime"})
|
|
1106
|
+
and (_has_explicit_iot_registration(plan) or _has_explicit_iot_control(plan))
|
|
1107
|
+
)
|
|
1108
|
+
if capability == "iot_registration":
|
|
1109
|
+
return (
|
|
1110
|
+
"IoT/home gateway" in archetypes
|
|
1111
|
+
and "iot_registration" in sample_capabilities
|
|
1112
|
+
and "iot_runtime" in runtime_features
|
|
1113
|
+
and _has_explicit_iot_registration(plan)
|
|
1114
|
+
)
|
|
1115
|
+
if capability == "iot_control":
|
|
1116
|
+
return (
|
|
1117
|
+
"IoT/home gateway" in archetypes
|
|
1118
|
+
and "iot_control" in sample_capabilities
|
|
1119
|
+
and "iot_runtime" in runtime_features
|
|
1120
|
+
and _has_explicit_iot_control(plan)
|
|
1121
|
+
)
|
|
1122
|
+
if capability == "wireless_client_association":
|
|
1123
|
+
return (
|
|
1124
|
+
bool(archetypes & {"IoT/home gateway", "wireless-heavy"})
|
|
1125
|
+
and "wireless_client_association" in sample_capabilities
|
|
1126
|
+
and "wireless_runtime" in runtime_features
|
|
1127
|
+
and _has_explicit_wireless_association(plan)
|
|
1128
|
+
)
|
|
1129
|
+
wan_runtime_by_capability = {
|
|
1130
|
+
"vpn": {"tunnel_runtime", "security_runtime", "wan_runtime"},
|
|
1131
|
+
"ipsec": {"tunnel_runtime", "security_runtime"},
|
|
1132
|
+
"gre": {"tunnel_runtime", "wan_runtime"},
|
|
1133
|
+
"ppp": {"wan_runtime"},
|
|
1134
|
+
"security_edge": {"security_runtime"},
|
|
1135
|
+
"multilayer_switching": {"multilayer_runtime"},
|
|
1136
|
+
}
|
|
1137
|
+
if capability in wan_runtime_by_capability:
|
|
1138
|
+
has_acceptance_backed_wan_generate = (
|
|
1139
|
+
bool(selected_sample.acceptance_fixtures)
|
|
1140
|
+
and selected_sample.apply_safety_level in {"acceptance-verified", "safe-open-generate-supported"}
|
|
1141
|
+
)
|
|
1142
|
+
return (
|
|
1143
|
+
"WAN/security edge" in archetypes
|
|
1144
|
+
and capability in sample_capabilities
|
|
1145
|
+
and bool(runtime_features & wan_runtime_by_capability[capability])
|
|
1146
|
+
and has_acceptance_backed_wan_generate
|
|
1147
|
+
and _has_explicit_wan_security_intent(plan)
|
|
1148
|
+
)
|
|
1149
|
+
if capability in IPV6_DONOR_BACKED_CAPABILITIES:
|
|
1150
|
+
return (
|
|
1151
|
+
"IPv6/routing" in archetypes
|
|
1152
|
+
and capability in sample_capabilities
|
|
1153
|
+
and "ipv6_runtime" in runtime_features
|
|
1154
|
+
and _has_explicit_ipv6_operation(plan, capability)
|
|
1155
|
+
)
|
|
1156
|
+
return False
|
|
1157
|
+
|
|
1158
|
+
|
|
366
1159
|
def _best_maturity_level(status: dict[str, object]) -> str:
|
|
367
1160
|
level = str(status.get("best_maturity_level") or "").strip()
|
|
368
1161
|
if level in MATURITY_RANK:
|
|
@@ -381,6 +1174,60 @@ def _best_maturity_level(status: dict[str, object]) -> str:
|
|
|
381
1174
|
|
|
382
1175
|
|
|
383
1176
|
def _recommended_next_action_for_capability(capability: str, mismatch_reason: str | None) -> str:
|
|
1177
|
+
if mismatch_reason == "report_only":
|
|
1178
|
+
return f"Keep {capability} in atlas/report mode until donor-backed edit or generate evidence exists."
|
|
1179
|
+
if capability in {"vpn", "ipsec", "gre", "ppp", "security_edge", "multilayer_switching", "cbac", "zfw"}:
|
|
1180
|
+
if mismatch_reason == "supported_in_edit_only":
|
|
1181
|
+
return f"Use explicit WAN/security edit commands for {capability}; strict prompt generate still needs an acceptance-backed WAN/security donor."
|
|
1182
|
+
if mismatch_reason == "supported_but_donor_limited":
|
|
1183
|
+
return f"Provide a WAN/security edge donor with reusable ASA/cloud/serial or tunnel skeleton for {capability}."
|
|
1184
|
+
if mismatch_reason == "supported_but_acceptance_gated":
|
|
1185
|
+
return f"Keep {capability} in report/selection flow until a WAN/security edge donor is acceptance-backed."
|
|
1186
|
+
return f"Prefer a WAN/security edge donor with explicit {capability} evidence before strict generate."
|
|
1187
|
+
if capability in IPV6_DONOR_BACKED_CAPABILITIES:
|
|
1188
|
+
if mismatch_reason == "supported_in_edit_only":
|
|
1189
|
+
return f"Use explicit edit commands for {capability}; strict prompt generate still needs a selected IPv6/routing donor."
|
|
1190
|
+
if mismatch_reason == "supported_but_donor_limited":
|
|
1191
|
+
return f"Provide an IPv6/routing donor with reusable IPv6 interface and routing skeleton for {capability}."
|
|
1192
|
+
if mismatch_reason == "supported_but_acceptance_gated":
|
|
1193
|
+
return f"Keep {capability} in report/edit flow until an IPv6/routing donor is acceptance-backed."
|
|
1194
|
+
return f"Prefer an IPv6/routing donor and explicit router/interface targets before strict generate for {capability}."
|
|
1195
|
+
if capability in L2_SECURITY_MONITORING_EDIT_PROVEN_CAPABILITIES:
|
|
1196
|
+
if mismatch_reason == "supported_in_edit_only":
|
|
1197
|
+
return f"Use explicit edit commands for {capability}; strict prompt generate still needs an acceptance-backed L2 security/monitoring donor."
|
|
1198
|
+
if mismatch_reason == "supported_but_donor_limited":
|
|
1199
|
+
return f"Provide an L2 security/monitoring donor with reusable switch/router monitoring skeleton for {capability}."
|
|
1200
|
+
return f"Keep {capability} constrained to edit/inventory proof until an L2 security/monitoring donor is acceptance-backed."
|
|
1201
|
+
if capability in WIRELESS_ADVANCED_EDIT_PROVEN_CAPABILITIES:
|
|
1202
|
+
if mismatch_reason == "supported_in_edit_only":
|
|
1203
|
+
return f"Use explicit wireless edit commands for {capability}; strict prompt generate still needs an acceptance-backed advanced wireless donor."
|
|
1204
|
+
if mismatch_reason == "supported_but_donor_limited":
|
|
1205
|
+
return f"Provide an advanced wireless donor with reusable AP/router/WLC skeleton for {capability}."
|
|
1206
|
+
return f"Keep {capability} constrained to explicit wireless edit proof until an advanced wireless donor is acceptance-backed."
|
|
1207
|
+
if capability in VOICE_COLLABORATION_EDIT_PROVEN_CAPABILITIES:
|
|
1208
|
+
if mismatch_reason == "supported_in_edit_only":
|
|
1209
|
+
return f"Use explicit voice IOS edit commands for {capability}; strict prompt generate still needs an acceptance-backed voice/collaboration donor."
|
|
1210
|
+
if mismatch_reason == "supported_but_donor_limited":
|
|
1211
|
+
return f"Provide a voice/collaboration donor with reusable telephony-service, ephone, or dial-peer skeleton for {capability}."
|
|
1212
|
+
return f"Keep {capability} constrained to voice inventory/edit proof until a voice/collaboration donor is acceptance-backed."
|
|
1213
|
+
if capability in AUTOMATION_CONTROLLER_EDIT_PROVEN_CAPABILITIES:
|
|
1214
|
+
if mismatch_reason == "supported_in_edit_only":
|
|
1215
|
+
return f"Use explicit script-file edit commands for {capability}; strict prompt generate still needs an acceptance-backed automation/controller donor."
|
|
1216
|
+
if mismatch_reason == "supported_but_donor_limited":
|
|
1217
|
+
return f"Provide an automation/controller donor with reusable existing app/file script surfaces for {capability}."
|
|
1218
|
+
return f"Keep {capability} constrained to automation inventory/edit proof until an automation/controller donor is acceptance-backed."
|
|
1219
|
+
if capability == "iot_registration":
|
|
1220
|
+
if mismatch_reason == "supported_but_acceptance_gated":
|
|
1221
|
+
return "Use an IoT/home gateway donor and explicitly name the thing plus gateway/server target before strict generate."
|
|
1222
|
+
return "Prefer an IoT/home gateway donor and explicitly name the thing plus gateway/server target."
|
|
1223
|
+
if capability == "iot_control":
|
|
1224
|
+
if mismatch_reason == "supported_but_acceptance_gated":
|
|
1225
|
+
return "Use a donor that already contains IoT rules and explicitly name the rule-bearing device before strict generate."
|
|
1226
|
+
return "Prefer an IoT/home gateway donor with existing rule/control structure before strict generate."
|
|
1227
|
+
if capability == "wireless_client_association":
|
|
1228
|
+
if mismatch_reason == "supported_but_acceptance_gated":
|
|
1229
|
+
return "Use a donor-backed wireless prompt that explicitly names the client, AP/router, and SSID before strict generate."
|
|
1230
|
+
return "Prefer a wireless-heavy or IoT/home gateway donor and explicitly name the client, AP/router, and SSID."
|
|
384
1231
|
if mismatch_reason == "supported_in_edit_only":
|
|
385
1232
|
return f"Use donor-backed edit flow for {capability} until safe-open generate coverage is raised."
|
|
386
1233
|
if mismatch_reason == "supported_but_donor_limited":
|
|
@@ -528,6 +1375,7 @@ def build_coverage_gap_report(
|
|
|
528
1375
|
selected_donor: str | None = None,
|
|
529
1376
|
) -> CoverageGapReport:
|
|
530
1377
|
iot_acceptance_gated = {"iot", "iot_registration", "iot_control"}
|
|
1378
|
+
selected_sample = _selected_sample_for_donor(samples, selected_donor)
|
|
531
1379
|
matrix = build_capability_matrix(samples)
|
|
532
1380
|
by_capability: dict[str, list[CapabilityMatrixEntry]] = {}
|
|
533
1381
|
by_family: dict[str, list[CapabilityMatrixEntry]] = {}
|
|
@@ -544,6 +1392,79 @@ def build_coverage_gap_report(
|
|
|
544
1392
|
capability_statuses: list[dict[str, object]] = []
|
|
545
1393
|
for capability in requested_capabilities:
|
|
546
1394
|
provider_families = _provider_families_for_capability(capability, requested_families)
|
|
1395
|
+
selected_donor_backed = _selected_donor_capability_override(plan, capability, selected_sample)
|
|
1396
|
+
wireless_edit_proven_only = (
|
|
1397
|
+
capability in WIRELESS_ADVANCED_EDIT_PROVEN_CAPABILITIES
|
|
1398
|
+
and _has_explicit_advanced_wireless_edit(plan, capability)
|
|
1399
|
+
and not selected_donor_backed
|
|
1400
|
+
)
|
|
1401
|
+
programming_edit_proven_only = (
|
|
1402
|
+
capability in PROGRAMMING_EDIT_PROVEN_CAPABILITIES
|
|
1403
|
+
and _has_explicit_programming_edit(plan, capability)
|
|
1404
|
+
and not selected_donor_backed
|
|
1405
|
+
)
|
|
1406
|
+
programming_donor_backed_ready = (
|
|
1407
|
+
capability in PROGRAMMING_DONOR_BACKED_READY_CAPABILITIES
|
|
1408
|
+
and programming_edit_proven_only
|
|
1409
|
+
)
|
|
1410
|
+
voice_edit_proven_only = (
|
|
1411
|
+
capability in VOICE_COLLABORATION_EDIT_PROVEN_CAPABILITIES
|
|
1412
|
+
and _has_explicit_voice_edit(plan, capability)
|
|
1413
|
+
and not selected_donor_backed
|
|
1414
|
+
)
|
|
1415
|
+
voice_donor_backed_ready = (
|
|
1416
|
+
capability in VOICE_COLLABORATION_DONOR_BACKED_READY_CAPABILITIES
|
|
1417
|
+
and voice_edit_proven_only
|
|
1418
|
+
)
|
|
1419
|
+
l2_security_edit_proven_only = (
|
|
1420
|
+
capability in {"dot1x", "qos"}
|
|
1421
|
+
and _has_explicit_l2_security_edit(plan, capability)
|
|
1422
|
+
and not selected_donor_backed
|
|
1423
|
+
)
|
|
1424
|
+
l2_security_donor_backed_ready = (
|
|
1425
|
+
capability in L2_SECURITY_MONITORING_DONOR_BACKED_READY_CAPABILITIES
|
|
1426
|
+
and l2_security_edit_proven_only
|
|
1427
|
+
)
|
|
1428
|
+
wan_security_edit_proven_only = (
|
|
1429
|
+
capability in WAN_SECURITY_EDIT_PROVEN_CAPABILITIES
|
|
1430
|
+
and _has_explicit_wan_security_edit(plan, capability)
|
|
1431
|
+
and not selected_donor_backed
|
|
1432
|
+
)
|
|
1433
|
+
l2_resiliency_edit_proven_only = (
|
|
1434
|
+
capability in L2_RESILIENCY_ROUTING_EDIT_PROVEN_CAPABILITIES
|
|
1435
|
+
and _has_explicit_l2_resiliency_edit(plan, capability)
|
|
1436
|
+
and not selected_donor_backed
|
|
1437
|
+
)
|
|
1438
|
+
ipv4_routing_management_edit_proven_only = (
|
|
1439
|
+
capability in IPV4_ROUTING_MANAGEMENT_EDIT_PROVEN_CAPABILITIES
|
|
1440
|
+
and _has_explicit_ipv4_routing_management_edit(plan, capability)
|
|
1441
|
+
and not selected_donor_backed
|
|
1442
|
+
)
|
|
1443
|
+
wan_security_donor_backed_ready = (
|
|
1444
|
+
capability in WAN_SECURITY_DONOR_BACKED_READY_EDIT_CAPABILITIES
|
|
1445
|
+
and wan_security_edit_proven_only
|
|
1446
|
+
)
|
|
1447
|
+
wan_security_donor_limited = (
|
|
1448
|
+
capability in WAN_SECURITY_DONOR_LIMITED_CAPABILITIES
|
|
1449
|
+
and _has_explicit_wan_security_intent(plan)
|
|
1450
|
+
and not selected_donor_backed
|
|
1451
|
+
and not wan_security_edit_proven_only
|
|
1452
|
+
)
|
|
1453
|
+
edit_proven_only = (
|
|
1454
|
+
capability in EDIT_PROVEN_REPORT_CAPABILITIES
|
|
1455
|
+
or wireless_edit_proven_only
|
|
1456
|
+
or programming_edit_proven_only
|
|
1457
|
+
or voice_edit_proven_only
|
|
1458
|
+
or l2_security_edit_proven_only
|
|
1459
|
+
or wan_security_edit_proven_only
|
|
1460
|
+
or l2_resiliency_edit_proven_only
|
|
1461
|
+
or ipv4_routing_management_edit_proven_only
|
|
1462
|
+
) and not selected_donor_backed
|
|
1463
|
+
ipv6_donor_backed_ready = (
|
|
1464
|
+
capability in IPV6_DONOR_BACKED_READY_EDIT_CAPABILITIES
|
|
1465
|
+
and edit_proven_only
|
|
1466
|
+
)
|
|
1467
|
+
report_only = capability in REPORT_ONLY_CAPABILITIES and not selected_donor_backed and not edit_proven_only
|
|
547
1468
|
matching_entries = [
|
|
548
1469
|
entry
|
|
549
1470
|
for entry in by_capability.get(capability, [])
|
|
@@ -552,8 +1473,18 @@ def build_coverage_gap_report(
|
|
|
552
1473
|
if matching_entries:
|
|
553
1474
|
supported_capabilities.append(capability)
|
|
554
1475
|
best_entry = max(matching_entries, key=lambda entry: MATURITY_RANK[entry.maturity_level])
|
|
555
|
-
acceptance_verified =
|
|
556
|
-
|
|
1476
|
+
acceptance_verified = (
|
|
1477
|
+
(any(entry.maturity_level == "acceptance-verified" for entry in matching_entries) and capability not in iot_acceptance_gated)
|
|
1478
|
+
or selected_donor_backed
|
|
1479
|
+
)
|
|
1480
|
+
if report_only or edit_proven_only or wan_security_donor_limited:
|
|
1481
|
+
acceptance_verified = False
|
|
1482
|
+
requires_curated = (
|
|
1483
|
+
best_entry.maturity_level == "safe-open-generate-supported"
|
|
1484
|
+
and not selected_donor_backed
|
|
1485
|
+
and not edit_proven_only
|
|
1486
|
+
) or wan_security_donor_limited
|
|
1487
|
+
if requires_curated:
|
|
557
1488
|
requires_curated_donor.append(capability)
|
|
558
1489
|
if not acceptance_verified:
|
|
559
1490
|
requires_manual_acceptance.append(capability)
|
|
@@ -562,18 +1493,101 @@ def build_coverage_gap_report(
|
|
|
562
1493
|
"capability": capability,
|
|
563
1494
|
"provider_families": sorted(provider_families),
|
|
564
1495
|
"matching_device_families": sorted({entry.device_family for entry in matching_entries}),
|
|
565
|
-
"best_maturity_level": best_entry.maturity_level,
|
|
1496
|
+
"best_maturity_level": "inventory-supported" if report_only else "config-mutation-supported" if edit_proven_only else best_entry.maturity_level,
|
|
566
1497
|
"inventory_supported": any(MATURITY_RANK[entry.maturity_level] >= MATURITY_RANK["inventory-supported"] for entry in matching_entries),
|
|
567
|
-
"edit_supported": any(MATURITY_RANK[entry.maturity_level] >= MATURITY_RANK["config-mutation-supported"] for entry in matching_entries),
|
|
568
|
-
"config_mutation_supported": any(MATURITY_RANK[entry.maturity_level] >= MATURITY_RANK["config-mutation-supported"] for entry in matching_entries),
|
|
569
|
-
"safe_open_generate_supported": any(MATURITY_RANK[entry.maturity_level] >= MATURITY_RANK["safe-open-generate-supported"] for entry in matching_entries),
|
|
1498
|
+
"edit_supported": True if edit_proven_only else False if report_only else any(MATURITY_RANK[entry.maturity_level] >= MATURITY_RANK["config-mutation-supported"] for entry in matching_entries),
|
|
1499
|
+
"config_mutation_supported": True if edit_proven_only else False if report_only else any(MATURITY_RANK[entry.maturity_level] >= MATURITY_RANK["config-mutation-supported"] for entry in matching_entries),
|
|
1500
|
+
"safe_open_generate_supported": False if report_only or edit_proven_only or wan_security_donor_limited else any(MATURITY_RANK[entry.maturity_level] >= MATURITY_RANK["safe-open-generate-supported"] for entry in matching_entries),
|
|
570
1501
|
"acceptance_verified": acceptance_verified,
|
|
571
|
-
"
|
|
1502
|
+
"donor_backed_ready": bool(
|
|
1503
|
+
selected_donor_backed
|
|
1504
|
+
or ipv6_donor_backed_ready
|
|
1505
|
+
or programming_donor_backed_ready
|
|
1506
|
+
or voice_donor_backed_ready
|
|
1507
|
+
or l2_security_donor_backed_ready
|
|
1508
|
+
or wan_security_donor_backed_ready
|
|
1509
|
+
),
|
|
1510
|
+
"requires_curated_donor": requires_curated,
|
|
572
1511
|
"requires_manual_acceptance": not acceptance_verified,
|
|
1512
|
+
"report_only": report_only,
|
|
1513
|
+
"donor_limited_generate": wan_security_donor_limited,
|
|
573
1514
|
"recommended_donors": list(dict.fromkeys(donor for entry in matching_entries for donor in entry.accepted_donors))[:10],
|
|
574
1515
|
"known_limitations": list(dict.fromkeys(item for entry in matching_entries for item in entry.known_limitations)),
|
|
575
1516
|
}
|
|
576
1517
|
)
|
|
1518
|
+
elif edit_proven_only:
|
|
1519
|
+
supported_capabilities.append(capability)
|
|
1520
|
+
requires_manual_acceptance.append(capability)
|
|
1521
|
+
capability_statuses.append(
|
|
1522
|
+
{
|
|
1523
|
+
"capability": capability,
|
|
1524
|
+
"provider_families": sorted(provider_families),
|
|
1525
|
+
"matching_device_families": sorted(provider_families),
|
|
1526
|
+
"best_maturity_level": "config-mutation-supported",
|
|
1527
|
+
"inventory_supported": True,
|
|
1528
|
+
"edit_supported": True,
|
|
1529
|
+
"config_mutation_supported": True,
|
|
1530
|
+
"safe_open_generate_supported": False,
|
|
1531
|
+
"acceptance_verified": False,
|
|
1532
|
+
"donor_backed_ready": bool(
|
|
1533
|
+
programming_donor_backed_ready
|
|
1534
|
+
or ipv6_donor_backed_ready
|
|
1535
|
+
or voice_donor_backed_ready
|
|
1536
|
+
or l2_security_donor_backed_ready
|
|
1537
|
+
or wan_security_donor_backed_ready
|
|
1538
|
+
),
|
|
1539
|
+
"requires_curated_donor": False,
|
|
1540
|
+
"requires_manual_acceptance": True,
|
|
1541
|
+
"report_only": False,
|
|
1542
|
+
"donor_limited_generate": False,
|
|
1543
|
+
"recommended_donors": [],
|
|
1544
|
+
"known_limitations": ["editor-proven without sample-backed donor selection"],
|
|
1545
|
+
}
|
|
1546
|
+
)
|
|
1547
|
+
elif report_only:
|
|
1548
|
+
supported_capabilities.append(capability)
|
|
1549
|
+
requires_manual_acceptance.append(capability)
|
|
1550
|
+
capability_statuses.append(
|
|
1551
|
+
{
|
|
1552
|
+
"capability": capability,
|
|
1553
|
+
"provider_families": sorted(provider_families),
|
|
1554
|
+
"matching_device_families": sorted(provider_families),
|
|
1555
|
+
"best_maturity_level": "inventory-supported",
|
|
1556
|
+
"inventory_supported": True,
|
|
1557
|
+
"edit_supported": False,
|
|
1558
|
+
"config_mutation_supported": False,
|
|
1559
|
+
"safe_open_generate_supported": False,
|
|
1560
|
+
"acceptance_verified": False,
|
|
1561
|
+
"donor_backed_ready": False,
|
|
1562
|
+
"requires_curated_donor": False,
|
|
1563
|
+
"requires_manual_acceptance": True,
|
|
1564
|
+
"report_only": True,
|
|
1565
|
+
"donor_limited_generate": False,
|
|
1566
|
+
"recommended_donors": [],
|
|
1567
|
+
"known_limitations": ["report-supported without decode-backed donor selection"],
|
|
1568
|
+
}
|
|
1569
|
+
)
|
|
1570
|
+
elif selected_donor_backed and selected_sample is not None:
|
|
1571
|
+
supported_capabilities.append(capability)
|
|
1572
|
+
capability_statuses.append(
|
|
1573
|
+
{
|
|
1574
|
+
"capability": capability,
|
|
1575
|
+
"provider_families": sorted(provider_families),
|
|
1576
|
+
"matching_device_families": sorted({family for family in (selected_sample.device_families or []) if not provider_families or family in provider_families}),
|
|
1577
|
+
"best_maturity_level": selected_sample.apply_safety_level or "safe-open-generate-supported",
|
|
1578
|
+
"inventory_supported": True,
|
|
1579
|
+
"edit_supported": True,
|
|
1580
|
+
"config_mutation_supported": True,
|
|
1581
|
+
"safe_open_generate_supported": True,
|
|
1582
|
+
"acceptance_verified": True,
|
|
1583
|
+
"donor_backed_ready": True,
|
|
1584
|
+
"requires_curated_donor": False,
|
|
1585
|
+
"requires_manual_acceptance": False,
|
|
1586
|
+
"donor_limited_generate": False,
|
|
1587
|
+
"recommended_donors": [selected_sample.relative_path],
|
|
1588
|
+
"known_limitations": [],
|
|
1589
|
+
}
|
|
1590
|
+
)
|
|
577
1591
|
else:
|
|
578
1592
|
unsupported_capabilities.append(capability)
|
|
579
1593
|
capability_statuses.append(
|
|
@@ -587,8 +1601,10 @@ def build_coverage_gap_report(
|
|
|
587
1601
|
"config_mutation_supported": False,
|
|
588
1602
|
"safe_open_generate_supported": False,
|
|
589
1603
|
"acceptance_verified": False,
|
|
1604
|
+
"donor_backed_ready": False,
|
|
590
1605
|
"requires_curated_donor": False,
|
|
591
1606
|
"requires_manual_acceptance": True,
|
|
1607
|
+
"donor_limited_generate": False,
|
|
592
1608
|
"recommended_donors": [],
|
|
593
1609
|
"known_limitations": ["no matching donor coverage found"],
|
|
594
1610
|
}
|
|
@@ -603,6 +1619,7 @@ def build_coverage_gap_report(
|
|
|
603
1619
|
"capability": str(status.get("capability")),
|
|
604
1620
|
"inventory_supported": bool(status.get("inventory_supported")),
|
|
605
1621
|
"edit_supported": bool(status.get("edit_supported")),
|
|
1622
|
+
"donor_backed_ready": _donor_backed_ready(status),
|
|
606
1623
|
"generate_supported": bool(status.get("safe_open_generate_supported")),
|
|
607
1624
|
"acceptance_verified": bool(status.get("acceptance_verified")),
|
|
608
1625
|
"best_maturity_level": _best_maturity_level(status),
|
|
@@ -694,6 +1711,18 @@ def build_inventory_capability_report(payload: dict[str, Any]) -> dict[str, obje
|
|
|
694
1711
|
capabilities.add("acl")
|
|
695
1712
|
if payload.get("topology_summary", {}).get("network_style") == "wan_security":
|
|
696
1713
|
capabilities.update({"vpn", "ipsec", "gre", "ppp"})
|
|
1714
|
+
for routing_details in payload.get("routing", {}).values():
|
|
1715
|
+
capabilities.update(str(capability) for capability in routing_details.get("capabilities", []) if str(capability).strip())
|
|
1716
|
+
for l2_details in payload.get("l2_security_monitoring", {}).values():
|
|
1717
|
+
capabilities.update(str(capability) for capability in l2_details.get("capabilities", []) if str(capability).strip())
|
|
1718
|
+
for l2_resiliency_details in payload.get("l2_resiliency_routing", {}).values():
|
|
1719
|
+
capabilities.update(str(capability) for capability in l2_resiliency_details.get("capabilities", []) if str(capability).strip())
|
|
1720
|
+
for ipv4_details in payload.get("ipv4_routing_management", {}).values():
|
|
1721
|
+
capabilities.update(str(capability) for capability in ipv4_details.get("capabilities", []) if str(capability).strip())
|
|
1722
|
+
for voice_details in payload.get("voice", {}).values():
|
|
1723
|
+
capabilities.update(str(capability) for capability in voice_details.get("capabilities", []) if str(capability).strip())
|
|
1724
|
+
for programming_details in payload.get("programming", {}).values():
|
|
1725
|
+
capabilities.update(str(capability) for capability in programming_details.get("feature_tags", []) if str(capability).strip())
|
|
697
1726
|
return {
|
|
698
1727
|
"device_families": families,
|
|
699
1728
|
"capabilities": sorted(capabilities),
|