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.
Files changed (43) hide show
  1. package/CHANGELOG.md +53 -2
  2. package/README.md +443 -124
  3. package/docs/automation-controller-proof.md +35 -0
  4. package/docs/campus-donor-proof.md +103 -0
  5. package/docs/curated-donor-registry.md +37 -0
  6. package/docs/generate-ready-pilot-design.md +30 -0
  7. package/docs/github-launch-ops-0.2.1.md +45 -0
  8. package/docs/github-launch-ops-0.2.2.md +42 -0
  9. package/docs/github-metadata.md +23 -6
  10. package/docs/home-iot-donor-proof.md +64 -0
  11. package/docs/industrial-programming-proof.md +48 -0
  12. package/docs/ipv4-routing-management-proof.md +37 -0
  13. package/docs/l2-resiliency-bgp-proof.md +60 -0
  14. package/docs/l2-security-qos-proof.md +59 -0
  15. package/docs/launch-announcement-0.2.2.md +15 -0
  16. package/docs/packet-tracer-feature-gap-atlas.md +244 -0
  17. package/docs/post-launch-follow-up.md +39 -0
  18. package/docs/publish-preview-roadmap.md +20 -17
  19. package/docs/release-checklist.md +32 -17
  20. package/docs/release-notes-0.2.1.md +7 -6
  21. package/docs/release-notes-0.2.2.md +26 -0
  22. package/docs/release-notes-0.2.3.md +59 -0
  23. package/docs/runtime-truth.md +31 -0
  24. package/docs/security-edge-deepening-proof.md +65 -0
  25. package/docs/voice-collaboration-proof.md +38 -0
  26. package/docs/wan-security-donor-proof.md +58 -0
  27. package/docs/wireless-advanced-proof.md +60 -0
  28. package/examples/README.md +11 -8
  29. package/examples/gallery.md +9 -3
  30. package/examples/index.json +3 -3
  31. package/package.json +20 -1
  32. package/references/curated-donor-registry.json +5 -2
  33. package/references/packettracer-feature-atlas.json +183 -0
  34. package/references/scenario-fixture-corpus.json +1 -0
  35. package/scripts/build_examples_index.py +26 -15
  36. package/scripts/coverage_matrix.py +1054 -25
  37. package/scripts/feature_atlas.py +293 -0
  38. package/scripts/generate_pkt.py +491 -34
  39. package/scripts/intent_parser.py +798 -8
  40. package/scripts/pkt_editor.py +651 -0
  41. package/scripts/remote_search.py +197 -21
  42. package/scripts/runtime_doctor.py +83 -10
  43. package/scripts/sample_catalog.py +266 -6
@@ -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,95 @@ CAPABILITY_PATTERNS = {
38
38
  "ospf": [r"\bospf\b"],
39
39
  "eigrp": [r"\beigrp\b"],
40
40
  "rip": [r"\brip\b"],
41
+ "ospfv2": [r"\bospfv2\b", r"\bospf v2\b", r"\bsingle-area ospf\b", r"\brouter ospf\b"],
42
+ "eigrp_ipv4": [r"\beigrp ipv4\b", r"\bipv4 eigrp\b", r"\brouter eigrp\b"],
43
+ "ripv2": [r"\bripv2\b", r"\brip v2\b", r"\brip version 2\b", r"\brouter rip\b"],
44
+ "static_route": [r"\bstatic route\b", r"\bstatic-route\b", r"\bip route\b"],
45
+ "default_route": [r"\bdefault route\b", r"\bdefault-route\b", r"\b0\.0\.0\.0/0\b"],
46
+ "dhcp_relay": [r"\bdhcp relay\b", r"\bdhcp-relay\b", r"\bhelper-address\b", r"\bip helper\b"],
47
+ "nat_static": [r"\bstatic nat\b", r"\bnat static\b"],
48
+ "nat_dynamic": [r"\bdynamic nat\b", r"\bnat dynamic\b"],
49
+ "pat": [r"\bpat\b", r"\boverload\b"],
50
+ "ssh_ios": [r"\bssh\b", r"\bip ssh\b", r"\bcrypto key\b"],
51
+ "ntp_ios": [r"\bntp\b", r"\bntp server\b"],
52
+ "syslog_ios": [r"\bsyslog\b", r"\blogging host\b"],
53
+ "ipv6_slaac": [r"\bslaac\b", r"\bipv6\s+slaac\b"],
54
+ "dhcpv6_stateful": [r"\bdhcpv6\b", r"\bstateful dhcpv6\b", r"\bipv6 stateful\b"],
55
+ "dhcpv6_stateless": [r"\bstateless dhcpv6\b", r"\bipv6 stateless\b"],
56
+ "ipv6_prefix_delegation": [r"\bprefix delegation\b", r"\bipv6 prefix\b"],
57
+ "ipv6_dns_aaaa": [r"\baaaa\b", r"\bipv6 dns\b"],
58
+ "ipv6_tunneling": [r"\bipv6ip\b", r"\bipv6 tunnel\b", r"\bipv6 tunneling\b"],
59
+ "isatap": [r"\bisatap\b"],
60
+ "ospfv3": [r"\bospfv3\b", r"\bipv6 ospf\b"],
61
+ "eigrp_ipv6": [r"\bipv6 eigrp\b", r"\beigrp ipv6\b"],
62
+ "ripng": [r"\bripng\b", r"\bipv6 rip\b"],
63
+ "hsrp": [r"\bhsrp\b"],
41
64
  "nat": [r"\bnat\b"],
42
65
  "acl": [r"\bacl\b", r"\baccess-list\b"],
66
+ "dhcp_snooping": [r"\bdhcp snooping\b"],
67
+ "dai": [r"\bdai\b", r"\bdynamic arp inspection\b"],
68
+ "dot1x": [r"\bdot1x\b", r"\b802\.1x\b", r"\bnac\b"],
69
+ "lldp": [r"\blldp\b"],
70
+ "rep": [r"\brep\b"],
71
+ "snmp": [r"\bsnmp\b"],
72
+ "netflow": [r"\bnetflow\b"],
73
+ "span": [r"\bmonitor\s+session\b", r"\bspan\b", r"\brspan\b"],
74
+ "qos": [r"\bqos policy\b", r"\bqos class\b", r"\bclass-map\b", r"\bpolicy-map\b", r"\bquality of service\b"],
75
+ "port_security": [r"\bport security\b", r"\bport-security\b"],
76
+ "bgp": [r"\bbgp\b", r"\brouter bgp\b", r"\bremote-as\b"],
77
+ "stp": [r"\bstp\b", r"\bspanning tree\b", r"\bspanning-tree\b", r"\bpvst\b"],
78
+ "rstp": [r"\brstp\b", r"\brapid-pvst\b", r"\brapid pvst\b", r"\brapid spanning\b"],
79
+ "etherchannel": [r"\betherchannel\b", r"\bport-channel\b", r"\bchannel-group\b"],
80
+ "lacp": [r"\blacp\b"],
81
+ "pagp": [r"\bpagp\b"],
82
+ "vtp": [r"\bvtp\b"],
83
+ "dtp": [r"\bdtp\b", r"\bdynamic desirable\b", r"\bdynamic auto\b"],
43
84
  "vpn": [r"\bvpn\b", r"\bsite-to-site\b", r"\bipsec\b", r"\btunnel\b"],
44
85
  "ipsec": [r"\bipsec\b", r"\bike\b", r"\bphase 1\b", r"\bphase 2\b"],
45
86
  "gre": [r"\bgre\b", r"\bgre tunnel\b"],
46
87
  "ppp": [r"\bppp\b", r"\bchap\b", r"\bpap\b"],
88
+ "security_edge": [r"\bsecurity edge\b", r"\bsecurity appliance\b", r"\bfirewall\b", r"\basa\b"],
89
+ "asa_acl_nat": [r"\basa acl\b", r"\basa nat\b", r"\basa acl nat\b"],
90
+ "asa_service_policy": [r"\basa service policy\b", r"\bservice policy\b"],
91
+ "clientless_vpn": [r"\bclientless vpn\b"],
92
+ "cbac": [r"\bcbac\b"],
93
+ "zfw": [r"\bzfw\b", r"\bzone based firewall\b", r"\bzone-based firewall\b"],
94
+ "sniffer": [r"\bsniffer\b"],
47
95
  "multilayer_switching": [r"\bmultilayer switch\b", r"\blayer 3 switch\b", r"\b3560\b", r"\bsvi\b"],
96
+ "wlc": [r"\bwlc\b", r"\bwireless lan controller\b"],
97
+ "wpa_enterprise": [r"\bwpa enterprise\b", r"\bwpa2 enterprise\b", r"\bwpa-enterprise\b", r"\bwpa2-enterprise\b", r"\b802\.1x wireless\b"],
98
+ "wep": [r"\bwep\b"],
99
+ "guest_wifi": [r"\bguest wifi\b", r"\bguest wlan\b"],
100
+ "beamforming": [r"\bbeamforming\b"],
101
+ "meraki": [r"\bmeraki\b"],
102
+ "cellular_5g": [r"\b5g\b", r"\bcellular\b", r"\bcell tower\b"],
103
+ "bluetooth": [r"\bbluetooth\b"],
104
+ "network_controller": [r"\bnetwork controller\b", r"\bnetcon\b"],
105
+ "python_programming": [r"\bpython programming\b", r"\bpython app\b"],
106
+ "javascript_programming": [r"\bjavascript\b", r"\bjs app\b"],
107
+ "blockly_programming": [r"\bblockly\b"],
108
+ "tcp_udp_app": [r"\btcp test app\b", r"\budp test app\b", r"\btcp app\b", r"\budp app\b"],
109
+ "vm_iox": [r"\biox\b", r"\bvm\b", r"\bvirtual machine\b"],
110
+ "voip": [r"\bvoip\b"],
111
+ "ip_phone": [r"\bip phone\b", r"\bphone\b"],
112
+ "call_manager": [r"\bcall manager\b", r"\btelephony\b"],
113
+ "linksys_voice": [r"\blinksys voice\b"],
114
+ "mqtt": [r"\bmqtt\b"],
115
+ "real_http": [r"\breal http\b"],
116
+ "real_websocket": [r"\bwebsocket\b", r"\breal websocket\b"],
117
+ "visual_scripting": [r"\bvisual scripting\b"],
118
+ "ptp": [r"\bptp\b", r"\bprecision time protocol\b"],
119
+ "profinet": [r"\bprofinet\b"],
120
+ "l2nat": [r"\bl2nat\b", r"\bl2 nat\b"],
121
+ "cyberobserver": [r"\bcyberobserver\b"],
122
+ "industrial_firewall": [r"\bindustrial firewall\b", r"\bisa3000\b"],
123
+ "coaxial": [r"\bcoaxial\b"],
124
+ "cable_dsl": [r"\bcable modem\b", r"\bdsl\b", r"\bdsl modem\b"],
125
+ "central_office": [r"\bcentral office\b"],
126
+ "cell_tower": [r"\bcell tower\b"],
127
+ "power_distribution": [r"\bpower distribution\b"],
128
+ "hot_swappable": [r"\bhot swappable\b", r"\bhot-swappable\b"],
129
+ "ios_license": [r"\bios license\b", r"\blicense\b", r"\bios15\b", r"\bios 15\b"],
48
130
  }
