packet-tracer-skill 0.2.0 → 0.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/CHANGELOG.md +42 -4
  2. package/README.md +237 -88
  3. package/docs/campus-donor-proof.md +103 -0
  4. package/docs/curated-donor-registry.md +26 -0
  5. package/docs/github-launch-ops-0.2.1.md +45 -0
  6. package/docs/github-launch-ops-0.2.2.md +42 -0
  7. package/docs/github-metadata.md +23 -6
  8. package/docs/hero-demo-plan.md +18 -10
  9. package/docs/home-iot-donor-proof.md +64 -0
  10. package/docs/launch-announcement-0.2.1.md +15 -0
  11. package/docs/launch-announcement-0.2.2.md +15 -0
  12. package/docs/packet-tracer-feature-gap-atlas.md +87 -0
  13. package/docs/post-launch-follow-up.md +39 -0
  14. package/docs/publish-preview-roadmap.md +20 -17
  15. package/docs/release-checklist.md +22 -12
  16. package/docs/{release-notes-0.2.0.md → release-notes-0.2.1.md} +7 -6
  17. package/docs/release-notes-0.2.2.md +26 -0
  18. package/docs/runtime-truth.md +31 -0
  19. package/docs/wan-security-donor-proof.md +41 -0
  20. package/docs/wireless-advanced-proof.md +60 -0
  21. package/examples/README.md +13 -10
  22. package/examples/gallery.md +10 -4
  23. package/examples/index.json +3 -3
  24. package/package.json +44 -16
  25. package/references/curated-donor-registry.json +5 -2
  26. package/references/packettracer-feature-atlas.json +133 -0
  27. package/references/scenario-fixture-corpus.json +1 -0
  28. package/scripts/build_examples_index.py +27 -16
  29. package/scripts/coverage_matrix.py +559 -23
  30. package/scripts/feature_atlas.py +229 -0
  31. package/scripts/generate_pkt.py +330 -31
  32. package/scripts/intent_parser.py +278 -7
  33. package/scripts/pkt_editor.py +174 -0
  34. package/scripts/runtime_doctor.py +83 -10
  35. package/scripts/sample_catalog.py +210 -5
  36. package/docs/screenshots/.gitkeep +0 -1
  37. package/docs/screenshots/packet-tracer-topology-cropped.png +0 -0
  38. package/scripts/__pycache__/build_examples_index.cpython-314.pyc +0 -0
  39. package/scripts/__pycache__/build_sample_catalog.cpython-314.pyc +0 -0
  40. package/scripts/__pycache__/coverage_matrix.cpython-314.pyc +0 -0
  41. package/scripts/__pycache__/donor_diagnostics.cpython-314.pyc +0 -0
  42. package/scripts/__pycache__/generate_pkt.cpython-314.pyc +0 -0
  43. package/scripts/__pycache__/install_skill.cpython-314.pyc +0 -0
  44. package/scripts/__pycache__/intent_parser.cpython-314.pyc +0 -0
  45. package/scripts/__pycache__/packet_tracer_env.cpython-314.pyc +0 -0
  46. package/scripts/__pycache__/pkt_builder.cpython-314.pyc +0 -0
  47. package/scripts/__pycache__/pkt_codec.cpython-314.pyc +0 -0
  48. package/scripts/__pycache__/pkt_editor.cpython-314.pyc +0 -0
  49. package/scripts/__pycache__/pkt_transformer.cpython-314.pyc +0 -0
  50. package/scripts/__pycache__/remote_search.cpython-314.pyc +0 -0
  51. package/scripts/__pycache__/runtime_doctor.cpython-314.pyc +0 -0
  52. package/scripts/__pycache__/sample_catalog.cpython-314.pyc +0 -0
  53. package/scripts/__pycache__/sample_selector.cpython-314.pyc +0 -0
  54. package/scripts/__pycache__/twofish_diagnostics.cpython-314.pyc +0 -0
  55. package/scripts/__pycache__/twofish_runtime.cpython-314.pyc +0 -0
  56. package/scripts/__pycache__/workspace_repair.cpython-314.pyc +0 -0
  57. package/scripts/vendor/__pycache__/twofish.cpython-314.pyc +0 -0
