packet-tracer-skill 0.2.2 → 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.
@@ -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",
@@ -77,6 +81,26 @@ REPORT_ONLY_CAPABILITIES = {
77
81
  "span",
78
82
  "qos",
79
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",
80
104
  "asa_acl_nat",
81
105
  "asa_service_policy",
82
106
  "clientless_vpn",
@@ -127,21 +151,108 @@ IPV6_DONOR_BACKED_CAPABILITIES = {
127
151
  "ripng",
128
152
  "hsrp",
129
153
  }
154
+ IPV6_DONOR_BACKED_READY_EDIT_CAPABILITIES = {
155
+ "ospfv3",
156
+ "eigrp_ipv6",
157
+ "ripng",
158
+ "hsrp",
159
+ }
130
160
  L2_SECURITY_MONITORING_EDIT_PROVEN_CAPABILITIES = {
131
161
  "dhcp_snooping",
132
162
  "dai",
163
+ "dot1x",
133
164
  "lldp",
134
165
  "rep",
135
166
  "snmp",
136
167
  "netflow",
137
168
  "span",
169
+ "qos",
138
170
  "port_security",
139
171
  }
140
172
  WIRELESS_ADVANCED_EDIT_PROVEN_CAPABILITIES = {
141
173
  "wep",
142
174
  "wpa_enterprise",
143
175
  }
144
- EDIT_PROVEN_REPORT_CAPABILITIES = IPV6_DONOR_BACKED_CAPABILITIES | L2_SECURITY_MONITORING_EDIT_PROVEN_CAPABILITIES
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
145
256
 
146
257
  CAPABILITY_PROVIDER_FAMILIES = {
147
258
  "access_port": {"switches", "multilayer switches"},
@@ -161,6 +272,25 @@ CAPABILITY_PROVIDER_FAMILIES = {
161
272
  "ipsec": {"routers", "security devices", "wan/cloud/dsl/cable devices"},
162
273
  "gre": {"routers", "wan/cloud/dsl/cable devices"},
163
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"},
164
294
  "multilayer_switching": {"multilayer switches"},
165
295
  "wan": {"wan/cloud/dsl/cable devices", "routers"},
166
296
  "security_edge": {"security devices", "routers"},
@@ -278,6 +408,33 @@ class CoverageGapReport:
278
408
  scenario_generate_readiness: dict[str, object] = field(default_factory=dict)
279
409
  recommended_next_actions: list[str] = field(default_factory=list)
280
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
+
281
438
 
282
439
  @dataclass
283
440
  class BlueprintPlan:
@@ -457,10 +614,12 @@ def _scenario_family_for_plan(plan: IntentPlan, requested_families: list[str]) -
457
614
  )
458
615
  wan_signal = (
459
616
  plan.network_style == "wan_security"
460
- or capabilities & {"vpn", "ipsec", "gre", "ppp", "multilayer_switching", "security_edge"}
617
+ or capabilities & {"vpn", "ipsec", "gre", "ppp", "multilayer_switching", "security_edge", "cbac", "zfw"}
461
618
  or {"wan/cloud/dsl/cable devices", "security devices"} & requested_family_set
462
619
  )
463
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"}
464
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"}
465
624
  wireless_advanced_signal = plan.network_style == "wireless_advanced" or capabilities & {"wlc", "wpa_enterprise", "wep", "guest_wifi", "beamforming", "meraki", "cellular_5g", "bluetooth"}
466
625
  automation_controller_signal = plan.network_style == "automation_controller" or capabilities & {"network_controller", "python_programming", "javascript_programming", "blockly_programming", "tcp_udp_app", "vm_iox"}
@@ -479,18 +638,22 @@ def _scenario_family_for_plan(plan: IntentPlan, requested_families: list[str]) -
479
638
  return "automation_controller"
480
639
  if voice_collaboration_signal:
481
640
  return "voice_collaboration"
641
+ if physical_media_device_signal:
642
+ return "physical_media_device"
482
643
  if wireless_advanced_signal:
483
644
  return "wireless_advanced"
645
+ if wan_signal:
646
+ return "wan_security_edge"
647
+ if l2_resiliency_routing_signal:
648
+ return "l2_resiliency_routing"
649
+ if ipv4_routing_management_signal:
650
+ return "ipv4_routing_management"
484
651
  if l2_security_monitoring_signal:
485
652
  return "l2_security_monitoring"
486
653
  if ipv6_routing_signal:
487
654
  return "ipv6_routing"
488
- if physical_media_device_signal:
489
- return "physical_media_device"
490
655
  if home_iot_signal:
491
656
  return "home_iot"
492
- if wan_signal:
493
- return "wan_security_edge"
494
657
  if campus_signal:
495
658
  return "campus"
496
659
  if service_signal:
@@ -502,8 +665,10 @@ SCENARIO_CAPABILITY_SETS = {
502
665
  "campus": {"router_on_a_stick", "trunk", "access_port", "management_vlan", "telnet", "verification", "acl", "nat"},
503
666
  "service_heavy": {"server_dns", "server_dhcp", "server_http", "server_https", "server_ftp", "server_tftp", "server_email", "server_syslog", "server_aaa", "ntp"},
504
667
  "home_iot": {"iot", "iot_registration", "iot_control", "wireless_ap", "wireless_mutation"},
505
- "wan_security_edge": {"vpn", "ipsec", "gre", "ppp", "acl", "nat"},
668
+ "wan_security_edge": {"vpn", "ipsec", "gre", "ppp", "acl", "nat", "cbac", "zfw"},
506
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"},
507
672
  "l2_security_monitoring": {"dhcp_snooping", "dai", "dot1x", "lldp", "rep", "snmp", "netflow", "span", "qos", "port_security"},
508
673
  "wireless_advanced": {"wlc", "wpa_enterprise", "wep", "guest_wifi", "beamforming", "meraki", "cellular_5g", "bluetooth"},
509
674
  "automation_controller": {"network_controller", "python_programming", "javascript_programming", "blockly_programming", "tcp_udp_app", "vm_iox"},
@@ -535,6 +700,8 @@ CAPABILITY_REQUIRED_ARCHETYPES = {
535
700
  "ppp": ["WAN/security edge"],
536
701
  "multilayer_switching": ["WAN/security edge", "campus/core"],
537
702
  "security_edge": ["WAN/security edge"],
703
+ "cbac": ["WAN/security edge"],
704
+ "zfw": ["WAN/security edge"],
538
705
  "ipv6_slaac": ["IPv6/routing"],
539
706
  "dhcpv6_stateful": ["IPv6/routing"],
540
707
  "dhcpv6_stateless": ["IPv6/routing"],
@@ -556,6 +723,26 @@ CAPABILITY_REQUIRED_ARCHETYPES = {
556
723
  "span": ["L2 security/monitoring"],
557
724
  "qos": ["L2 security/monitoring"],
558
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"],
559
746
  "wlc": ["advanced wireless"],
560
747
  "wpa_enterprise": ["advanced wireless"],
561
748
  "wep": ["advanced wireless"],
@@ -604,6 +791,8 @@ CAPABILITY_REQUIRED_RUNTIME_FEATURES = {
604
791
  "gre": ["workspace_validated", "tunnel_runtime"],
605
792
  "ppp": ["workspace_validated", "wan_runtime"],
606
793
  "security_edge": ["workspace_validated", "security_runtime"],
794
+ "cbac": ["workspace_validated", "security_runtime"],
795
+ "zfw": ["workspace_validated", "security_runtime"],
607
796
  "multilayer_switching": ["workspace_validated", "multilayer_runtime"],
608
797
  "ipv6_slaac": ["workspace_validated", "ipv6_runtime"],
609
798
  "dhcpv6_stateful": ["workspace_validated", "ipv6_runtime"],
@@ -613,6 +802,18 @@ CAPABILITY_REQUIRED_RUNTIME_FEATURES = {
613
802
  "hsrp": ["workspace_validated", "ipv6_runtime"],
614
803
  "wep": ["workspace_validated", "wireless_runtime"],
615
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"],
616
817
  }
617
818
 
618
819
 
@@ -621,6 +822,8 @@ def _generate_mismatch_reason(status: dict[str, object]) -> str | None:
621
822
  return "report_only"
622
823
  if not bool(status.get("inventory_supported")):
623
824
  return "unsupported"
825
+ if bool(status.get("donor_limited_generate")):
826
+ return "supported_but_donor_limited"
624
827
  if bool(status.get("edit_supported")) and not bool(status.get("safe_open_generate_supported")):
625
828
  return "supported_in_edit_only"
626
829
  if bool(status.get("safe_open_generate_supported")) and bool(status.get("requires_curated_donor")):
@@ -632,6 +835,10 @@ def _generate_mismatch_reason(status: dict[str, object]) -> str | None:
632
835
  return "unsupported"
633
836
 
634
837
 
838
+ def _donor_backed_ready(status: dict[str, object]) -> bool:
839
+ return bool(status.get("donor_backed_ready"))
840
+
841
+
635
842
  def _selected_sample_for_donor(
636
843
  samples: list[SampleDescriptor],
637
844
  selected_donor: str | None,
@@ -691,6 +898,159 @@ def _has_explicit_advanced_wireless_edit(plan: IntentPlan, capability: str) -> b
691
898
  return False
692
899
 
693
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
+
694
1054
  def _has_explicit_wan_security_intent(plan: IntentPlan) -> bool:
695
1055
  capabilities = set(plan.capabilities)
696
1056
  prompt_lower = str(plan.prompt or "").lower()
@@ -701,6 +1061,17 @@ def _has_explicit_wan_security_intent(plan: IntentPlan) -> bool:
701
1061
  )
702
1062
 
703
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
+
704
1075
  def _has_explicit_ipv6_operation(plan: IntentPlan, capability: str) -> bool:
705
1076
  operation_map = {
706
1077
  "ipv6_slaac": {"enable_ipv6_unicast_routing", "set_ipv6_address", "set_ipv6_slaac"},
@@ -764,10 +1135,15 @@ def _selected_donor_capability_override(
764
1135
  "multilayer_switching": {"multilayer_runtime"},
765
1136
  }
766
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
+ )
767
1142
  return (
768
1143
  "WAN/security edge" in archetypes
769
1144
  and capability in sample_capabilities
770
1145
  and bool(runtime_features & wan_runtime_by_capability[capability])
1146
+ and has_acceptance_backed_wan_generate
771
1147
  and _has_explicit_wan_security_intent(plan)
772
1148
  )
773
1149
  if capability in IPV6_DONOR_BACKED_CAPABILITIES:
@@ -800,7 +1176,9 @@ def _best_maturity_level(status: dict[str, object]) -> str:
800
1176
  def _recommended_next_action_for_capability(capability: str, mismatch_reason: str | None) -> str:
801
1177
  if mismatch_reason == "report_only":
802
1178
  return f"Keep {capability} in atlas/report mode until donor-backed edit or generate evidence exists."
803
- if capability in {"vpn", "ipsec", "gre", "ppp", "security_edge", "multilayer_switching"}:
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."
804
1182
  if mismatch_reason == "supported_but_donor_limited":
805
1183
  return f"Provide a WAN/security edge donor with reusable ASA/cloud/serial or tunnel skeleton for {capability}."
806
1184
  if mismatch_reason == "supported_but_acceptance_gated":
@@ -826,6 +1204,18 @@ def _recommended_next_action_for_capability(capability: str, mismatch_reason: st
826
1204
  if mismatch_reason == "supported_but_donor_limited":
827
1205
  return f"Provide an advanced wireless donor with reusable AP/router/WLC skeleton for {capability}."
828
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."
829
1219
  if capability == "iot_registration":
830
1220
  if mismatch_reason == "supported_but_acceptance_gated":
831
1221
  return "Use an IoT/home gateway donor and explicitly name the thing plus gateway/server target before strict generate."
@@ -1008,7 +1398,72 @@ def build_coverage_gap_report(
1008
1398
  and _has_explicit_advanced_wireless_edit(plan, capability)
1009
1399
  and not selected_donor_backed
1010
1400
  )
1011
- edit_proven_only = (capability in EDIT_PROVEN_REPORT_CAPABILITIES or wireless_edit_proven_only) and not selected_donor_backed
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
+ )
1012
1467
  report_only = capability in REPORT_ONLY_CAPABILITIES and not selected_donor_backed and not edit_proven_only
1013
1468
  matching_entries = [
1014
1469
  entry
@@ -1022,9 +1477,13 @@ def build_coverage_gap_report(
1022
1477
  (any(entry.maturity_level == "acceptance-verified" for entry in matching_entries) and capability not in iot_acceptance_gated)
1023
1478
  or selected_donor_backed
1024
1479
  )
1025
- if report_only or edit_proven_only:
1480
+ if report_only or edit_proven_only or wan_security_donor_limited:
1026
1481
  acceptance_verified = False
1027
- requires_curated = best_entry.maturity_level == "safe-open-generate-supported" and not selected_donor_backed and not edit_proven_only
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
1028
1487
  if requires_curated:
1029
1488
  requires_curated_donor.append(capability)
1030
1489
  if not acceptance_verified:
@@ -1038,17 +1497,27 @@ def build_coverage_gap_report(
1038
1497
  "inventory_supported": any(MATURITY_RANK[entry.maturity_level] >= MATURITY_RANK["inventory-supported"] for entry in matching_entries),
1039
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),
1040
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),
1041
- "safe_open_generate_supported": False if report_only or edit_proven_only else any(MATURITY_RANK[entry.maturity_level] >= MATURITY_RANK["safe-open-generate-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),
1042
1501
  "acceptance_verified": acceptance_verified,
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
+ ),
1043
1510
  "requires_curated_donor": requires_curated,
1044
1511
  "requires_manual_acceptance": not acceptance_verified,
1045
1512
  "report_only": report_only,
1513
+ "donor_limited_generate": wan_security_donor_limited,
1046
1514
  "recommended_donors": list(dict.fromkeys(donor for entry in matching_entries for donor in entry.accepted_donors))[:10],
1047
1515
  "known_limitations": list(dict.fromkeys(item for entry in matching_entries for item in entry.known_limitations)),
1048
1516
  }
1049
1517
  )
1050
1518
  elif edit_proven_only:
1051
1519
  supported_capabilities.append(capability)
1520
+ requires_manual_acceptance.append(capability)
1052
1521
  capability_statuses.append(
1053
1522
  {
1054
1523
  "capability": capability,
@@ -1060,15 +1529,24 @@ def build_coverage_gap_report(
1060
1529
  "config_mutation_supported": True,
1061
1530
  "safe_open_generate_supported": False,
1062
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
+ ),
1063
1539
  "requires_curated_donor": False,
1064
1540
  "requires_manual_acceptance": True,
1065
1541
  "report_only": False,
1542
+ "donor_limited_generate": False,
1066
1543
  "recommended_donors": [],
1067
1544
  "known_limitations": ["editor-proven without sample-backed donor selection"],
1068
1545
  }
1069
1546
  )
1070
1547
  elif report_only:
1071
1548
  supported_capabilities.append(capability)
1549
+ requires_manual_acceptance.append(capability)
1072
1550
  capability_statuses.append(
1073
1551
  {
1074
1552
  "capability": capability,
@@ -1080,9 +1558,11 @@ def build_coverage_gap_report(
1080
1558
  "config_mutation_supported": False,
1081
1559
  "safe_open_generate_supported": False,
1082
1560
  "acceptance_verified": False,
1561
+ "donor_backed_ready": False,
1083
1562
  "requires_curated_donor": False,
1084
1563
  "requires_manual_acceptance": True,
1085
1564
  "report_only": True,
1565
+ "donor_limited_generate": False,
1086
1566
  "recommended_donors": [],
1087
1567
  "known_limitations": ["report-supported without decode-backed donor selection"],
1088
1568
  }
@@ -1100,8 +1580,10 @@ def build_coverage_gap_report(
1100
1580
  "config_mutation_supported": True,
1101
1581
  "safe_open_generate_supported": True,
1102
1582
  "acceptance_verified": True,
1583
+ "donor_backed_ready": True,
1103
1584
  "requires_curated_donor": False,
1104
1585
  "requires_manual_acceptance": False,
1586
+ "donor_limited_generate": False,
1105
1587
  "recommended_donors": [selected_sample.relative_path],
1106
1588
  "known_limitations": [],
1107
1589
  }
@@ -1119,8 +1601,10 @@ def build_coverage_gap_report(
1119
1601
  "config_mutation_supported": False,
1120
1602
  "safe_open_generate_supported": False,
1121
1603
  "acceptance_verified": False,
1604
+ "donor_backed_ready": False,
1122
1605
  "requires_curated_donor": False,
1123
1606
  "requires_manual_acceptance": True,
1607
+ "donor_limited_generate": False,
1124
1608
  "recommended_donors": [],
1125
1609
  "known_limitations": ["no matching donor coverage found"],
1126
1610
  }
@@ -1135,6 +1619,7 @@ def build_coverage_gap_report(
1135
1619
  "capability": str(status.get("capability")),
1136
1620
  "inventory_supported": bool(status.get("inventory_supported")),
1137
1621
  "edit_supported": bool(status.get("edit_supported")),
1622
+ "donor_backed_ready": _donor_backed_ready(status),
1138
1623
  "generate_supported": bool(status.get("safe_open_generate_supported")),
1139
1624
  "acceptance_verified": bool(status.get("acceptance_verified")),
1140
1625
  "best_maturity_level": _best_maturity_level(status),
@@ -1230,6 +1715,14 @@ def build_inventory_capability_report(payload: dict[str, Any]) -> dict[str, obje
1230
1715
  capabilities.update(str(capability) for capability in routing_details.get("capabilities", []) if str(capability).strip())
1231
1716
  for l2_details in payload.get("l2_security_monitoring", {}).values():
1232
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())
1233
1726
  return {
1234
1727
  "device_families": families,
1235
1728
  "capabilities": sorted(capabilities),