49
131
 
50
132
  DEVICE_SYNONYMS = {
@@ -97,6 +179,15 @@ NATURAL_DEVICE_ALIASES = {
97
179
  NETWORK_STYLE_PATTERNS = {
98
180
  "campus": [r"\bcampus\b", r"\bkampus\b", r"\bsobeli\b", r"\bdepart", r"\bdepartment\b"],
99
181
  "branch": [r"\bbranch\b", r"\bfilial\b"],
182
+ "ipv6_routing": [r"\bipv6\b", r"\bslaac\b", r"\bdhcpv6\b", r"\bospfv3\b", r"\bhsrp\b"],
183
+ "ipv4_routing_management": [r"\bospfv2\b", r"\bospf v2\b", r"\beigrp ipv4\b", r"\bripv2\b", r"\brip version 2\b", r"\bstatic route\b", r"\bdefault route\b", r"\bdhcp relay\b", r"\bnat\b", r"\bpat\b", r"\bip ssh\b", r"\bntp server\b", r"\blogging host\b"],
184
+ "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"],
185
+ "l2_resiliency_routing": [r"\bbgp\b", r"\bstp\b", r"\brstp\b", r"\bspanning tree\b", r"\bspanning-tree\b", r"\betherchannel\b", r"\bport-channel\b", r"\bchannel-group\b", r"\blacp\b", r"\bpagp\b", r"\bvtp\b", r"\bdtp\b"],
186
+ "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"],
187
+ "automation_controller": [r"\bnetwork controller\b", r"\bnetcon\b", r"\bblockly\b", r"\bjavascript\b", r"\bpython programming\b", r"\biox\b"],
188
+ "voice_collaboration": [r"\bvoip\b", r"\bip phone\b", r"\bcall manager\b", r"\btelephony\b"],
189
+ "industrial_iot": [r"\bmqtt\b", r"\bwebsocket\b", r"\bprofinet\b", r"\bl2nat\b", r"\bptp\b", r"\bcyberobserver\b", r"\bindustrial\b"],
190
+ "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
191
  "small_office": [r"\bsmall office\b", r"\bhome\b", r"\bofis\b"],
101
192
  "wireless_branch": [r"\bwireless\b", r"\bwifi\b", r"\bssid\b"],
102
193
  "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 +227,8 @@ SECURITY_TO_AUTH = {
136
227
  "wpa": ("2", "2"),
137
228
  "wpa2": ("4", "4"),
138
229
  "802.1x": ("5", "5"),
230
+ "wpa-enterprise": ("5", "5"),
231
+ "wpa2-enterprise": ("5", "5"),
139
232
  }
140
233
 
141
234
  TRANSLITERATION_TABLE = str.maketrans(
@@ -163,7 +256,11 @@ TRANSLITERATION_TABLE = str.maketrans(
163
256
 
164
257
 
165
258
  def _command_segments(prompt: str) -> list[str]:
166
- parts = re.split(r"(?=\b(?:device|connect|link|set|enable|associate|change|update|rename|apply)\b)", prompt, flags=re.IGNORECASE)
259
+ parts = re.split(
260
+ r"(?=(?<!-)\b(?:device|connect|link|set|enable|associate|change|update|rename|apply)\b)",
261
+ prompt,
262
+ flags=re.IGNORECASE,
263
+ )
167
264
  return [part.strip() for part in parts if part.strip()]
168
265
 
169
266
 
@@ -235,6 +332,7 @@ class IntentPlan:
235
332
  management_ops: list[dict[str, object]] = field(default_factory=list)
236
333
  verification_ops: list[dict[str, object]] = field(default_factory=list)
237
334
  iot_ops: list[dict[str, object]] = field(default_factory=list)
335
+ programming_ops: list[dict[str, object]] = field(default_factory=list)
238
336
 
239
337
  def all_operations(self) -> list[dict[str, object]]:
240
338
  merged: list[dict[str, object]] = []
@@ -248,6 +346,7 @@ class IntentPlan:
248
346
  self.management_ops,
249
347
  self.verification_ops,
250
348
  self.iot_ops,
349
+ self.programming_ops,
251
350
  ]:
252
351
  merged.extend(bucket)
253
352
  return merged
@@ -389,7 +488,7 @@ def _extract_service_requirements(capabilities: list[str], prompt: str) -> dict[
389
488
  for service in ["dhcp", "dns", "http", "https", "ftp", "tftp", "ntp", "email", "syslog", "aaa"]:
390
489
  if service in lowered:
391
490
  requirements["services"].append(service)
392
- for security in ["acl", "telnet", "wpa2", "wpa", "wep", "nat", "vpn", "ipsec", "gre", "ppp", "ssh"]:
491
+ for security in ["acl", "telnet", "wpa2", "wpa", "wep", "nat", "vpn", "ipsec", "gre", "ppp", "security_edge", "ssh"]:
393
492
  if security in lowered:
394
493
  requirements["security"].append(security)
395
494
  return requirements
@@ -503,6 +602,122 @@ def _extract_switch_ops(prompt: str) -> list[dict[str, object]]:
503
602
  "native": int(native) if native else None,
504
603
  }
505
604
  )