@@ -28,7 +28,7 @@ CAPABILITY_PATTERNS = {
28
28
  "management_vlan": [r"\bmanagement vlan\b", r"\bvlan 99\b", r"\bdefault-gateway\b"],
29
29
  "telnet": [r"\btelnet\b", r"\bvty\b", r"\btransport input telnet\b"],
30
30
  "wireless_ap": [r"\bssid\b", r"\bwifi\b", r"\bwpa\b", r"\bwpa2\b", r"\bwireless\b", r"\baccess point\b", r"\bap\b"],
31
- "wireless_mutation": [r"\bset\s+[A-Za-z0-9_ -]+?\s+ssid\b", r"\bsecurity\s+(?:wpa|wpa2|wep|open|802\.1x)\b", r"\bpassphrase\b", r"\bchannel\s+\d+\b"],
31
+ "wireless_mutation": [r"\bset\s+[A-Za-z0-9_ -]+?\s+ssid\b", r"\bsecurity\s+(?:wpa|wpa2|wpa-enterprise|wpa2-enterprise|wep|open|802\.1x)\b", r"\bpassphrase\b", r"\bchannel\s+\d+\b"],
32
32
  "wireless_client": [r"\btablet\b", r"\blaptop\b", r"\bsmartphone\b", r"\bassociate\b", r"\bjoin\b"],
33
33
  "wireless_client_association": [r"\bassociate\b", r"\bjoin\b"],
34
34
  "tablet": [r"\btablet\b"],
@@ -38,13 +38,75 @@ CAPABILITY_PATTERNS = {
38
38
  "ospf": [r"\bospf\b"],
39
39
  "eigrp": [r"\beigrp\b"],
40
40
  "rip": [r"\brip\b"],
41
+ "ipv6_slaac": [r"\bslaac\b", r"\bipv6\s+slaac\b"],
42
+ "dhcpv6_stateful": [r"\bdhcpv6\b", r"\bstateful dhcpv6\b", r"\bipv6 stateful\b"],
43
+ "dhcpv6_stateless": [r"\bstateless dhcpv6\b", r"\bipv6 stateless\b"],
44
+ "ipv6_prefix_delegation": [r"\bprefix delegation\b", r"\bipv6 prefix\b"],
45
+ "ipv6_dns_aaaa": [r"\baaaa\b", r"\bipv6 dns\b"],
46
+ "ipv6_tunneling": [r"\bipv6ip\b", r"\bipv6 tunnel\b", r"\bipv6 tunneling\b"],
47
+ "isatap": [r"\bisatap\b"],
48
+ "ospfv3": [r"\bospfv3\b", r"\bipv6 ospf\b"],
49
+ "eigrp_ipv6": [r"\bipv6 eigrp\b", r"\beigrp ipv6\b"],
50
+ "ripng": [r"\bripng\b", r"\bipv6 rip\b"],
51
+ "hsrp": [r"\bhsrp\b"],
41
52
  "nat": [r"\bnat\b"],
42
53
  "acl": [r"\bacl\b", r"\baccess-list\b"],
54
+ "dhcp_snooping": [r"\bdhcp snooping\b"],
55
+ "dai": [r"\bdai\b", r"\bdynamic arp inspection\b"],
56
+ "dot1x": [r"\bdot1x\b", r"\b802\.1x\b", r"\bnac\b"],
57
+ "lldp": [r"\blldp\b"],
58
+ "rep": [r"\brep\b"],
59
+ "snmp": [r"\bsnmp\b"],
60
+ "netflow": [r"\bnetflow\b"],
61
+ "span": [r"\bspan\b", r"\brspan\b"],
62
+ "qos": [r"\bqos policy\b", r"\bqos class\b", r"\bclass-map\b", r"\bpolicy-map\b", r"\bquality of service\b"],
63
+ "port_security": [r"\bport security\b", r"\bport-security\b"],
43
64
  "vpn": [r"\bvpn\b", r"\bsite-to-site\b", r"\bipsec\b", r"\btunnel\b"],
44
65
  "ipsec": [r"\bipsec\b", r"\bike\b", r"\bphase 1\b", r"\bphase 2\b"],
45
66
  "gre": [r"\bgre\b", r"\bgre tunnel\b"],
46
67
  "ppp": [r"\bppp\b", r"\bchap\b", r"\bpap\b"],
68
+ "security_edge": [r"\bsecurity edge\b", r"\bsecurity appliance\b", r"\bfirewall\b", r"\basa\b"],
69
+ "asa_acl_nat": [r"\basa acl\b", r"\basa nat\b", r"\basa acl nat\b"],
70
+ "asa_service_policy": [r"\basa service policy\b", r"\bservice policy\b"],
71
+ "clientless_vpn": [r"\bclientless vpn\b"],
72
+ "cbac": [r"\bcbac\b"],
73
+ "zfw": [r"\bzfw\b", r"\bzone based firewall\b", r"\bzone-based firewall\b"],
74
+ "sniffer": [r"\bsniffer\b"],
47
75
  "multilayer_switching": [r"\bmultilayer switch\b", r"\blayer 3 switch\b", r"\b3560\b", r"\bsvi\b"],
76
+ "wlc": [r"\bwlc\b", r"\bwireless lan controller\b"],
77
+ "wpa_enterprise": [r"\bwpa enterprise\b", r"\bwpa2 enterprise\b", r"\bwpa-enterprise\b", r"\bwpa2-enterprise\b", r"\b802\.1x wireless\b"],
78
+ "wep": [r"\bwep\b"],
79
+ "guest_wifi": [r"\bguest wifi\b", r"\bguest wlan\b"],
80
+ "beamforming": [r"\bbeamforming\b"],
81
+ "meraki": [r"\bmeraki\b"],
82
+ "cellular_5g": [r"\b5g\b", r"\bcellular\b", r"\bcell tower\b"],
83
+ "bluetooth": [r"\bbluetooth\b"],
84
+ "network_controller": [r"\bnetwork controller\b", r"\bnetcon\b"],
85
+ "python_programming": [r"\bpython programming\b", r"\bpython app\b"],
86
+ "javascript_programming": [r"\bjavascript\b", r"\bjs app\b"],
87
+ "blockly_programming": [r"\bblockly\b"],
88
+ "tcp_udp_app": [r"\btcp test app\b", r"\budp test app\b", r"\btcp app\b", r"\budp app\b"],
89
+ "vm_iox": [r"\biox\b", r"\bvm\b", r"\bvirtual machine\b"],
90
+ "voip": [r"\bvoip\b"],
91
+ "ip_phone": [r"\bip phone\b", r"\bphone\b"],
92
+ "call_manager": [r"\bcall manager\b", r"\btelephony\b"],
93
+ "linksys_voice": [r"\blinksys voice\b"],
94
+ "mqtt": [r"\bmqtt\b"],
95
+ "real_http": [r"\breal http\b"],
96
+ "real_websocket": [r"\bwebsocket\b", r"\breal websocket\b"],
97
+ "visual_scripting": [r"\bvisual scripting\b"],
98
+ "ptp": [r"\bptp\b", r"\bprecision time protocol\b"],
99
+ "profinet": [r"\bprofinet\b"],
100
+ "l2nat": [r"\bl2nat\b", r"\bl2 nat\b"],
101
+ "cyberobserver": [r"\bcyberobserver\b"],
102
+ "industrial_firewall": [r"\bindustrial firewall\b", r"\bisa3000\b"],
103
+ "coaxial": [r"\bcoaxial\b"],
104
+ "cable_dsl": [r"\bcable modem\b", r"\bdsl\b", r"\bdsl modem\b"],
105
+ "central_office": [r"\bcentral office\b"],
106
+ "cell_tower": [r"\bcell tower\b"],
107
+ "power_distribution": [r"\bpower distribution\b"],
108
+ "hot_swappable": [r"\bhot swappable\b", r"\bhot-swappable\b"],
109
+ "ios_license": [r"\bios license\b", r"\blicense\b", r"\bios15\b", r"\bios 15\b"],
48
110
  }
49
111
 
50
112
  DEVICE_SYNONYMS = {
@@ -97,6 +159,13 @@ NATURAL_DEVICE_ALIASES = {
97
159
  NETWORK_STYLE_PATTERNS = {
98
160
  "campus": [r"\bcampus\b", r"\bkampus\b", r"\bsobeli\b", r"\bdepart", r"\bdepartment\b"],
99
161
  "branch": [r"\bbranch\b", r"\bfilial\b"],
162
+ "ipv6_routing": [r"\bipv6\b", r"\bslaac\b", r"\bdhcpv6\b", r"\bospfv3\b", r"\bhsrp\b"],
163
+ "l2_security_monitoring": [r"\bdhcp snooping\b", r"\bdai\b", r"\bdot1x\b", r"\b802\.1x\b", r"\bsnmp\b", r"\bnetflow\b", r"\bspan\b", r"\brspan\b", r"\bquality of service\b", r"\blldp\b", r"\brep\b", r"\bport security\b", r"\bport-security\b"],
164
+ "wireless_advanced": [r"\bwlc\b", r"\bwireless lan controller\b", r"\bmeraki\b", r"\bbluetooth\b", r"\b5g\b", r"\bcellular\b", r"\bwpa enterprise\b", r"\bwpa2 enterprise\b", r"\bwep\b", r"\bguest wifi\b", r"\bguest wlan\b", r"\bbeamforming\b"],
165
+ "automation_controller": [r"\bnetwork controller\b", r"\bnetcon\b", r"\bblockly\b", r"\bjavascript\b", r"\bpython programming\b", r"\biox\b"],
166
+ "voice_collaboration": [r"\bvoip\b", r"\bip phone\b", r"\bcall manager\b", r"\btelephony\b"],
167
+ "industrial_iot": [r"\bmqtt\b", r"\bwebsocket\b", r"\bprofinet\b", r"\bl2nat\b", r"\bptp\b", r"\bcyberobserver\b", r"\bindustrial\b"],
168
+ "physical_media_device": [r"\bcoaxial\b", r"\bcable modem\b", r"\bdsl modem\b", r"\bcentral office\b", r"\bcell tower\b", r"\bpower distribution\b"],
100
169
  "small_office": [r"\bsmall office\b", r"\bhome\b", r"\bofis\b"],
101
170
  "wireless_branch": [r"\bwireless\b", r"\bwifi\b", r"\bssid\b"],
102
171
  "wan_security": [r"\bwan\b", r"\bgre\b", r"\bppp\b", r"\bipsec\b", r"\bvpn\b", r"\bsecurity edge\b", r"\bfirewall\b", r"\basa\b"],
@@ -136,6 +205,8 @@ SECURITY_TO_AUTH = {
136
205
  "wpa": ("2", "2"),
137
206
  "wpa2": ("4", "4"),
138
207
  "802.1x": ("5", "5"),
208
+ "wpa-enterprise": ("5", "5"),
209
+ "wpa2-enterprise": ("5", "5"),
139
210
  }
140
211
 
141
212
  TRANSLITERATION_TABLE = str.maketrans(
@@ -389,7 +460,7 @@ def _extract_service_requirements(capabilities: list[str], prompt: str) -> dict[
389
460
  for service in ["dhcp", "dns", "http", "https", "ftp", "tftp", "ntp", "email", "syslog", "aaa"]:
390
461
  if service in lowered:
391
462
  requirements["services"].append(service)
392
- for security in ["acl", "telnet", "wpa2", "wpa", "wep", "nat", "vpn", "ipsec", "gre", "ppp", "ssh"]:
463
+ for security in ["acl", "telnet", "wpa2", "wpa", "wep", "nat", "vpn", "ipsec", "gre", "ppp", "security_edge", "ssh"]:
393
464
  if security in lowered:
394
465
  requirements["security"].append(security)
395
466
  return requirements
@@ -503,6 +574,39 @@ def _extract_switch_ops(prompt: str) -> list[dict[str, object]]:
503
574
  "native": int(native) if native else None,
504
575
  }
505
576
  )
577
+ dhcp_snooping_pattern = re.compile(r"set\s+([A-Za-z0-9_-]+)\s+dhcp\s+snooping\s+vlan\s+(\d+)(?:\s+trust\s+([A-Za-z0-9/._-]+))?", flags=re.IGNORECASE)
578
+ dai_pattern = re.compile(r"set\s+([A-Za-z0-9_-]+)\s+dai\s+vlan\s+(\d+)(?:\s+trust\s+([A-Za-z0-9/._-]+))?", flags=re.IGNORECASE)
579
+ port_security_pattern = re.compile(
580
+ r"set\s+([A-Za-z0-9_-]+)\s+port-security\s+([A-Za-z0-9/._-]+)(?:\s+max\s+(\d+))?(?:\s+violation\s+(protect|restrict|shutdown))?",
581
+ flags=re.IGNORECASE,
582
+ )
583
+ lldp_pattern = re.compile(r"set\s+([A-Za-z0-9_-]+)\s+lldp(?:\s+(?:enable|run))?", flags=re.IGNORECASE)
584
+ rep_pattern = re.compile(r"set\s+([A-Za-z0-9_-]+)\s+rep\s+segment\s+(\d+)\s+interface\s+([A-Za-z0-9/._-]+)", flags=re.IGNORECASE)
585
+ span_pattern = re.compile(
586
+ r"set\s+([A-Za-z0-9_-]+)\s+(?:r?span)\s+(\d+)\s+source\s+([A-Za-z0-9/._-]+)\s+destination\s+([A-Za-z0-9/._-]+)",
587
+ flags=re.IGNORECASE,
588
+ )
589
+ for segment in _command_segments(prompt):
590
+ for device, vlan_id, trust_port in dhcp_snooping_pattern.findall(segment):
591
+ ops.append({"op": "set_dhcp_snooping", "device": device, "vlan": int(vlan_id), "trust_port": trust_port or None})
592
+ for device, vlan_id, trust_port in dai_pattern.findall(segment):
593
+ ops.append({"op": "set_dai", "device": device, "vlan": int(vlan_id), "trust_port": trust_port or None})
594
+ for device, port, maximum, violation in port_security_pattern.findall(segment):
595
+ ops.append(
596
+ {
597
+ "op": "set_port_security",
598
+ "device": device,
599
+ "port": port,
600
+ "maximum": int(maximum) if maximum else None,
601
+ "violation": violation.lower() if violation else None,
602
+ }
603
+ )
604
+ for device in lldp_pattern.findall(segment):
605
+ ops.append({"op": "set_lldp", "device": device})
606
+ for device, segment_id, interface_name in rep_pattern.findall(segment):
607
+ ops.append({"op": "set_rep", "device": device, "segment": int(segment_id), "interface": interface_name})
608
+ for device, session, source, destination in span_pattern.findall(segment):
609
+ ops.append({"op": "set_span", "device": device, "session": int(session), "source": source, "destination": destination})
506
610
  return ops
507
611
 
508
612
 
@@ -570,6 +674,91 @@ def _extract_router_ops(prompt: str) -> list[dict[str, object]]:
570
674
  for segment in _command_segments(prompt):
571
675
  for acl_name, direction, device, interface_name in acl_apply_pattern.findall(segment):
572
676
  ops.append({"op": "apply_acl", "device": device.strip(), "acl_name": acl_name, "direction": direction.lower(), "interface": interface_name})
677
+ ipv6_unicast_pattern = re.compile(r"set\s+([A-Za-z0-9_-]+)\s+ipv6\s+unicast-routing", flags=re.IGNORECASE)
678
+ ipv6_address_pattern = re.compile(
679
+ r"set\s+([A-Za-z0-9_-]+)\s+(?:interface\s+)?([A-Za-z0-9/._-]+)\s+ipv6\s+([0-9A-Fa-f:]+)/(\d+)",
680
+ flags=re.IGNORECASE,
681
+ )
682
+ ipv6_slaac_pattern = re.compile(
683
+ r"set\s+([A-Za-z0-9_-]+)\s+slaac\s+on\s+([A-Za-z0-9/._-]+)(?:\s+prefix\s+([0-9A-Fa-f:]+)/(\d+))?",
684
+ flags=re.IGNORECASE,
685
+ )
686
+ dhcpv6_pattern = re.compile(
687
+ r"set\s+([A-Za-z0-9_-]+)\s+dhcpv6\s+pool\s+([A-Za-z0-9_-]+)\s+prefix\s+([0-9A-Fa-f:]+)/(\d+)\s+interface\s+([A-Za-z0-9/._-]+)"
688
+ r"(?:\s+dns\s+([0-9A-Fa-f:]+))?(?:\s+domain\s+([A-Za-z0-9._-]+))?",
689
+ flags=re.IGNORECASE,
690
+ )
691
+ ospfv3_pattern = re.compile(r"set\s+([A-Za-z0-9_-]+)\s+ospfv3\s+(\d+)\s+area\s+(\d+)\s+interface\s+([A-Za-z0-9/._-]+)", flags=re.IGNORECASE)
692
+ eigrp_ipv6_pattern = re.compile(r"set\s+([A-Za-z0-9_-]+)\s+eigrp\s+ipv6\s+(\d+)\s+interface\s+([A-Za-z0-9/._-]+)", flags=re.IGNORECASE)
693
+ ripng_pattern = re.compile(r"set\s+([A-Za-z0-9_-]+)\s+ripng\s+([A-Za-z0-9_-]+)\s+interface\s+([A-Za-z0-9/._-]+)", flags=re.IGNORECASE)
694
+ hsrp_pattern = re.compile(
695
+ r"set\s+([A-Za-z0-9_-]+)\s+hsrp\s+(\d+)\s+ipv6\s+([0-9A-Fa-f:]+)\s+interface\s+([A-Za-z0-9/._-]+)(?:\s+priority\s+(\d+))?",
696
+ flags=re.IGNORECASE,
697
+ )
698
+ snmp_pattern = re.compile(r"set\s+([A-Za-z0-9_-]+)\s+snmp\s+community\s+([A-Za-z0-9_-]+)\s+(ro|rw)", flags=re.IGNORECASE)
699
+ netflow_pattern = re.compile(
700
+ r"set\s+([A-Za-z0-9_-]+)\s+netflow\s+destination\s+(\d+\.\d+\.\d+\.\d+)\s+(\d+)(?:\s+version\s+(\d+))?(?:\s+interface\s+([A-Za-z0-9/._-]+)\s+(ingress|egress))?",
701
+ flags=re.IGNORECASE,
702
+ )
703
+ for segment in _command_segments(prompt):
704
+ for device in ipv6_unicast_pattern.findall(segment):
705
+ ops.append({"op": "enable_ipv6_unicast_routing", "device": device})
706
+ for device, interface_name, address, prefix in ipv6_address_pattern.findall(segment):
707
+ ops.append({"op": "set_ipv6_address", "device": device, "interface": interface_name, "address": address, "prefix": int(prefix)})
708
+ for device, interface_name, prefix_address, prefix_len in ipv6_slaac_pattern.findall(segment):
709
+ ops.append(
710
+ {
711
+ "op": "set_ipv6_slaac",
712
+ "device": device,
713
+ "interface": interface_name,
714
+ "prefix": prefix_address or None,
715
+ "prefix_len": int(prefix_len) if prefix_len else None,
716
+ }
717
+ )
718
+ for device, name, prefix_address, prefix_len, interface_name, dns, domain in dhcpv6_pattern.findall(segment):
719
+ ops.append(
720
+ {
721
+ "op": "set_dhcpv6_pool",
722
+ "device": device,
723
+ "name": name,
724
+ "prefix": prefix_address,
725
+ "prefix_len": int(prefix_len),
726
+ "interface": interface_name,
727
+ "dns": dns or None,
728
+ "domain": domain or None,
729
+ }
730
+ )
731
+ for device, process_id, area, interface_name in ospfv3_pattern.findall(segment):
732
+ ops.append({"op": "set_ospfv3_interface", "device": device, "process_id": int(process_id), "area": int(area), "interface": interface_name})
733
+ for device, asn, interface_name in eigrp_ipv6_pattern.findall(segment):
734
+ ops.append({"op": "set_eigrp_ipv6_interface", "device": device, "asn": int(asn), "interface": interface_name})
735
+ for device, process_name, interface_name in ripng_pattern.findall(segment):
736
+ ops.append({"op": "set_ripng_interface", "device": device, "process_name": process_name, "interface": interface_name})
737
+ for device, group, virtual_ipv6, interface_name, priority in hsrp_pattern.findall(segment):
738
+ ops.append(
739
+ {
740
+ "op": "set_hsrp_ipv6",
741
+ "device": device,
742
+ "group": int(group),
743
+ "virtual_ipv6": virtual_ipv6,
744
+ "interface": interface_name,
745
+ "priority": int(priority) if priority else None,
746
+ }
747
+ )
748
+ for device, community, mode in snmp_pattern.findall(segment):
749
+ ops.append({"op": "set_snmp_community", "device": device, "community": community, "mode": mode.lower()})
750
+ for device, destination, port, version, interface_name, direction in netflow_pattern.findall(segment):
751
+ ops.append(
752
+ {
753
+ "op": "set_netflow",
754
+ "device": device,
755
+ "destination": destination,
756
+ "port": int(port),
757
+ "version": int(version) if version else 9,
758
+ "interface": interface_name or None,
759
+ "direction": direction.lower() if direction else None,
760
+ }
761
+ )
573
762
  return ops
574
763
 
575
764
 
@@ -630,14 +819,31 @@ def _extract_management_ops(prompt: str) -> list[dict[str, object]]:
630
819
  def _extract_wireless_ops(prompt: str) -> list[dict[str, object]]:
631
820
  ops: list[dict[str, object]] = []
632
821
  ssid_pattern = re.compile(
633
- r"set\s+([A-Za-z0-9_ -]+?)\s+ssid\s+([A-Za-z0-9._-]+)(?:\s+security\s+([A-Za-z0-9.-]+))?(?:\s+passphrase\s+([A-Za-z0-9._-]+))?(?:\s+channel\s+(\d+))?",
822
+ r"set\s+([A-Za-z0-9_ -]+?)\s+ssid\s+([A-Za-z0-9._-]+)"
823
+ r"(?:\s+security\s+(wpa2?\s+enterprise|wpa2?-enterprise|wpa2?-psk|802\.1x|wep|open|wpa2?|wpa))?"
824
+ r"(?:\s+passphrase\s+([A-Za-z0-9._-]+))?"
825
+ r"(?:\s+radius\s+(\d+\.\d+\.\d+\.\d+)\s+secret\s+([A-Za-z0-9._-]+))?"
826
+ r"(?:\s+channel\s+(\d+))?",
634
827
  flags=re.IGNORECASE,
635
828
  )
636
829
  for segment in _command_segments(prompt):
637
- for device, ssid, security, passphrase, channel in ssid_pattern.findall(segment):
638
- security_key = (security or "open").lower()
830
+ for device, ssid, security, passphrase, radius_server, radius_secret, channel in ssid_pattern.findall(segment):
831
+ security_key = (security or "open").lower().replace(" ", "-")
639
832
  auth_type, encrypt_type = SECURITY_TO_AUTH.get(security_key, ("0", "0"))
640
- ops.append({"op": "set_wireless_ssid", "device": device.strip(), "ssid": ssid, "security": security_key, "auth_type": auth_type, "encrypt_type": encrypt_type, "passphrase": passphrase or "", "channel": int(channel) if channel else 1})
833
+ operation = {
834
+ "op": "set_wireless_ssid",
835
+ "device": device.strip(),
836
+ "ssid": ssid,
837
+ "security": security_key,
838
+ "auth_type": auth_type,
839
+ "encrypt_type": encrypt_type,
840
+ "passphrase": passphrase or "",
841
+ "channel": int(channel) if channel else 1,
842
+ }
843
+ if radius_server:
844
+ operation["radius_server"] = radius_server
845
+ operation["radius_secret"] = radius_secret or ""
846
+ ops.append(operation)
641
847
  assoc_pattern = re.compile(r"associate\s+([A-Za-z0-9_ -]+?)\s+to\s+([A-Za-z0-9_ -]+?)\s+ssid\s+([A-Za-z0-9._-]+)(?:\s+(dhcp|static))?", flags=re.IGNORECASE)
642
848
  for segment in _command_segments(prompt):
643
849
  for client, ap, ssid, ip_mode in assoc_pattern.findall(segment):
@@ -739,6 +945,22 @@ def parse_intent(prompt: str) -> IntentPlan:
739
945
  host_link_intent = _extract_host_link_intent(normalized_prompt)
740
946
  host_vlan_assignment = _extract_host_vlan_assignment(normalized_prompt)
741
947
  network_style = _extract_network_style(normalized_prompt)
948
+ if (
949
+ network_style == "wireless_advanced"
950
+ and "server_aaa" in capability_set
951
+ and re.search(r"\bradius\b", lowered)
952
+ and not re.search(r"\b(?:aaa|acs|server aaa|aaa server)\b", lowered)
953
+ ):
954
+ capability_set.discard("server_aaa")
955
+ if re.search(r"\bqos\b", lowered) and (
956
+ re.search(r"\b(?:snmp|netflow|span|rspan|lldp|rep|dai|dot1x|802\.1x|dhcp snooping|port-security|port security)\b", lowered)
957
+ or network_style == "l2_security_monitoring"
958
+ ):
959
+ capability_set.add("qos")
960
+ if "dhcp_snooping" in capability_set and not re.search(r"\b(?:dhcp\s+pool|server\s+dhcp|dhcp\s+server|default-router)\b", lowered):
961
+ capability_set.discard("dhcp_pool")
962
+ capability_set.discard("router_dhcp")
963
+ capability_set.discard("server_dhcp")
742
964
  department_count = _extract_department_count(normalized_prompt)
743
965
  per_department_devices = _extract_per_department_devices(normalized_prompt)
744
966
 
@@ -793,12 +1015,61 @@ def parse_intent(prompt: str) -> IntentPlan:
793
1015
  capability_set.update({"wireless_client", "wireless_client_association"})
794
1016
  if any(op["op"] == "set_wireless_ssid" for op in wireless_ops):
795
1017
  capability_set.update({"wireless_ap", "wireless_mutation"})
1018
+ for op in wireless_ops:
1019
+ if op.get("op") != "set_wireless_ssid":
1020
+ continue
1021
+ security = str(op.get("security") or "")
1022
+ if security == "wep":
1023
+ capability_set.add("wep")
1024
+ if security in {"wpa-enterprise", "wpa2-enterprise", "802.1x"} or op.get("radius_server"):
1025
+ capability_set.add("wpa_enterprise")
1026
+ elif network_style == "wireless_advanced":
1027
+ capability_set.discard("wireless_ap")
1028
+ capability_set.discard("wireless_mutation")
796
1029
  if end_device_ops:
797
1030
  capability_set.add("end_device_mutation")
798
1031
  if iot_ops:
799
1032
  capability_set.update({"iot", "iot_registration"})
800
- if capability_set & {"vpn", "ipsec", "gre", "ppp"}:
1033
+ switch_op_names = {str(op.get("op")) for op in switch_ops}
1034
+ if "set_dhcp_snooping" in switch_op_names:
1035
+ capability_set.add("dhcp_snooping")
1036
+ if "set_dai" in switch_op_names:
1037
+ capability_set.add("dai")
1038
+ if "set_lldp" in switch_op_names:
1039
+ capability_set.add("lldp")
1040
+ if "set_rep" in switch_op_names:
1041
+ capability_set.add("rep")
1042
+ if "set_span" in switch_op_names:
1043
+ capability_set.add("span")
1044
+ if "set_port_security" in switch_op_names:
1045
+ capability_set.add("port_security")
1046
+ router_op_names = {str(op.get("op")) for op in router_ops}
1047
+ if "set_snmp_community" in router_op_names:
1048
+ capability_set.add("snmp")
1049
+ if "set_netflow" in router_op_names:
1050
+ capability_set.add("netflow")
1051
+ if router_op_names & {"enable_ipv6_unicast_routing", "set_ipv6_address", "set_ipv6_slaac"}:
1052
+ capability_set.add("ipv6_slaac")
1053
+ if "set_dhcpv6_pool" in router_op_names:
1054
+ capability_set.add("dhcpv6_stateful")
1055
+ if "set_ospfv3_interface" in router_op_names:
1056
+ capability_set.add("ospfv3")
1057
+ if "set_eigrp_ipv6_interface" in router_op_names:
1058
+ capability_set.add("eigrp_ipv6")
1059
+ if "set_ripng_interface" in router_op_names:
1060
+ capability_set.add("ripng")
1061
+ if "set_hsrp_ipv6" in router_op_names:
1062
+ capability_set.add("hsrp")
1063
+ if capability_set & {"vpn", "ipsec", "gre", "ppp", "security_edge"}:
801
1064
  network_style = network_style or "wan_security"
1065
+ if capability_set & {"ipv6_slaac", "dhcpv6_stateful", "dhcpv6_stateless", "ospfv3", "eigrp_ipv6", "ripng", "hsrp"}:
1066
+ network_style = network_style or "ipv6_routing"
1067
+ if capability_set & {"dhcp_snooping", "dai", "dot1x", "lldp", "rep", "snmp", "netflow", "span", "qos", "port_security"}:
1068
+ network_style = network_style or "l2_security_monitoring"
1069
+ if capability_set & {"wlc", "wpa_enterprise", "wep", "guest_wifi", "beamforming", "meraki", "cellular_5g", "bluetooth"}:
1070
+ network_style = "wireless_advanced"
1071
+ if capability_set & {"mqtt", "real_http", "real_websocket", "visual_scripting", "ptp", "profinet", "l2nat", "cyberobserver", "industrial_firewall"}:
1072
+ network_style = "industrial_iot"
802
1073
 
803
1074
  capabilities = sorted(capability_set)
804
1075
 
@@ -277,6 +277,64 @@ def inventory_management(root: ET.Element) -> dict[str, dict[str, object]]:
277
277
  return result
278
278
 
279
279
 
280
+ def inventory_routing(root: ET.Element) -> dict[str, dict[str, object]]:
281
+ result: dict[str, dict[str, object]] = {}
282
+ for device in root.findall(".//DEVICES/DEVICE"):
283
+ name = device.findtext("./ENGINE/NAME", default="")
284
+ running = "\n".join(line.text or "" for line in device.findall("./ENGINE/RUNNINGCONFIG/LINE"))
285
+ if not running:
286
+ continue
287
+ capabilities: set[str] = set()
288
+ if re.search(r"(?mi)^\s*ipv6\s+unicast-routing\s*$", running) or re.search(r"(?mi)^\s*ipv6\s+(?:enable|address)\b", running):
289
+ capabilities.add("ipv6_slaac")
290
+ if re.search(r"(?mi)^\s*ipv6\s+dhcp\s+pool\b", running) or re.search(r"(?mi)^\s*ipv6\s+dhcp\s+server\b", running):
291
+ capabilities.add("dhcpv6_stateful")
292
+ if re.search(r"(?mi)^\s*ipv6\s+ospf\b", running) or re.search(r"(?mi)^\s*ipv6\s+router\s+ospf\b", running):
293
+ capabilities.add("ospfv3")
294
+ if re.search(r"(?mi)^\s*ipv6\s+eigrp\b", running) or re.search(r"(?mi)^\s*ipv6\s+router\s+eigrp\b", running):
295
+ capabilities.add("eigrp_ipv6")
296
+ if re.search(r"(?mi)^\s*ipv6\s+rip\b", running):
297
+ capabilities.add("ripng")
298
+ if re.search(r"(?mi)^\s*standby\s+\d+\s+ipv6\b", running):
299
+ capabilities.add("hsrp")
300
+ if capabilities:
301
+ result[name] = {"capabilities": sorted(capabilities)}
302
+ return result
303
+
304
+
305
+ def inventory_l2_security_monitoring(root: ET.Element) -> dict[str, dict[str, object]]:
306
+ result: dict[str, dict[str, object]] = {}
307
+ for device in root.findall(".//DEVICES/DEVICE"):
308
+ name = device.findtext("./ENGINE/NAME", default="")
309
+ running = "\n".join(line.text or "" for line in device.findall("./ENGINE/RUNNINGCONFIG/LINE"))
310
+ if not running:
311
+ continue
312
+ capabilities: set[str] = set()
313
+ if re.search(r"(?mi)^\s*ip\s+dhcp\s+snooping\b", running):
314
+ capabilities.add("dhcp_snooping")
315
+ if re.search(r"(?mi)^\s*ip\s+arp\s+inspection\b", running):
316
+ capabilities.add("dai")
317
+ if re.search(r"(?mi)^\s*(?:aaa\s+new-model|radius-server\s+host|authentication\s+port-control|dot1x\b)", running):
318
+ capabilities.add("dot1x")
319
+ if re.search(r"(?mi)^\s*lldp\s+run\b", running):
320
+ capabilities.add("lldp")
321
+ if re.search(r"(?mi)^\s*rep\s+segment\b", running):
322
+ capabilities.add("rep")
323
+ if re.search(r"(?mi)^\s*snmp-server\b", running):
324
+ capabilities.add("snmp")
325
+ if re.search(r"(?mi)^\s*ip\s+flow-export\b", running) or re.search(r"(?mi)^\s*ip\s+flow\s+(?:ingress|egress)\b", running):
326
+ capabilities.add("netflow")
327
+ if re.search(r"(?mi)^\s*monitor\s+session\b", running):
328
+ capabilities.add("span")
329
+ if re.search(r"(?mi)^\s*(?:mls\s+qos|auto\s+qos|class-map|policy-map|service-policy)\b", running):
330
+ capabilities.add("qos")
331
+ if re.search(r"(?mi)^\s*switchport\s+port-security\b", running):
332
+ capabilities.add("port_security")
333
+ if capabilities:
334
+ result[name] = {"capabilities": sorted(capabilities)}
335
+ return result
336
+
337
+
280
338
  def inventory_topology_summary(root: ET.Element) -> dict[str, object]:
281
339
  devices = inventory_devices(root)
282
340
  counts: dict[str, int] = {}
@@ -303,6 +361,8 @@ def inventory_root(root: ET.Element) -> dict[str, object]:
303
361
  "dhcp_pools": inventory_dhcp_pools(root),
304
362
  "acl_names": inventory_acl_names(root),
305
363
  "management": inventory_management(root),
364
+ "routing": inventory_routing(root),
365
+ "l2_security_monitoring": inventory_l2_security_monitoring(root),
306
366
  "topology_summary": inventory_topology_summary(root),
307
367
  }
308
368
 
@@ -702,6 +762,45 @@ def _apply_switch_op(device: ET.Element, operation: dict[str, object]) -> None:
702
762
  for target in _config_targets(device):
703
763
  _append_config_block(target, f"interface {operation['port']}", body)
704
764
  return
765
+ elif operation["op"] == "set_dhcp_snooping":
766
+ for target in _config_targets(device):
767
+ _append_unique_config_lines(target, ["ip dhcp snooping", f"ip dhcp snooping vlan {operation['vlan']}"])
768
+ if operation.get("trust_port"):
769
+ _append_config_block(target, f"interface {operation['trust_port']}", [" ip dhcp snooping trust"])
770
+ return
771
+ elif operation["op"] == "set_dai":
772
+ for target in _config_targets(device):
773
+ _append_unique_config_lines(target, ["ip dhcp snooping", f"ip dhcp snooping vlan {operation['vlan']}", f"ip arp inspection vlan {operation['vlan']}"])
774
+ if operation.get("trust_port"):
775
+ _append_config_block(target, f"interface {operation['trust_port']}", [" ip arp inspection trust", " ip dhcp snooping trust"])
776
+ return
777
+ elif operation["op"] == "set_port_security":
778
+ body = [" switchport mode access", " switchport port-security"]
779
+ if operation.get("maximum"):
780
+ body.append(f" switchport port-security maximum {operation['maximum']}")
781
+ if operation.get("violation"):
782
+ body.append(f" switchport port-security violation {operation['violation']}")
783
+ for target in _config_targets(device):
784
+ _append_config_block(target, f"interface {operation['port']}", body)
785
+ return
786
+ elif operation["op"] == "set_lldp":
787
+ for target in _config_targets(device):
788
+ _append_unique_config_lines(target, ["lldp run"])
789
+ return
790
+ elif operation["op"] == "set_rep":
791
+ for target in _config_targets(device):
792
+ _append_config_block(target, f"interface {operation['interface']}", [f" rep segment {operation['segment']}"])
793
+ return
794
+ elif operation["op"] == "set_span":
795
+ for target in _config_targets(device):
796
+ _append_unique_config_lines(
797
+ target,
798
+ [
799
+ f"monitor session {operation['session']} source interface {operation['source']}",
800
+ f"monitor session {operation['session']} destination interface {operation['destination']}",
801
+ ],
802
+ )
803
+ return
705
804
  else:
706
805
  return
707
806
 
@@ -742,6 +841,81 @@ def _apply_router_op(device: ET.Element, operation: dict[str, object]) -> None:
742
841
  for target in _config_targets(device):
743
842
  _append_config_block(target, f"interface {operation['interface']}", [f" ip access-group {operation['acl_name']} {operation['direction']}"])
744
843
  return
844
+ elif operation["op"] == "enable_ipv6_unicast_routing":
845
+ for target in _config_targets(device):
846
+ _append_unique_config_lines(target, ["ipv6 unicast-routing"])
847
+ return
848
+ elif operation["op"] == "set_ipv6_address":
849
+ for target in _config_targets(device):
850
+ _append_unique_config_lines(target, ["ipv6 unicast-routing"])
851
+ _append_config_block(
852
+ target,
853
+ f"interface {operation['interface']}",
854
+ [f" ipv6 address {operation['address']}/{operation['prefix']}", " no shutdown"],
855
+ )
856
+ return
857
+ elif operation["op"] == "set_ipv6_slaac":
858
+ body = [" ipv6 enable", " no shutdown"]
859
+ if operation.get("prefix") and operation.get("prefix_len"):
860
+ body.insert(1, f" ipv6 nd prefix {operation['prefix']}/{operation['prefix_len']}")
861
+ for target in _config_targets(device):
862
+ _append_unique_config_lines(target, ["ipv6 unicast-routing"])
863
+ _append_config_block(target, f"interface {operation['interface']}", body)
864
+ return
865
+ elif operation["op"] == "set_dhcpv6_pool":
866
+ pool_body = [f" address prefix {operation['prefix']}/{operation['prefix_len']}"]
867
+ if operation.get("dns"):
868
+ pool_body.append(f" dns-server {operation['dns']}")
869
+ if operation.get("domain"):
870
+ pool_body.append(f" domain-name {operation['domain']}")
871
+ interface_body = [f" ipv6 dhcp server {operation['name']}", " ipv6 nd managed-config-flag", " no shutdown"]
872
+ for target in _config_targets(device):
873
+ _append_unique_config_lines(target, ["ipv6 unicast-routing"])
874
+ _append_config_block(target, f"ipv6 dhcp pool {operation['name']}", pool_body)
875
+ _append_config_block(target, f"interface {operation['interface']}", interface_body)
876
+ return
877
+ elif operation["op"] == "set_ospfv3_interface":
878
+ process_id = operation["process_id"]
879
+ for target in _config_targets(device):
880
+ _append_unique_config_lines(target, ["ipv6 unicast-routing"])
881
+ _append_config_block(target, f"interface {operation['interface']}", [f" ipv6 ospf {process_id} area {operation['area']}", " no shutdown"])
882
+ _append_config_block(target, f"ipv6 router ospf {process_id}", [])
883
+ return
884
+ elif operation["op"] == "set_eigrp_ipv6_interface":
885
+ asn = operation["asn"]
886
+ for target in _config_targets(device):
887
+ _append_unique_config_lines(target, ["ipv6 unicast-routing", "no ipv6 cef"])
888
+ _append_config_block(target, f"interface {operation['interface']}", [f" ipv6 eigrp {asn}", " no shutdown"])
889
+ _append_config_block(target, f"ipv6 router eigrp {asn}", [" no shutdown"])
890
+ return
891
+ elif operation["op"] == "set_ripng_interface":
892
+ for target in _config_targets(device):
893
+ _append_unique_config_lines(target, ["ipv6 unicast-routing"])
894
+ _append_config_block(target, f"interface {operation['interface']}", [f" ipv6 rip {operation['process_name']} enable", " no shutdown"])
895
+ return
896
+ elif operation["op"] == "set_hsrp_ipv6":
897
+ body = [" standby version 2", f" standby {operation['group']} ipv6 {operation['virtual_ipv6']}"]
898
+ if operation.get("priority"):
899
+ body.append(f" standby {operation['group']} priority {operation['priority']}")
900
+ body.append(f" standby {operation['group']} preempt")
901
+ for target in _config_targets(device):
902
+ _append_unique_config_lines(target, ["ipv6 unicast-routing"])
903
+ _append_config_block(target, f"interface {operation['interface']}", body)
904
+ return
905
+ elif operation["op"] == "set_snmp_community":
906
+ for target in _config_targets(device):
907
+ _append_unique_config_lines(target, [f"snmp-server community {operation['community']} {operation['mode']}"])
908
+ return
909
+ elif operation["op"] == "set_netflow":
910
+ global_lines = [
911
+ f"ip flow-export destination {operation['destination']} {operation['port']}",
912
+ f"ip flow-export version {operation['version']}",
913
+ ]
914
+ for target in _config_targets(device):
915
+ _append_unique_config_lines(target, global_lines)
916
+ if operation.get("interface") and operation.get("direction"):
917
+ _append_config_block(target, f"interface {operation['interface']}", [f" ip flow {operation['direction']}"])
918
+ return
745
919
  else:
746
920
  return
747
921
  for target in _config_targets(device):