605
+ 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)
606
+ 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)
607
+ port_security_pattern = re.compile(
608
+ 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))?",
609
+ flags=re.IGNORECASE,
610
+ )
611
+ lldp_pattern = re.compile(r"set\s+([A-Za-z0-9_-]+)\s+lldp(?:\s+(?:enable|run))?", flags=re.IGNORECASE)
612
+ 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)
613
+ span_pattern = re.compile(
614
+ 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/._-]+)",
615
+ flags=re.IGNORECASE,
616
+ )
617
+ dot1x_pattern = re.compile(
618
+ r"set\s+([A-Za-z0-9_-]+)\s+dot1x\s+interface\s+([A-Za-z0-9/._-]+)\s+mode\s+(auto|force-authorized|force-unauthorized)"
619
+ r"(?:\s+radius\s+(\d+\.\d+\.\d+\.\d+)\s+key\s+([A-Za-z0-9._-]+))?",
620
+ flags=re.IGNORECASE,
621
+ )
622
+ qos_pattern = re.compile(
623
+ r"set\s+([A-Za-z0-9_-]+)\s+qos\s+class-map\s+([A-Za-z0-9_-]+)\s+match\s+(.+?)\s+policy-map\s+([A-Za-z0-9_-]+)\s+class\s+\2\s+(priority|bandwidth\s+\d+|police\s+\d+)"
624
+ r"\s+service-policy\s+(input|output)\s+([A-Za-z0-9/._-]+)(?=\s+set\s+|$)",
625
+ flags=re.IGNORECASE,
626
+ )
627
+ stp_pattern = re.compile(
628
+ r"set\s+([A-Za-z0-9_-]+)\s+stp\s+mode\s+([A-Za-z0-9_-]+)(?:\s+vlan\s+(\d+)\s+root\s+(primary|secondary))?",
629
+ flags=re.IGNORECASE,
630
+ )
631
+ etherchannel_pattern = re.compile(
632
+ r"set\s+([A-Za-z0-9_-]+)\s+etherchannel\s+(\d+)\s+mode\s+(active|passive|desirable|auto|on)\s+interfaces\s+(.+?)(?=\s+set\s+|$)",
633
+ flags=re.IGNORECASE,
634
+ )
635
+ vtp_pattern = re.compile(
636
+ r"set\s+([A-Za-z0-9_-]+)\s+vtp\s+domain\s+([A-Za-z0-9_.-]+)\s+mode\s+(server|client|transparent)(?:\s+version\s+(\d+))?",
637
+ flags=re.IGNORECASE,
638
+ )
639
+ dtp_pattern = re.compile(
640
+ r"set\s+([A-Za-z0-9_-]+)\s+dtp\s+interface\s+([A-Za-z0-9/._-]+)\s+mode\s+(dynamic\s+desirable|dynamic\s+auto|trunk|access)",
641
+ flags=re.IGNORECASE,
642
+ )
643
+ for segment in _command_segments(prompt):
644
+ for device, vlan_id, trust_port in dhcp_snooping_pattern.findall(segment):
645
+ ops.append({"op": "set_dhcp_snooping", "device": device, "vlan": int(vlan_id), "trust_port": trust_port or None})
646
+ for device, vlan_id, trust_port in dai_pattern.findall(segment):
647
+ ops.append({"op": "set_dai", "device": device, "vlan": int(vlan_id), "trust_port": trust_port or None})
648
+ for device, port, maximum, violation in port_security_pattern.findall(segment):
649
+ ops.append(
650
+ {
651
+ "op": "set_port_security",
652
+ "device": device,
653
+ "port": port,
654
+ "maximum": int(maximum) if maximum else None,
655
+ "violation": violation.lower() if violation else None,
656
+ }
657
+ )
658
+ for device in lldp_pattern.findall(segment):
659
+ ops.append({"op": "set_lldp", "device": device})
660
+ for device, segment_id, interface_name in rep_pattern.findall(segment):
661
+ ops.append({"op": "set_rep", "device": device, "segment": int(segment_id), "interface": interface_name})
662
+ for device, session, source, destination in span_pattern.findall(segment):
663
+ ops.append({"op": "set_span", "device": device, "session": int(session), "source": source, "destination": destination})
664
+ for device, interface_name, mode, radius_host, radius_key in dot1x_pattern.findall(segment):
665
+ ops.append(
666
+ {
667
+ "op": "set_dot1x",
668
+ "device": device,
669
+ "interface": interface_name,
670
+ "mode": mode.lower(),
671
+ "radius_host": radius_host or None,
672
+ "radius_key": radius_key or None,
673
+ }
674
+ )
675
+ for device, class_map, match_expr, policy_map, action, direction, interface_name in qos_pattern.findall(segment):
676
+ ops.append(
677
+ {
678
+ "op": "set_qos_policy",
679
+ "device": device,
680
+ "class_map": class_map,
681
+ "match": match_expr.strip(),
682
+ "policy_map": policy_map,
683
+ "action": action.lower(),
684
+ "direction": direction.lower(),
685
+ "interface": interface_name,
686
+ }
687
+ )
688
+ for device, mode, vlan_id, root_priority in stp_pattern.findall(segment):
689
+ ops.append(
690
+ {
691
+ "op": "set_stp",
692
+ "device": device,
693
+ "mode": mode.lower(),
694
+ "vlan": int(vlan_id) if vlan_id else None,
695
+ "root": root_priority.lower() if root_priority else None,
696
+ }
697
+ )
698
+ for device, channel, mode, interfaces_text in etherchannel_pattern.findall(segment):
699
+ interfaces = [value for value in re.findall(r"[A-Za-z]+[A-Za-z]*[0-9/._-]+", interfaces_text) if value.lower() != "set"]
700
+ ops.append(
701
+ {
702
+ "op": "set_etherchannel",
703
+ "device": device,
704
+ "channel": int(channel),
705
+ "mode": mode.lower(),
706
+ "interfaces": interfaces,
707
+ }
708
+ )
709
+ for device, domain, mode, version in vtp_pattern.findall(segment):
710
+ ops.append(
711
+ {
712
+ "op": "set_vtp",
713
+ "device": device,
714
+ "domain": domain,
715
+ "mode": mode.lower(),
716
+ "version": int(version) if version else None,
717
+ }
718
+ )
719
+ for device, interface_name, mode in dtp_pattern.findall(segment):
720
+ ops.append({"op": "set_dtp", "device": device, "interface": interface_name, "mode": mode.lower()})
506
721
  return ops
507
722
 
508
723
 
@@ -570,6 +785,319 @@ def _extract_router_ops(prompt: str) -> list[dict[str, object]]:
570
785
  for segment in _command_segments(prompt):
571
786
  for acl_name, direction, device, interface_name in acl_apply_pattern.findall(segment):
572
787
  ops.append({"op": "apply_acl", "device": device.strip(), "acl_name": acl_name, "direction": direction.lower(), "interface": interface_name})
788
+ ipv6_unicast_pattern = re.compile(r"set\s+([A-Za-z0-9_-]+)\s+ipv6\s+unicast-routing", flags=re.IGNORECASE)
789
+ ipv6_address_pattern = re.compile(
790
+ r"set\s+([A-Za-z0-9_-]+)\s+(?:interface\s+)?([A-Za-z0-9/._-]+)\s+ipv6\s+([0-9A-Fa-f:]+)/(\d+)",
791
+ flags=re.IGNORECASE,
792
+ )
793
+ ipv6_slaac_pattern = re.compile(
794
+ r"set\s+([A-Za-z0-9_-]+)\s+slaac\s+on\s+([A-Za-z0-9/._-]+)(?:\s+prefix\s+([0-9A-Fa-f:]+)/(\d+))?",
795
+ flags=re.IGNORECASE,
796
+ )
797
+ dhcpv6_pattern = re.compile(
798
+ 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/._-]+)"
799
+ r"(?:\s+dns\s+([0-9A-Fa-f:]+))?(?:\s+domain\s+([A-Za-z0-9._-]+))?",
800
+ flags=re.IGNORECASE,
801
+ )
802
+ 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)
803
+ 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)
804
+ 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)
805
+ hsrp_pattern = re.compile(
806
+ 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+))?",
807
+ flags=re.IGNORECASE,
808
+ )
809
+ ospfv2_pattern = re.compile(
810
+ r"set\s+([A-Za-z0-9_-]+)\s+ospfv2\s+(\d+)\s+network\s+(\d+\.\d+\.\d+\.\d+)\s+wildcard\s+(\d+\.\d+\.\d+\.\d+)\s+area\s+(\d+)",
811
+ flags=re.IGNORECASE,
812
+ )
813
+ eigrp_ipv4_pattern = re.compile(
814
+ r"set\s+([A-Za-z0-9_-]+)\s+eigrp\s+ipv4\s+(\d+)\s+network\s+(\d+\.\d+\.\d+\.\d+)\s+wildcard\s+(\d+\.\d+\.\d+\.\d+)(?:\s+(no-auto-summary))?",
815
+ flags=re.IGNORECASE,
816
+ )
817
+ ripv2_pattern = re.compile(
818
+ r"set\s+([A-Za-z0-9_-]+)\s+rip\s+version\s+2\s+network\s+(\d+\.\d+\.\d+\.\d+)(?:\s+(no-auto-summary))?",
819
+ flags=re.IGNORECASE,
820
+ )
821
+ static_route_pattern = re.compile(
822
+ r"set\s+([A-Za-z0-9_-]+)\s+static-route\s+(\d+\.\d+\.\d+\.\d+)/(\d+)\s+via\s+(\d+\.\d+\.\d+\.\d+)",
823
+ flags=re.IGNORECASE,
824
+ )
825
+ dhcp_relay_pattern = re.compile(
826
+ r"set\s+([A-Za-z0-9_-]+)\s+dhcp-relay\s+interface\s+([A-Za-z0-9/._-]+)\s+helper\s+(\d+\.\d+\.\d+\.\d+)",
827
+ flags=re.IGNORECASE,
828
+ )
829
+ nat_inside_outside_pattern = re.compile(
830
+ r"set\s+([A-Za-z0-9_-]+)\s+nat\s+(inside|outside)\s+interface\s+([A-Za-z0-9/._-]+)",
831
+ flags=re.IGNORECASE,
832
+ )
833
+ nat_static_pattern = re.compile(
834
+ r"set\s+([A-Za-z0-9_-]+)\s+nat\s+static\s+(\d+\.\d+\.\d+\.\d+)\s+(\d+\.\d+\.\d+\.\d+)",
835
+ flags=re.IGNORECASE,
836
+ )
837
+ pat_pattern = re.compile(
838
+ r"set\s+([A-Za-z0-9_-]+)\s+pat\s+acl\s+([A-Za-z0-9_-]+)\s+interface\s+([A-Za-z0-9/._-]+)(?:\s+(overload))?",
839
+ flags=re.IGNORECASE,
840
+ )
841
+ ssh_ios_pattern = re.compile(
842
+ r"set\s+([A-Za-z0-9_-]+)\s+ssh\s+domain\s+([A-Za-z0-9._-]+)\s+username\s+([A-Za-z0-9._-]+)\s+password\s+([A-Za-z0-9._-]+)(?:\s+modulus\s+(\d+))?",
843
+ flags=re.IGNORECASE,
844
+ )
845
+ ntp_ios_pattern = re.compile(r"set\s+([A-Za-z0-9_-]+)\s+ntp\s+server\s+(\d+\.\d+\.\d+\.\d+)", flags=re.IGNORECASE)
846
+ syslog_ios_pattern = re.compile(r"set\s+([A-Za-z0-9_-]+)\s+syslog\s+server\s+(\d+\.\d+\.\d+\.\d+)", flags=re.IGNORECASE)
847
+ bgp_pattern = re.compile(
848
+ r"set\s+([A-Za-z0-9_-]+)\s+bgp\s+(\d+)\s+neighbor\s+(\d+\.\d+\.\d+\.\d+)\s+remote-as\s+(\d+)"
849
+ r"(?:\s+network\s+(\d+\.\d+\.\d+\.\d+)\s+mask\s+(\d+\.\d+\.\d+\.\d+))?",
850
+ flags=re.IGNORECASE,
851
+ )
852
+ 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)
853
+ netflow_pattern = re.compile(
854
+ 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))?",
855
+ flags=re.IGNORECASE,
856
+ )
857
+ gre_tunnel_pattern = re.compile(
858
+ r"set\s+([A-Za-z0-9_-]+)\s+gre\s+tunnel\s+([A-Za-z0-9/._-]+)\s+source\s+([A-Za-z0-9/._:-]+)\s+destination\s+([A-Za-z0-9/._:-]+)"
859
+ r"(?:\s+ip\s+(\d+\.\d+\.\d+\.\d+)/(\d+))?",
860
+ flags=re.IGNORECASE,
861
+ )
862
+ ppp_pattern = re.compile(
863
+ r"set\s+([A-Za-z0-9_-]+)\s+ppp\s+interface\s+([A-Za-z0-9/._-]+)(?:\s+authentication\s+(chap|pap))?",
864
+ flags=re.IGNORECASE,
865
+ )
866
+ ipsec_transform_pattern = re.compile(
867
+ r"set\s+([A-Za-z0-9_-]+)\s+ipsec\s+transform-set\s+([A-Za-z0-9_-]+)\s+([A-Za-z0-9_-]+)\s+([A-Za-z0-9_-]+)",
868
+ flags=re.IGNORECASE,
869
+ )
870
+ crypto_map_pattern = re.compile(
871
+ r"set\s+([A-Za-z0-9_-]+)\s+crypto\s+map\s+([A-Za-z0-9_-]+)\s+(\d+)\s+peer\s+(\d+\.\d+\.\d+\.\d+)\s+transform-set\s+([A-Za-z0-9_-]+)\s+match\s+([A-Za-z0-9_-]+)(?:\s+interface\s+([A-Za-z0-9/._-]+))?",
872
+ flags=re.IGNORECASE,
873
+ )
874
+ cbac_pattern = re.compile(
875
+ r"set\s+([A-Za-z0-9_-]+)\s+cbac\s+inspect\s+([A-Za-z0-9_-]+)\s+protocol\s+([A-Za-z0-9_-]+)\s+interface\s+([A-Za-z0-9/._-]+)\s+direction\s+(in|out)",
876
+ flags=re.IGNORECASE,
877
+ )
878
+ zfw_zone_interface_pattern = re.compile(
879
+ r"set\s+([A-Za-z0-9_-]+)\s+zfw\s+zone\s+([A-Za-z0-9_-]+)\s+interface\s+([A-Za-z0-9/._-]+)",
880
+ flags=re.IGNORECASE,
881
+ )
882
+ zfw_zone_pair_pattern = re.compile(
883
+ r"set\s+([A-Za-z0-9_-]+)\s+zfw\s+zone-pair\s+([A-Za-z0-9_-]+)\s+source\s+([A-Za-z0-9_-]+)\s+destination\s+([A-Za-z0-9_-]+)\s+policy\s+([A-Za-z0-9_-]+)",
884
+ flags=re.IGNORECASE,
885
+ )
886
+ zfw_policy_pattern = re.compile(
887
+ r"set\s+([A-Za-z0-9_-]+)\s+zfw\s+class-map\s+([A-Za-z0-9_-]+)\s+match\s+protocol\s+([A-Za-z0-9_-]+)\s+policy-map\s+([A-Za-z0-9_-]+)\s+action\s+(inspect|pass|drop)",
888
+ flags=re.IGNORECASE,
889
+ )
890
+ telephony_service_pattern = re.compile(
891
+ r'set\s+"?([^"]+?)"?\s+telephony\s+service\s+source-address\s+(\d+\.\d+\.\d+\.\d+)\s+port\s+(\d+)'
892
+ r"(?:\s+max-ephones\s+(\d+))?(?:\s+max-dn\s+(\d+))?",
893
+ flags=re.IGNORECASE,
894
+ )
895
+ ephone_dn_pattern = re.compile(r'set\s+"?([^"]+?)"?\s+ephone-dn\s+(\d+)\s+number\s+([A-Za-z0-9*+#.-]+)', flags=re.IGNORECASE)
896
+ ephone_pattern = re.compile(
897
+ r'set\s+"?([^"]+?)"?\s+ephone\s+(\d+)\s+mac\s+([0-9A-Fa-f.:-]+)\s+button\s+([0-9:]+)',
898
+ flags=re.IGNORECASE,
899
+ )
900
+ dial_peer_pattern = re.compile(
901
+ r'set\s+"?([^"]+?)"?\s+dial-peer\s+voice\s+(\d+)\s+destination-pattern\s+(\S+)\s+session-target\s+ipv4:(\d+\.\d+\.\d+\.\d+)',
902
+ flags=re.IGNORECASE,
903
+ )
904
+ for segment in _command_segments(prompt):
905
+ for device in ipv6_unicast_pattern.findall(segment):
906
+ ops.append({"op": "enable_ipv6_unicast_routing", "device": device})
907
+ for device, interface_name, address, prefix in ipv6_address_pattern.findall(segment):
908
+ ops.append({"op": "set_ipv6_address", "device": device, "interface": interface_name, "address": address, "prefix": int(prefix)})
909
+ for device, interface_name, prefix_address, prefix_len in ipv6_slaac_pattern.findall(segment):
910
+ ops.append(
911
+ {
912
+ "op": "set_ipv6_slaac",
913
+ "device": device,
914
+ "interface": interface_name,
915
+ "prefix": prefix_address or None,
916
+ "prefix_len": int(prefix_len) if prefix_len else None,
917
+ }
918
+ )
919
+ for device, name, prefix_address, prefix_len, interface_name, dns, domain in dhcpv6_pattern.findall(segment):
920
+ ops.append(
921
+ {
922
+ "op": "set_dhcpv6_pool",
923
+ "device": device,
924
+ "name": name,
925
+ "prefix": prefix_address,
926
+ "prefix_len": int(prefix_len),
927
+ "interface": interface_name,
928
+ "dns": dns or None,
929
+ "domain": domain or None,
930
+ }
931
+ )
932
+ for device, process_id, area, interface_name in ospfv3_pattern.findall(segment):
933
+ ops.append({"op": "set_ospfv3_interface", "device": device, "process_id": int(process_id), "area": int(area), "interface": interface_name})
934
+ for device, asn, interface_name in eigrp_ipv6_pattern.findall(segment):
935
+ ops.append({"op": "set_eigrp_ipv6_interface", "device": device, "asn": int(asn), "interface": interface_name})
936
+ for device, process_name, interface_name in ripng_pattern.findall(segment):
937
+ ops.append({"op": "set_ripng_interface", "device": device, "process_name": process_name, "interface": interface_name})
938
+ for device, group, virtual_ipv6, interface_name, priority in hsrp_pattern.findall(segment):
939
+ ops.append(
940
+ {
941
+ "op": "set_hsrp_ipv6",
942
+ "device": device,
943
+ "group": int(group),
944
+ "virtual_ipv6": virtual_ipv6,
945
+ "interface": interface_name,
946
+ "priority": int(priority) if priority else None,
947
+ }
948
+ )
949
+ for device, process_id, network, wildcard, area in ospfv2_pattern.findall(segment):
950
+ ops.append({"op": "set_ospfv2_network", "device": device, "process_id": int(process_id), "network": network, "wildcard": wildcard, "area": int(area)})
951
+ for device, asn, network, wildcard, no_auto in eigrp_ipv4_pattern.findall(segment):
952
+ ops.append({"op": "set_eigrp_ipv4_network", "device": device, "asn": int(asn), "network": network, "wildcard": wildcard, "no_auto_summary": bool(no_auto)})
953
+ for device, network, no_auto in ripv2_pattern.findall(segment):
954
+ ops.append({"op": "set_ripv2_network", "device": device, "network": network, "no_auto_summary": bool(no_auto)})
955
+ for device, network, prefix, next_hop in static_route_pattern.findall(segment):
956
+ ops.append({"op": "set_static_route", "device": device, "network": network, "prefix": int(prefix), "next_hop": next_hop})
957
+ for device, interface_name, helper in dhcp_relay_pattern.findall(segment):
958
+ ops.append({"op": "set_dhcp_relay", "device": device, "interface": interface_name, "helper": helper})
959
+ for device, role, interface_name in nat_inside_outside_pattern.findall(segment):
960
+ ops.append({"op": "set_nat_interface", "device": device, "role": role.lower(), "interface": interface_name})
961
+ for device, inside_local, inside_global in nat_static_pattern.findall(segment):
962
+ ops.append({"op": "set_nat_static", "device": device, "inside_local": inside_local, "inside_global": inside_global})
963
+ for device, acl, interface_name, overload in pat_pattern.findall(segment):
964
+ ops.append({"op": "set_pat_overload", "device": device, "acl": acl, "interface": interface_name, "overload": bool(overload)})
965
+ for device, domain, username, password, modulus in ssh_ios_pattern.findall(segment):
966
+ ops.append({"op": "set_ssh_ios", "device": device, "domain": domain, "username": username, "password": password, "modulus": int(modulus) if modulus else 1024})
967
+ for device, server in ntp_ios_pattern.findall(segment):
968
+ ops.append({"op": "set_ntp_server", "device": device, "server": server})
969
+ for device, server in syslog_ios_pattern.findall(segment):
970
+ ops.append({"op": "set_syslog_server", "device": device, "server": server})
971
+ for device, asn, neighbor, remote_as, network, mask in bgp_pattern.findall(segment):
972
+ ops.append(
973
+ {
974
+ "op": "set_bgp_neighbor",
975
+ "device": device,
976
+ "asn": int(asn),
977
+ "neighbor": neighbor,
978
+ "remote_as": int(remote_as),
979
+ "network": network or None,
980
+ "mask": mask or None,
981
+ }
982
+ )
983
+ for device, community, mode in snmp_pattern.findall(segment):
984
+ ops.append({"op": "set_snmp_community", "device": device, "community": community, "mode": mode.lower()})
985
+ for device, destination, port, version, interface_name, direction in netflow_pattern.findall(segment):
986
+ ops.append(
987
+ {
988
+ "op": "set_netflow",
989
+ "device": device,
990
+ "destination": destination,
991
+ "port": int(port),
992
+ "version": int(version) if version else 9,
993
+ "interface": interface_name or None,
994
+ "direction": direction.lower() if direction else None,
995
+ }
996
+ )
997
+ for device, tunnel_interface, source, destination, ip, prefix in gre_tunnel_pattern.findall(segment):
998
+ ops.append(
999
+ {
1000
+ "op": "set_gre_tunnel",
1001
+ "device": device,
1002
+ "interface": tunnel_interface,
1003
+ "source": source,
1004
+ "destination": destination,
1005
+ "ip": ip or None,
1006
+ "prefix": int(prefix) if prefix else None,
1007
+ }
1008
+ )
1009
+ for device, interface_name, authentication in ppp_pattern.findall(segment):
1010
+ ops.append(
1011
+ {
1012
+ "op": "set_ppp_interface",
1013
+ "device": device,
1014
+ "interface": interface_name,
1015
+ "authentication": authentication.lower() if authentication else None,
1016
+ }
1017
+ )
1018
+ for device, name, encryption, integrity in ipsec_transform_pattern.findall(segment):
1019
+ ops.append(
1020
+ {
1021
+ "op": "set_ipsec_transform_set",
1022
+ "device": device,
1023
+ "name": name,
1024
+ "encryption": encryption,
1025
+ "integrity": integrity,
1026
+ }
1027
+ )
1028
+ for device, map_name, sequence, peer, transform_set, acl_name, interface_name in crypto_map_pattern.findall(segment):
1029
+ ops.append(
1030
+ {
1031
+ "op": "set_crypto_map",
1032
+ "device": device,
1033
+ "map_name": map_name,
1034
+ "sequence": int(sequence),
1035
+ "peer": peer,
1036
+ "transform_set": transform_set,
1037
+ "acl_name": acl_name,
1038
+ "interface": interface_name or None,
1039
+ }
1040
+ )
1041
+ for device, name, protocol, interface_name, direction in cbac_pattern.findall(segment):
1042
+ ops.append(
1043
+ {
1044
+ "op": "set_cbac_inspect",
1045
+ "device": device,
1046
+ "name": name,
1047
+ "protocol": protocol.lower(),
1048
+ "interface": interface_name,
1049
+ "direction": direction.lower(),
1050
+ }
1051
+ )
1052
+ for device, zone, interface_name in zfw_zone_interface_pattern.findall(segment):
1053
+ ops.append({"op": "set_zfw_zone_interface", "device": device, "zone": zone, "interface": interface_name})
1054
+ for device, pair_name, source, destination, policy in zfw_zone_pair_pattern.findall(segment):
1055
+ ops.append(
1056
+ {
1057
+ "op": "set_zfw_zone_pair",
1058
+ "device": device,
1059
+ "pair_name": pair_name,
1060
+ "source": source,
1061
+ "destination": destination,
1062
+ "policy": policy,
1063
+ }
1064
+ )
1065
+ for device, class_map, protocol, policy_map, action in zfw_policy_pattern.findall(segment):
1066
+ ops.append(
1067
+ {
1068
+ "op": "set_zfw_policy",
1069
+ "device": device,
1070
+ "class_map": class_map,
1071
+ "protocol": protocol.lower(),
1072
+ "policy_map": policy_map,
1073
+ "action": action.lower(),
1074
+ }
1075
+ )
1076
+ for device, source_address, port, max_ephones, max_dn in telephony_service_pattern.findall(segment):
1077
+ ops.append(
1078
+ {
1079
+ "op": "set_telephony_service",
1080
+ "device": device.strip(),
1081
+ "source_address": source_address,
1082
+ "port": int(port),
1083
+ "max_ephones": int(max_ephones) if max_ephones else None,
1084
+ "max_dn": int(max_dn) if max_dn else None,
1085
+ }
1086
+ )
1087
+ for device, dn_id, number in ephone_dn_pattern.findall(segment):
1088
+ ops.append({"op": "set_ephone_dn", "device": device.strip(), "dn_id": int(dn_id), "number": number})
1089
+ for device, ephone_id, mac, button in ephone_pattern.findall(segment):
1090
+ ops.append({"op": "set_ephone", "device": device.strip(), "ephone_id": int(ephone_id), "mac": mac.upper(), "button": button})
1091
+ for device, peer_id, destination_pattern, session_target in dial_peer_pattern.findall(segment):
1092
+ ops.append(
1093
+ {
1094
+ "op": "set_dial_peer_voice",
1095
+ "device": device.strip(),
1096
+ "peer_id": int(peer_id),
1097
+ "destination_pattern": destination_pattern,
1098
+ "session_target": session_target,
1099
+ }
1100
+ )
573
1101
  return ops
574
1102
 
575
1103
 
@@ -630,14 +1158,31 @@ def _extract_management_ops(prompt: str) -> list[dict[str, object]]:
630
1158
  def _extract_wireless_ops(prompt: str) -> list[dict[str, object]]:
631
1159
  ops: list[dict[str, object]] = []
632
1160
  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+))?",
1161
+ r"set\s+([A-Za-z0-9_ -]+?)\s+ssid\s+([A-Za-z0-9._-]+)"
1162
+ r"(?:\s+security\s+(wpa2?\s+enterprise|wpa2?-enterprise|wpa2?-psk|802\.1x|wep|open|wpa2?|wpa))?"
1163
+ r"(?:\s+passphrase\s+([A-Za-z0-9._-]+))?"
1164
+ r"(?:\s+radius\s+(\d+\.\d+\.\d+\.\d+)\s+secret\s+([A-Za-z0-9._-]+))?"
1165
+ r"(?:\s+channel\s+(\d+))?",
634
1166
  flags=re.IGNORECASE,
635
1167
  )
636
1168
  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()
1169
+ for device, ssid, security, passphrase, radius_server, radius_secret, channel in ssid_pattern.findall(segment):
1170
+ security_key = (security or "open").lower().replace(" ", "-")
639
1171
  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})
1172
+ operation = {
1173
+ "op": "set_wireless_ssid",
1174
+ "device": device.strip(),
1175
+ "ssid": ssid,
1176
+ "security": security_key,
1177
+ "auth_type": auth_type,
1178
+ "encrypt_type": encrypt_type,
1179
+ "passphrase": passphrase or "",
1180
+ "channel": int(channel) if channel else 1,
1181
+ }
1182
+ if radius_server:
1183
+ operation["radius_server"] = radius_server
1184
+ operation["radius_secret"] = radius_secret or ""
1185
+ ops.append(operation)
641
1186
  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
1187
  for segment in _command_segments(prompt):
643
1188
  for client, ap, ssid, ip_mode in assoc_pattern.findall(segment):
@@ -724,6 +1269,36 @@ def _extract_iot_ops(prompt: str) -> list[dict[str, object]]:
724
1269
  return ops
725
1270
 
726
1271
 
1272
+ def _decode_prompt_string(value: str) -> str:
1273
+ return (
1274
+ value.replace(r"\\", "\0")
1275
+ .replace(r"\"", '"')
1276
+ .replace(r"\n", "\n")
1277
+ .replace(r"\r", "\r")
1278
+ .replace(r"\t", "\t")
1279
+ .replace("\0", "\\")
1280
+ )
1281
+
1282
+
1283
+ def _extract_programming_ops(prompt: str) -> list[dict[str, object]]:
1284
+ ops: list[dict[str, object]] = []
1285
+ script_pattern = re.compile(
1286
+ r'set\s+"((?:\\.|[^"\\])*)"\s+script\s+app\s+"((?:\\.|[^"\\])*)"\s+file\s+"((?:\\.|[^"\\])*)"\s+content\s+"((?:\\.|[^"\\])*)"',
1287
+ flags=re.IGNORECASE | re.DOTALL,
1288
+ )
1289
+ for device, app_name, file_name, content in script_pattern.findall(prompt):
1290
+ ops.append(
1291
+ {
1292
+ "op": "set_script_file_content",
1293
+ "device": _decode_prompt_string(device).strip(),
1294
+ "app_name": _decode_prompt_string(app_name).strip(),
1295
+ "file_name": _decode_prompt_string(file_name).strip(),
1296
+ "content": _decode_prompt_string(content),
1297
+ }
1298
+ )
1299
+ return ops
1300
+
1301
+
727
1302
  def parse_intent(prompt: str) -> IntentPlan:
728
1303
  pkt_path_match = re.search(r"([A-Za-z]:\\[^\"\n]+?\.pkt)\b", prompt, flags=re.IGNORECASE)
729
1304
  pkt_path = pkt_path_match.group(1) if pkt_path_match else None
@@ -739,6 +1314,64 @@ def parse_intent(prompt: str) -> IntentPlan:
739
1314
  host_link_intent = _extract_host_link_intent(normalized_prompt)
740
1315
  host_vlan_assignment = _extract_host_vlan_assignment(normalized_prompt)
741
1316
  network_style = _extract_network_style(normalized_prompt)
1317
+ if (
1318
+ network_style == "wireless_advanced"
1319
+ and "server_aaa" in capability_set
1320
+ and re.search(r"\bradius\b", lowered)
1321
+ and not re.search(r"\b(?:aaa|acs|server aaa|aaa server)\b", lowered)
1322
+ ):
1323
+ capability_set.discard("server_aaa")
1324
+ if re.search(r"\bqos\b", lowered) and (
1325
+ re.search(r"\b(?:snmp|netflow|span|rspan|lldp|rep|dai|dot1x|802\.1x|dhcp snooping|port-security|port security)\b", lowered)
1326
+ or network_style == "l2_security_monitoring"
1327
+ ):
1328
+ capability_set.add("qos")
1329
+ if network_style == "automation_controller" or re.search(r"\b(?:network controller|netcon|blockly|iox)\b", lowered):
1330
+ if re.search(r"\bpython\b", lowered):
1331
+ capability_set.add("python_programming")
1332
+ if re.search(r"\b(?:tcp|udp)\b", lowered) and re.search(r"\b(?:app|application|test app)\b", lowered):
1333
+ capability_set.add("tcp_udp_app")
1334
+ if "ospf" in capability_set and not re.search(r"\b(?:ipv6|ospfv3)\b", lowered):
1335
+ capability_set.add("ospfv2")
1336
+ if "eigrp" in capability_set and not re.search(r"\bipv6\b", lowered):
1337
+ capability_set.add("eigrp_ipv4")
1338
+ if "rip" in capability_set and not re.search(r"\b(?:ipv6|ripng)\b", lowered):
1339
+ capability_set.add("ripv2")
1340
+ if "nat" in capability_set:
1341
+ if re.search(r"\bpat\b|\boverload\b", lowered):
1342
+ capability_set.add("pat")
1343
+ if re.search(r"\bstatic\s+nat\b|\bnat\s+static\b", lowered):
1344
+ capability_set.add("nat_static")
1345
+ if re.search(r"\bdynamic\s+nat\b|\bnat\s+dynamic\b", lowered):
1346
+ capability_set.add("nat_dynamic")
1347
+ if network_style == "ipv4_routing_management" and not {"nat_static", "nat_dynamic"} & capability_set:
1348
+ capability_set.update({"nat_static", "nat_dynamic"})
1349
+ if "ssh" in capability_set:
1350
+ capability_set.add("ssh_ios")
1351
+ if "ntp" in capability_set:
1352
+ capability_set.add("ntp_ios")
1353
+ if (
1354
+ "server_syslog" in capability_set
1355
+ and re.search(r"\b(?:server|ftp|email|aaa|dns)\b", lowered)
1356
+ and not re.search(r"\b(?:logging\s+host|set\s+\S+\s+syslog\s+server)\b", lowered)
1357
+ ):
1358
+ capability_set.discard("syslog_ios")
1359
+ if re.search(r"\blogging\s+host\b", lowered):
1360
+ capability_set.add("syslog_ios")
1361
+ if re.search(r"\bdefault\s+route\b|\b0\.0\.0\.0/0\b", lowered):
1362
+ capability_set.update({"static_route", "default_route"})
1363
+ elif re.search(r"\bstatic\s+route\b|\bip\s+route\b", lowered):
1364
+ capability_set.add("static_route")
1365
+ if re.search(r"\bdhcp\s+relay\b|\bdhcp-relay\b|\bhelper-address\b|\bip\s+helper\b", lowered):
1366
+ capability_set.add("dhcp_relay")
1367
+ if not re.search(r"\b(?:dhcp\s+pool|server\s+dhcp|dhcp\s+server|default-router)\b", lowered):
1368
+ capability_set.discard("dhcp_pool")
1369
+ capability_set.discard("router_dhcp")
1370
+ capability_set.discard("server_dhcp")
1371
+ 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):
1372
+ capability_set.discard("dhcp_pool")
1373
+ capability_set.discard("router_dhcp")
1374
+ capability_set.discard("server_dhcp")
742
1375
  department_count = _extract_department_count(normalized_prompt)
743
1376
  per_department_devices = _extract_per_department_devices(normalized_prompt)
744
1377
 
@@ -788,17 +1421,173 @@ def parse_intent(prompt: str) -> IntentPlan:
788
1421
  management_ops = _extract_management_ops(prompt)
789
1422
  verification_ops = _extract_verification_ops(prompt)
790
1423
  iot_ops = _extract_iot_ops(prompt)
1424
+ programming_ops = _extract_programming_ops(prompt)
791
1425
 
792
1426
  if any(op["op"] == "associate_wireless_client" for op in wireless_ops):
793
1427
  capability_set.update({"wireless_client", "wireless_client_association"})
794
1428
  if any(op["op"] == "set_wireless_ssid" for op in wireless_ops):
795
1429
  capability_set.update({"wireless_ap", "wireless_mutation"})
1430
+ for op in wireless_ops:
1431
+ if op.get("op") != "set_wireless_ssid":
1432
+ continue
1433
+ security = str(op.get("security") or "")
1434
+ if security == "wep":
1435
+ capability_set.add("wep")
1436
+ if security in {"wpa-enterprise", "wpa2-enterprise", "802.1x"} or op.get("radius_server"):
1437
+ capability_set.add("wpa_enterprise")
1438
+ elif network_style == "wireless_advanced":
1439
+ capability_set.discard("wireless_ap")
1440
+ capability_set.discard("wireless_mutation")
796
1441
  if end_device_ops:
797
1442
  capability_set.add("end_device_mutation")
798
1443
  if iot_ops:
799
1444
  capability_set.update({"iot", "iot_registration"})
800
- if capability_set & {"vpn", "ipsec", "gre", "ppp"}:
1445
+ if any(op.get("op") == "set_iot_rule_state" for op in iot_ops):
1446
+ capability_set.add("iot_control")
1447
+ for operation in programming_ops:
1448
+ file_name = str(operation.get("file_name") or "").lower()
1449
+ app_name = str(operation.get("app_name") or "").lower()
1450
+ content = str(operation.get("content") or "").lower()
1451
+ if file_name.endswith(".py") or "python" in app_name:
1452
+ capability_set.add("python_programming")
1453
+ if file_name.endswith(".js") or "javascript" in app_name:
1454
+ capability_set.add("javascript_programming")
1455
+ if file_name.endswith(".visual") or "visual" in app_name or "<xml" in content:
1456
+ capability_set.add("blockly_programming")
1457
+ if "tcp" in " ".join([file_name, app_name, content]) or "udp" in " ".join([file_name, app_name, content]):
1458
+ capability_set.add("tcp_udp_app")
1459
+ if "realhttp" in content or "real http" in app_name:
1460
+ capability_set.add("real_http")
1461
+ if "realws" in content or "websocket" in content or "websocket" in app_name:
1462
+ capability_set.add("real_websocket")
1463
+ if "mqtt" in content or "mqtt" in app_name:
1464
+ capability_set.add("mqtt")
1465
+ if programming_ops and capability_set & {"python_programming", "javascript_programming", "blockly_programming"} and not capability_set & {"real_http", "real_websocket", "mqtt"}:
1466
+ network_style = network_style or "automation_controller"
1467
+ switch_op_names = {str(op.get("op")) for op in switch_ops}
1468
+ if "set_dhcp_snooping" in switch_op_names:
1469
+ capability_set.add("dhcp_snooping")
1470
+ if "set_dai" in switch_op_names:
1471
+ capability_set.add("dai")
1472
+ if "set_lldp" in switch_op_names:
1473
+ capability_set.add("lldp")
1474
+ if "set_rep" in switch_op_names:
1475
+ capability_set.add("rep")
1476
+ if "set_span" in switch_op_names:
1477
+ capability_set.add("span")
1478
+ if "set_port_security" in switch_op_names:
1479
+ capability_set.add("port_security")
1480
+ if "set_dot1x" in switch_op_names:
1481
+ capability_set.add("dot1x")
1482
+ if "set_qos_policy" in switch_op_names:
1483
+ capability_set.add("qos")
1484
+ if "set_stp" in switch_op_names:
1485
+ capability_set.add("stp")
1486
+ for op in switch_ops:
1487
+ if op.get("op") == "set_stp" and re.search(r"(?:rapid|rstp)", str(op.get("mode") or ""), flags=re.IGNORECASE):
1488
+ capability_set.add("rstp")
1489
+ if "set_etherchannel" in switch_op_names:
1490
+ capability_set.add("etherchannel")
1491
+ for op in switch_ops:
1492
+ if op.get("op") != "set_etherchannel":
1493
+ continue
1494
+ mode = str(op.get("mode") or "").lower()
1495
+ if mode in {"active", "passive"}:
1496
+ capability_set.add("lacp")
1497
+ if mode in {"desirable", "auto"}:
1498
+ capability_set.add("pagp")
1499
+ if "set_vtp" in switch_op_names:
1500
+ capability_set.add("vtp")
1501
+ if "set_dtp" in switch_op_names:
1502
+ capability_set.add("dtp")
1503
+ router_op_names = {str(op.get("op")) for op in router_ops}
1504
+ if "set_bgp_neighbor" in router_op_names:
1505
+ capability_set.add("bgp")
1506
+ if "set_snmp_community" in router_op_names:
1507
+ capability_set.add("snmp")
1508
+ if "set_netflow" in router_op_names:
1509
+ capability_set.add("netflow")
1510
+ if "set_gre_tunnel" in router_op_names:
1511
+ capability_set.add("gre")
1512
+ if "set_ppp_interface" in router_op_names:
1513
+ capability_set.add("ppp")
1514
+ if router_op_names & {"set_ipsec_transform_set", "set_crypto_map"}:
1515
+ capability_set.update({"ipsec", "vpn"})
1516
+ if "set_cbac_inspect" in router_op_names:
1517
+ capability_set.update({"cbac", "security_edge"})
1518
+ if router_op_names & {"set_zfw_zone_interface", "set_zfw_zone_pair", "set_zfw_policy"}:
1519
+ capability_set.update({"zfw", "security_edge"})
1520
+ if "set_qos_policy" not in switch_op_names:
1521
+ capability_set.discard("qos")
1522
+ capability_set.discard("server_http")
1523
+ capability_set.discard("server_https")
1524
+ if router_op_names & {"enable_ipv6_unicast_routing", "set_ipv6_address", "set_ipv6_slaac"}:
1525
+ capability_set.add("ipv6_slaac")
1526
+ if "set_dhcpv6_pool" in router_op_names:
1527
+ capability_set.add("dhcpv6_stateful")
1528
+ if "set_ospfv3_interface" in router_op_names:
1529
+ capability_set.add("ospfv3")
1530
+ if "set_eigrp_ipv6_interface" in router_op_names:
1531
+ capability_set.add("eigrp_ipv6")
1532
+ if "set_ripng_interface" in router_op_names:
1533
+ capability_set.add("ripng")
1534
+ if "set_hsrp_ipv6" in router_op_names:
1535
+ capability_set.add("hsrp")
1536
+ if "set_ospfv2_network" in router_op_names:
1537
+ capability_set.add("ospfv2")
1538
+ if "set_eigrp_ipv4_network" in router_op_names:
1539
+ capability_set.add("eigrp_ipv4")
1540
+ if "set_ripv2_network" in router_op_names:
1541
+ capability_set.add("ripv2")
1542
+ if "set_static_route" in router_op_names:
1543
+ capability_set.add("static_route")
1544
+ if any(op.get("op") == "set_static_route" and op.get("network") == "0.0.0.0" and op.get("prefix") == 0 for op in router_ops):
1545
+ capability_set.add("default_route")
1546
+ if "set_dhcp_relay" in router_op_names:
1547
+ capability_set.add("dhcp_relay")
1548
+ if "set_nat_interface" in router_op_names:
1549
+ capability_set.add("nat")
1550
+ if "set_nat_static" in router_op_names:
1551
+ capability_set.update({"nat", "nat_static"})
1552
+ if "set_pat_overload" in router_op_names:
1553
+ capability_set.update({"nat", "pat"})
1554
+ if "set_ssh_ios" in router_op_names:
1555
+ capability_set.update({"ssh", "ssh_ios"})
1556
+ if "set_ntp_server" in router_op_names:
1557
+ capability_set.update({"ntp", "ntp_ios"})
1558
+ if "set_syslog_server" in router_op_names:
1559
+ capability_set.update({"syslog_ios"})
1560
+ if router_op_names & {"set_telephony_service", "set_ephone_dn", "set_ephone", "set_dial_peer_voice"}:
1561
+ capability_set.update({"voip", "call_manager"})
1562
+ if router_op_names & {"set_ephone_dn", "set_ephone"}:
1563
+ capability_set.add("ip_phone")
1564
+ if capability_set & {"vpn", "ipsec", "gre", "ppp", "security_edge"}:
801
1565
  network_style = network_style or "wan_security"
1566
+ if capability_set & {"ipv6_slaac", "dhcpv6_stateful", "dhcpv6_stateless", "ospfv3", "eigrp_ipv6", "ripng", "hsrp"}:
1567
+ network_style = network_style or "ipv6_routing"
1568
+ if capability_set & {"ospfv2", "eigrp_ipv4", "ripv2", "static_route", "default_route", "dhcp_relay", "nat_static", "nat_dynamic", "pat", "ssh_ios", "ntp_ios", "syslog_ios"}:
1569
+ network_style = network_style or "ipv4_routing_management"
1570
+ if capability_set & {"dhcp_snooping", "dai", "dot1x", "lldp", "rep", "snmp", "netflow", "span", "qos", "port_security"}:
1571
+ network_style = network_style or "l2_security_monitoring"
1572
+ if capability_set & {"bgp", "stp", "rstp", "etherchannel", "lacp", "pagp", "vtp", "dtp"}:
1573
+ network_style = "l2_resiliency_routing"
1574
+ if capability_set & {"wlc", "wpa_enterprise", "wep", "guest_wifi", "beamforming", "meraki", "cellular_5g", "bluetooth"}:
1575
+ network_style = "wireless_advanced"
1576
+ if capability_set & {"coaxial", "cable_dsl", "central_office", "cell_tower", "power_distribution", "hot_swappable", "ios_license"}:
1577
+ network_style = "physical_media_device"
1578
+ if capability_set & {"mqtt", "real_http", "real_websocket", "visual_scripting", "ptp", "profinet", "l2nat", "cyberobserver", "industrial_firewall"}:
1579
+ network_style = "industrial_iot"
1580
+ if capability_set & {"real_http", "real_websocket"}:
1581
+ capability_set.discard("server_http")
1582
+ capability_set.discard("server_https")
1583
+ if not (
1584
+ iot_ops
1585
+ or re.search(r"\b(?:home\s+gateway|smart\s+home|iot\s+registration|iot\s+control|iot\s+rule)\b", lowered)
1586
+ or re.search(r"\bregister\s+.+\s+to\b", lowered)
1587
+ ):
1588
+ capability_set.discard("iot")
1589
+ if capability_set & {"voip", "ip_phone", "call_manager", "linksys_voice"}:
1590
+ network_style = "voice_collaboration"
802
1591
 
803
1592
  capabilities = sorted(capability_set)
804
1593
 
@@ -864,4 +1653,5 @@ def parse_intent(prompt: str) -> IntentPlan:
864
1653
  management_ops=management_ops,
865
1654
  verification_ops=verification_ops,
866
1655
  iot_ops=iot_ops,
1656
+ programming_ops=programming_ops,
867
1657
  )