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.
- package/CHANGELOG.md +42 -4
- package/README.md +237 -88
- package/docs/campus-donor-proof.md +103 -0
- package/docs/curated-donor-registry.md +26 -0
- package/docs/github-launch-ops-0.2.1.md +45 -0
- package/docs/github-launch-ops-0.2.2.md +42 -0
- package/docs/github-metadata.md +23 -6
- package/docs/hero-demo-plan.md +18 -10
- package/docs/home-iot-donor-proof.md +64 -0
- package/docs/launch-announcement-0.2.1.md +15 -0
- package/docs/launch-announcement-0.2.2.md +15 -0
- package/docs/packet-tracer-feature-gap-atlas.md +87 -0
- package/docs/post-launch-follow-up.md +39 -0
- package/docs/publish-preview-roadmap.md +20 -17
- package/docs/release-checklist.md +22 -12
- package/docs/{release-notes-0.2.0.md → release-notes-0.2.1.md} +7 -6
- package/docs/release-notes-0.2.2.md +26 -0
- package/docs/runtime-truth.md +31 -0
- package/docs/wan-security-donor-proof.md +41 -0
- package/docs/wireless-advanced-proof.md +60 -0
- package/examples/README.md +13 -10
- package/examples/gallery.md +10 -4
- package/examples/index.json +3 -3
- package/package.json +44 -16
- package/references/curated-donor-registry.json +5 -2
- package/references/packettracer-feature-atlas.json +133 -0
- package/references/scenario-fixture-corpus.json +1 -0
- package/scripts/build_examples_index.py +27 -16
- package/scripts/coverage_matrix.py +559 -23
- package/scripts/feature_atlas.py +229 -0
- package/scripts/generate_pkt.py +330 -31
- package/scripts/intent_parser.py +278 -7
- package/scripts/pkt_editor.py +174 -0
- package/scripts/runtime_doctor.py +83 -10
- package/scripts/sample_catalog.py +210 -5
- package/docs/screenshots/.gitkeep +0 -1
- package/docs/screenshots/packet-tracer-topology-cropped.png +0 -0
- package/scripts/__pycache__/build_examples_index.cpython-314.pyc +0 -0
- package/scripts/__pycache__/build_sample_catalog.cpython-314.pyc +0 -0
- package/scripts/__pycache__/coverage_matrix.cpython-314.pyc +0 -0
- package/scripts/__pycache__/donor_diagnostics.cpython-314.pyc +0 -0
- package/scripts/__pycache__/generate_pkt.cpython-314.pyc +0 -0
- package/scripts/__pycache__/install_skill.cpython-314.pyc +0 -0
- package/scripts/__pycache__/intent_parser.cpython-314.pyc +0 -0
- package/scripts/__pycache__/packet_tracer_env.cpython-314.pyc +0 -0
- package/scripts/__pycache__/pkt_builder.cpython-314.pyc +0 -0
- package/scripts/__pycache__/pkt_codec.cpython-314.pyc +0 -0
- package/scripts/__pycache__/pkt_editor.cpython-314.pyc +0 -0
- package/scripts/__pycache__/pkt_transformer.cpython-314.pyc +0 -0
- package/scripts/__pycache__/remote_search.cpython-314.pyc +0 -0
- package/scripts/__pycache__/runtime_doctor.cpython-314.pyc +0 -0
- package/scripts/__pycache__/sample_catalog.cpython-314.pyc +0 -0
- package/scripts/__pycache__/sample_selector.cpython-314.pyc +0 -0
- package/scripts/__pycache__/twofish_diagnostics.cpython-314.pyc +0 -0
- package/scripts/__pycache__/twofish_runtime.cpython-314.pyc +0 -0
- package/scripts/__pycache__/workspace_repair.cpython-314.pyc +0 -0
- package/scripts/vendor/__pycache__/twofish.cpython-314.pyc +0 -0
|
@@ -43,23 +43,62 @@ def runtime_env_examples(host_os: str) -> list[str]:
|
|
|
43
43
|
return [
|
|
44
44
|
r"$env:PACKET_TRACER_ROOT='C:\Program Files\Cisco Packet Tracer 9.0.0'",
|
|
45
45
|
r"$env:PACKET_TRACER_COMPAT_DONOR='C:\path\to\your-working-9.0-donor.pkt'",
|
|
46
|
-
r'$env:PKT_TWOFISH_LIBRARY="
|
|
46
|
+
r'$env:PKT_TWOFISH_LIBRARY="C:\path\to\_twofish.cp314-win_amd64.pyd"',
|
|
47
|
+
r'$env:PKT_TWOFISH_SEARCH_ROOTS="C:\path\to\bridge-folder"',
|
|
47
48
|
]
|
|
48
49
|
if host_os == "macOS":
|
|
49
50
|
return [
|
|
50
51
|
"export PACKET_TRACER_ROOT='/Applications/Cisco Packet Tracer.app/Contents/Resources'",
|
|
51
52
|
"export PACKET_TRACER_COMPAT_DONOR=\"$HOME/path/to/your-working-9.0-donor.pkt\"",
|
|
52
|
-
"export PKT_TWOFISH_SEARCH_ROOTS=\"$HOME
|
|
53
|
+
"export PKT_TWOFISH_SEARCH_ROOTS=\"$HOME/path/to/bridge-folder:$HOME/pkt-bridges\"",
|
|
53
54
|
]
|
|
54
55
|
if host_os == "Linux":
|
|
55
56
|
return [
|
|
56
57
|
"export PACKET_TRACER_ROOT='/opt/pt/bin'",
|
|
57
58
|
"export PACKET_TRACER_COMPAT_DONOR=\"$HOME/path/to/your-working-9.0-donor.pkt\"",
|
|
58
|
-
"export PKT_TWOFISH_SEARCH_ROOTS=\"$HOME
|
|
59
|
+
"export PKT_TWOFISH_SEARCH_ROOTS=\"$HOME/path/to/bridge-folder:$HOME/pkt-bridges\"",
|
|
59
60
|
]
|
|
60
61
|
return []
|
|
61
62
|
|
|
62
63
|
|
|
64
|
+
def _format_operation_list(operations: list[str]) -> str:
|
|
65
|
+
return ", ".join(operations) if operations else "none"
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _best_next_fix(runtime_blockers: list[str], recommended_next_steps: list[str]) -> str:
|
|
69
|
+
blocker_map = {
|
|
70
|
+
"missing_or_incompatible_donor": "Fix the donor path first so inventory/edit/generate can use a compatible 9.0 donor.",
|
|
71
|
+
"missing_twofish_bridge": "Fix the Twofish bridge next so strict decode/edit/generate can run locally.",
|
|
72
|
+
"missing_packet_tracer_root": "Fix PACKET_TRACER_ROOT so the doctor can resolve the install layout deterministically.",
|
|
73
|
+
"missing_packet_tracer_executable": "Fix the Packet Tracer install root or executable path before relying on validate_open.",
|
|
74
|
+
"windows_first_runtime": "Do not assume non-Windows strict runtime support without a custom native bridge and explicit Packet Tracer paths.",
|
|
75
|
+
"using_external_bridge_only": "Move the external bridge into the repo-local vendor path if you need a self-contained runtime claim.",
|
|
76
|
+
}
|
|
77
|
+
for blocker in runtime_blockers:
|
|
78
|
+
if blocker in blocker_map:
|
|
79
|
+
return blocker_map[blocker]
|
|
80
|
+
return recommended_next_steps[0] if recommended_next_steps else "No immediate fix is required."
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _why_it_is_blocked(runtime_blockers: list[str], bridge_resolution: str) -> str:
|
|
84
|
+
if not runtime_blockers:
|
|
85
|
+
return "No runtime blocker is currently active."
|
|
86
|
+
reasons: list[str] = []
|
|
87
|
+
if "missing_or_incompatible_donor" in runtime_blockers:
|
|
88
|
+
reasons.append("no compatible donor is resolved")
|
|
89
|
+
if "missing_twofish_bridge" in runtime_blockers:
|
|
90
|
+
reasons.append("no local Twofish bridge is resolved")
|
|
91
|
+
if "missing_packet_tracer_root" in runtime_blockers:
|
|
92
|
+
reasons.append("Packet Tracer root is not resolved")
|
|
93
|
+
if "missing_packet_tracer_executable" in runtime_blockers:
|
|
94
|
+
reasons.append("Packet Tracer executable is not resolved")
|
|
95
|
+
if "windows_first_runtime" in runtime_blockers:
|
|
96
|
+
reasons.append("strict bundled validation is still Windows-first")
|
|
97
|
+
if bridge_resolution == "external_env":
|
|
98
|
+
reasons.append("strict runtime currently relies on an external bridge override")
|
|
99
|
+
return "; ".join(reasons) + "."
|
|
100
|
+
|
|
101
|
+
|
|
63
102
|
def build_recommended_next_steps(
|
|
64
103
|
*,
|
|
65
104
|
host_os: str,
|
|
@@ -205,13 +244,43 @@ def collect_runtime_doctor() -> dict[str, object]:
|
|
|
205
244
|
else:
|
|
206
245
|
runtime_grade = "ready"
|
|
207
246
|
if runtime_grade == "ready":
|
|
208
|
-
doctor_summary =
|
|
247
|
+
doctor_summary = (
|
|
248
|
+
"Runtime is ready. "
|
|
249
|
+
f"Ready operations: {_format_operation_list(ready_operations)}."
|
|
250
|
+
)
|
|
251
|
+
elif bridge_resolution == "external_env" and not blocked_operations:
|
|
252
|
+
doctor_summary = (
|
|
253
|
+
"Runtime operations are ready, but packaged runtime remains partially ready because the bridge "
|
|
254
|
+
"comes from an external environment instead of the repo-local vendor path. "
|
|
255
|
+
f"Ready operations: {_format_operation_list(ready_operations)}."
|
|
256
|
+
)
|
|
209
257
|
elif runtime_grade == "partially_ready":
|
|
210
|
-
doctor_summary =
|
|
258
|
+
doctor_summary = (
|
|
259
|
+
"Runtime is partially ready. "
|
|
260
|
+
f"Ready operations: {_format_operation_list(ready_operations)}. "
|
|
261
|
+
f"Blocked operations: {_format_operation_list(blocked_operations)}."
|
|
262
|
+
)
|
|
211
263
|
else:
|
|
212
|
-
doctor_summary =
|
|
213
|
-
|
|
264
|
+
doctor_summary = (
|
|
265
|
+
"Runtime is blocked. "
|
|
266
|
+
f"Blocked operations: {_format_operation_list(blocked_operations)}."
|
|
267
|
+
)
|
|
268
|
+
if bridge_resolution == "external_env" and blocked_operations:
|
|
214
269
|
doctor_summary = f"{doctor_summary} External bridge is being used instead of a repo-local vendor bridge."
|
|
270
|
+
elif bridge_resolution == "missing" and runtime_grade != "ready":
|
|
271
|
+
doctor_summary = f"{doctor_summary} Strict decode/edit/generate remain blocked until a local bridge is resolved."
|
|
272
|
+
what_currently_works = (
|
|
273
|
+
f"Currently working operations: {_format_operation_list(ready_operations)}."
|
|
274
|
+
if ready_operations
|
|
275
|
+
else "No strict runtime operations are currently working."
|
|
276
|
+
)
|
|
277
|
+
what_is_blocked = (
|
|
278
|
+
f"Blocked operations: {_format_operation_list(blocked_operations)}."
|
|
279
|
+
if blocked_operations
|
|
280
|
+
else "No runtime operations are currently blocked."
|
|
281
|
+
)
|
|
282
|
+
why_it_is_blocked = _why_it_is_blocked(runtime_blockers, bridge_resolution)
|
|
283
|
+
best_next_fix = _best_next_fix(runtime_blockers, recommended_next_steps)
|
|
215
284
|
bridge_recommendation = (
|
|
216
285
|
"Use or install a repo-local vendor bridge for fully self-contained runtime readiness."
|
|
217
286
|
if bridge_resolution == "external_env"
|
|
@@ -220,11 +289,11 @@ def collect_runtime_doctor() -> dict[str, object]:
|
|
|
220
289
|
else "Repo-local bridge is resolved."
|
|
221
290
|
)
|
|
222
291
|
runtime_contract_notes = (
|
|
223
|
-
"Repo-local bridge and donor are present."
|
|
292
|
+
"Repo-local bridge and donor are present, so this checkout can run strict decode/edit/generate locally."
|
|
224
293
|
if bridge_resolution == "repo_local"
|
|
225
|
-
else "
|
|
294
|
+
else "Strict decode/edit/generate currently rely on an external bridge path. Repo-local runtime packaging is still incomplete."
|
|
226
295
|
if bridge_resolution == "external_env"
|
|
227
|
-
else "
|
|
296
|
+
else "No bridge is resolved. validate_open may still work when Packet Tracer is installed, but strict decode/edit/generate remain blocked."
|
|
228
297
|
)
|
|
229
298
|
|
|
230
299
|
return {
|
|
@@ -270,6 +339,10 @@ def collect_runtime_doctor() -> dict[str, object]:
|
|
|
270
339
|
"runtime_blockers": runtime_blockers,
|
|
271
340
|
"ready_operations": ready_operations,
|
|
272
341
|
"blocked_operations": blocked_operations,
|
|
342
|
+
"what_currently_works": what_currently_works,
|
|
343
|
+
"what_is_blocked": what_is_blocked,
|
|
344
|
+
"why_it_is_blocked": why_it_is_blocked,
|
|
345
|
+
"best_next_fix": best_next_fix,
|
|
273
346
|
"doctor_summary": doctor_summary,
|
|
274
347
|
"runtime_grade": runtime_grade,
|
|
275
348
|
"recommended_next_steps": recommended_next_steps,
|
|
@@ -24,20 +24,82 @@ CAPABILITY_KEYWORDS = {
|
|
|
24
24
|
"dhcp_pool": ["dhcp", "reservation", "apipa"],
|
|
25
25
|
"router_dhcp": ["dhcp", "reservation", "apipa"],
|
|
26
26
|
"server_dhcp": ["dhcp", "reservation", "apipa"],
|
|
27
|
+
"dhcpv6_stateful": ["dhcpv6", "stateful_dhcpv6"],
|
|
28
|
+
"dhcpv6_stateless": ["stateless_dhcpv6"],
|
|
29
|
+
"ipv6_slaac": ["slaac"],
|
|
30
|
+
"ipv6_prefix_delegation": ["prefix_delegation"],
|
|
31
|
+
"ipv6_dns_aaaa": ["aaaa"],
|
|
32
|
+
"ipv6_tunneling": ["ipv6ip", "ipv6 tunneling"],
|
|
33
|
+
"isatap": ["isatap"],
|
|
27
34
|
"dhcp_snooping": ["dhcp snooping", "option_82", "trusted_untrusted"],
|
|
28
35
|
"ospf": ["ospf"],
|
|
36
|
+
"ospfv3": ["ospfv3", "ipv6_ospf"],
|
|
29
37
|
"eigrp": ["eigrp"],
|
|
38
|
+
"eigrp_ipv6": ["ipv6_eigrp"],
|
|
30
39
|
"rip": ["rip"],
|
|
40
|
+
"ripng": ["ripng", "ipv6 rip"],
|
|
41
|
+
"hsrp": ["hsrp"],
|
|
31
42
|
"nat": ["nat"],
|
|
32
43
|
"acl": ["acl", "access-list"],
|
|
44
|
+
"dai": ["dai", "dynamic arp inspection"],
|
|
45
|
+
"dot1x": ["dot1x", "802.1x", "port-based nac"],
|
|
46
|
+
"lldp": ["lldp"],
|
|
47
|
+
"rep": ["rep_"],
|
|
48
|
+
"snmp": ["snmp"],
|
|
49
|
+
"netflow": ["netflow"],
|
|
50
|
+
"span": ["span", "rspan"],
|
|
51
|
+
"qos": ["qos"],
|
|
52
|
+
"port_security": ["port security", "port-security"],
|
|
33
53
|
"vpn": ["vpn", "ipsec", "gre"],
|
|
34
54
|
"ipsec": ["ipsec", "ike"],
|
|
35
55
|
"gre": ["gre"],
|
|
36
56
|
"ppp": ["ppp", "chap", "pap"],
|
|
57
|
+
"wan": ["wan", "cloud", "serial", "pppoe", "ppp"],
|
|
58
|
+
"security_edge": ["asa", "firewall", "security appliance"],
|
|
59
|
+
"asa_acl_nat": ["asa_acl_nat"],
|
|
60
|
+
"asa_service_policy": ["asa_service_policy", "service policy"],
|
|
61
|
+
"clientless_vpn": ["clientless_vpn"],
|
|
62
|
+
"cbac": ["cbac"],
|
|
63
|
+
"zfw": ["zfw", "zone based firewall"],
|
|
64
|
+
"sniffer": ["sniffer"],
|
|
37
65
|
"multilayer_switching": ["multilayer", "layer 3", "3560", "svi"],
|
|
38
66
|
"wireless": ["wireless", "wlan", "wlc", "wifi", "ssid", "wpa", "wep", "5g", "bluetooth", "cellular"],
|
|
39
67
|
"wireless_ap": ["wireless", "wlan", "wlc", "wifi", "ssid", "wpa", "wep"],
|
|
40
68
|
"wireless_client": ["wireless", "wifi", "tablet", "smartphone", "laptop"],
|
|
69
|
+
"wlc": ["wlc", "wireless lan controller"],
|
|
70
|
+
"wpa_enterprise": ["wpa_radius", "wpa2_enterprise"],
|
|
71
|
+
"wep": ["wep"],
|
|
72
|
+
"guest_wifi": ["guest"],
|
|
73
|
+
"beamforming": ["beamforming"],
|
|
74
|
+
"meraki": ["meraki"],
|
|
75
|
+
"cellular_5g": ["5g", "cellular", "cell tower"],
|
|
76
|
+
"bluetooth": ["bluetooth"],
|
|
77
|
+
"network_controller": ["network controller", "netcon"],
|
|
78
|
+
"python_programming": ["python"],
|
|
79
|
+
"javascript_programming": ["javascript"],
|
|
80
|
+
"blockly_programming": ["blockly"],
|
|
81
|
+
"tcp_udp_app": ["tcp_test_app", "udp_test_app"],
|
|
82
|
+
"vm_iox": ["iox", "uploading_and_running_vm"],
|
|
83
|
+
"voip": ["voip"],
|
|
84
|
+
"ip_phone": ["ip phone", "phone"],
|
|
85
|
+
"call_manager": ["call manager", "telephony", "voip"],
|
|
86
|
+
"linksys_voice": ["voip linksys"],
|
|
87
|
+
"mqtt": ["mqtt"],
|
|
88
|
+
"real_http": ["real-http", "real http"],
|
|
89
|
+
"real_websocket": ["real-websocket", "real websocket"],
|
|
90
|
+
"visual_scripting": ["visual-scripting", "visual scripting"],
|
|
91
|
+
"ptp": ["ptp"],
|
|
92
|
+
"profinet": ["profinet"],
|
|
93
|
+
"l2nat": ["l2nat"],
|
|
94
|
+
"cyberobserver": ["cyberobserver"],
|
|
95
|
+
"industrial_firewall": ["isa3000", "industrial cybersecurity"],
|
|
96
|
+
"coaxial": ["coaxial"],
|
|
97
|
+
"cable_dsl": ["cable modem", "dsl modem"],
|
|
98
|
+
"central_office": ["centraloffice", "central office"],
|
|
99
|
+
"cell_tower": ["cell tower"],
|
|
100
|
+
"power_distribution": ["power distribution"],
|
|
101
|
+
"hot_swappable": ["hotswappable", "hot swappable"],
|
|
102
|
+
"ios_license": ["ios_15", "license"],
|
|
41
103
|
"ntp": ["ntp"],
|
|
42
104
|
"dns": ["dns"],
|
|
43
105
|
"server_dns": ["dns"],
|
|
@@ -54,6 +116,70 @@ CAPABILITY_KEYWORDS = {
|
|
|
54
116
|
"printer": ["printer"],
|
|
55
117
|
}
|
|
56
118
|
|
|
119
|
+
REPORT_ONLY_CAPABILITIES = {
|
|
120
|
+
"ipv6_slaac",
|
|
121
|
+
"dhcpv6_stateful",
|
|
122
|
+
"dhcpv6_stateless",
|
|
123
|
+
"ipv6_prefix_delegation",
|
|
124
|
+
"ipv6_dns_aaaa",
|
|
125
|
+
"ipv6_tunneling",
|
|
126
|
+
"isatap",
|
|
127
|
+
"ospfv3",
|
|
128
|
+
"eigrp_ipv6",
|
|
129
|
+
"ripng",
|
|
130
|
+
"hsrp",
|
|
131
|
+
"dhcp_snooping",
|
|
132
|
+
"dai",
|
|
133
|
+
"dot1x",
|
|
134
|
+
"lldp",
|
|
135
|
+
"rep",
|
|
136
|
+
"snmp",
|
|
137
|
+
"netflow",
|
|
138
|
+
"span",
|
|
139
|
+
"qos",
|
|
140
|
+
"port_security",
|
|
141
|
+
"asa_acl_nat",
|
|
142
|
+
"asa_service_policy",
|
|
143
|
+
"clientless_vpn",
|
|
144
|
+
"cbac",
|
|
145
|
+
"zfw",
|
|
146
|
+
"sniffer",
|
|
147
|
+
"wlc",
|
|
148
|
+
"wpa_enterprise",
|
|
149
|
+
"wep",
|
|
150
|
+
"guest_wifi",
|
|
151
|
+
"beamforming",
|
|
152
|
+
"meraki",
|
|
153
|
+
"cellular_5g",
|
|
154
|
+
"bluetooth",
|
|
155
|
+
"network_controller",
|
|
156
|
+
"python_programming",
|
|
157
|
+
"javascript_programming",
|
|
158
|
+
"blockly_programming",
|
|
159
|
+
"tcp_udp_app",
|
|
160
|
+
"vm_iox",
|
|
161
|
+
"voip",
|
|
162
|
+
"ip_phone",
|
|
163
|
+
"call_manager",
|
|
164
|
+
"linksys_voice",
|
|
165
|
+
"mqtt",
|
|
166
|
+
"real_http",
|
|
167
|
+
"real_websocket",
|
|
168
|
+
"visual_scripting",
|
|
169
|
+
"ptp",
|
|
170
|
+
"profinet",
|
|
171
|
+
"l2nat",
|
|
172
|
+
"cyberobserver",
|
|
173
|
+
"industrial_firewall",
|
|
174
|
+
"coaxial",
|
|
175
|
+
"cable_dsl",
|
|
176
|
+
"central_office",
|
|
177
|
+
"cell_tower",
|
|
178
|
+
"power_distribution",
|
|
179
|
+
"hot_swappable",
|
|
180
|
+
"ios_license",
|
|
181
|
+
}
|
|
182
|
+
|
|
57
183
|
TYPE_NORMALIZATION = {
|
|
58
184
|
"pc": "PC",
|
|
59
185
|
"pc-pt": "PC",
|
|
@@ -76,6 +202,9 @@ TYPE_NORMALIZATION = {
|
|
|
76
202
|
"printer": "Printer",
|
|
77
203
|
"smartphone": "Smartphone",
|
|
78
204
|
"mcucomponent": "IoT",
|
|
205
|
+
"networkcontroller": "NetworkController",
|
|
206
|
+
"centralofficeserver": "CentralOfficeServer",
|
|
207
|
+
"celltower": "CellTower",
|
|
79
208
|
}
|
|
80
209
|
|
|
81
210
|
LICENSE_FILENAMES = [
|
|
@@ -215,6 +344,7 @@ def infer_capability_tags(item: dict[str, Any]) -> list[str]:
|
|
|
215
344
|
rel = item.get("relative_path", "").lower().replace("\\", "/")
|
|
216
345
|
rel_flat = rel.replace("/", " ")
|
|
217
346
|
devices = item.get("devices", [])
|
|
347
|
+
raw_types = {str(device.get("type", "")).strip() for device in devices if str(device.get("type", "")).strip()}
|
|
218
348
|
normalized_types = [normalize_device_type(device.get("type", "")) for device in devices]
|
|
219
349
|
router_count = normalized_types.count("Router")
|
|
220
350
|
switch_count = normalized_types.count("Switch")
|
|
@@ -232,6 +362,15 @@ def infer_capability_tags(item: dict[str, Any]) -> list[str]:
|
|
|
232
362
|
if any(dtype in {"WirelessRouter", "LightWeightAccessPoint"} for dtype in normalized_types):
|
|
233
363
|
tags.add("wireless")
|
|
234
364
|
tags.add("wireless_ap")
|
|
365
|
+
if "HomeGateway" in raw_types or any(dtype == "IoT" for dtype in normalized_types):
|
|
366
|
+
tags.add("iot")
|
|
367
|
+
if any(dtype in {"ASA", "Security Appliance"} for dtype in normalized_types) or raw_types & {"ASA", "Security Appliance"}:
|
|
368
|
+
tags.add("security_edge")
|
|
369
|
+
tags.add("acl")
|
|
370
|
+
if any(dtype in {"Cloud", "Cable Modem", "Dsl Modem"} for dtype in normalized_types):
|
|
371
|
+
tags.add("wan")
|
|
372
|
+
if any(dtype == "MultiLayerSwitch" for dtype in normalized_types):
|
|
373
|
+
tags.add("multilayer_switching")
|
|
235
374
|
if any(dtype in {"Tablet", "Laptop", "Smartphone"} for dtype in normalized_types):
|
|
236
375
|
tags.add("wireless_client")
|
|
237
376
|
if any(dtype == "Tablet" for dtype in normalized_types):
|
|
@@ -314,6 +453,23 @@ def infer_wireless_mode_tags(item: dict[str, Any]) -> list[str]:
|
|
|
314
453
|
return sorted(tags)
|
|
315
454
|
|
|
316
455
|
|
|
456
|
+
def infer_iot_roles(item: dict[str, Any]) -> list[str]:
|
|
457
|
+
roles = {str(role).strip() for role in item.get("iot_roles", []) if str(role).strip()}
|
|
458
|
+
raw_types = {str(device.get("type", "")).strip() for device in item.get("devices", []) if str(device.get("type", "")).strip()}
|
|
459
|
+
normalized_types = {normalize_device_type(device.get("type", "")) for device in item.get("devices", []) if device.get("type")}
|
|
460
|
+
families = set(item.get("device_families", []) or infer_device_families(item))
|
|
461
|
+
has_thing = bool(raw_types & {"MCUComponent", "Board", "Sensor", "Actuator"}) or "iot devices" in families
|
|
462
|
+
has_gateway = "HomeGateway" in raw_types or ("home/wireless routers" in families and has_thing)
|
|
463
|
+
has_server = "Server" in normalized_types or "servers" in families
|
|
464
|
+
if has_thing:
|
|
465
|
+
roles.add("thing")
|
|
466
|
+
if has_gateway:
|
|
467
|
+
roles.add("gateway")
|
|
468
|
+
if has_server:
|
|
469
|
+
roles.add("server")
|
|
470
|
+
return sorted(roles)
|
|
471
|
+
|
|
472
|
+
|
|
317
473
|
def infer_device_families(item: dict[str, Any]) -> list[str]:
|
|
318
474
|
family_map = {
|
|
319
475
|
"Router": "routers",
|
|
@@ -336,6 +492,9 @@ def infer_device_families(item: dict[str, Any]) -> list[str]:
|
|
|
336
492
|
"Dsl Modem": "wan/cloud/dsl/cable devices",
|
|
337
493
|
"Security Appliance": "security devices",
|
|
338
494
|
"ASA": "security devices",
|
|
495
|
+
"NetworkController": "network controllers",
|
|
496
|
+
"CentralOfficeServer": "wan/cloud/dsl/cable devices",
|
|
497
|
+
"CellTower": "wan/cloud/dsl/cable devices",
|
|
339
498
|
"IoT": "iot devices",
|
|
340
499
|
"Sensor": "iot devices",
|
|
341
500
|
"Actuator": "iot devices",
|
|
@@ -397,12 +556,24 @@ def infer_service_support(item: dict[str, Any]) -> list[str]:
|
|
|
397
556
|
|
|
398
557
|
def infer_runtime_features(item: dict[str, Any]) -> list[str]:
|
|
399
558
|
features: set[str] = set()
|
|
559
|
+
tags = set(item.get("capability_tags", []))
|
|
560
|
+
families = set(item.get("device_families", []) or infer_device_families(item))
|
|
400
561
|
if item.get("wireless_mode_tags"):
|
|
401
562
|
features.add("wireless_runtime")
|
|
402
563
|
if item.get("service_support"):
|
|
403
564
|
features.add("service_runtime")
|
|
404
|
-
if item
|
|
565
|
+
if infer_iot_roles(item):
|
|
405
566
|
features.add("iot_runtime")
|
|
567
|
+
if families & {"wan/cloud/dsl/cable devices"} or tags & {"wan", "ppp"}:
|
|
568
|
+
features.add("wan_runtime")
|
|
569
|
+
if families & {"security devices"} or tags & {"security_edge", "acl", "nat"}:
|
|
570
|
+
features.add("security_runtime")
|
|
571
|
+
if tags & {"vpn", "ipsec", "gre"}:
|
|
572
|
+
features.add("tunnel_runtime")
|
|
573
|
+
if tags & {"ipv6_slaac", "dhcpv6_stateful", "dhcpv6_stateless", "ospfv3", "eigrp_ipv6", "ripng", "hsrp"}:
|
|
574
|
+
features.add("ipv6_runtime")
|
|
575
|
+
if families & {"multilayer switches"} or tags & {"multilayer_switching"}:
|
|
576
|
+
features.add("multilayer_runtime")
|
|
406
577
|
if item.get("workspace_validation"):
|
|
407
578
|
features.add("workspace_validated")
|
|
408
579
|
if any(str(link.get("from", "")) and str(link.get("to", "")) for link in item.get("links", [])):
|
|
@@ -416,7 +587,7 @@ def infer_archetype_tags(item: dict[str, Any]) -> list[str]:
|
|
|
416
587
|
families = set(item.get("device_families", []))
|
|
417
588
|
services = set(item.get("service_support", []))
|
|
418
589
|
wireless_modes = set(item.get("wireless_mode_tags", []))
|
|
419
|
-
iot_roles = set(item
|
|
590
|
+
iot_roles = set(infer_iot_roles(item))
|
|
420
591
|
|
|
421
592
|
if topology & {"chain", "core_access", "department_lan", "router_on_a_stick"} or (
|
|
422
593
|
families & {"routers", "switches", "multilayer switches"}
|
|
@@ -431,10 +602,29 @@ def infer_archetype_tags(item: dict[str, Any]) -> list[str]:
|
|
|
431
602
|
tags.add("IoT/home gateway")
|
|
432
603
|
if families & {"wan/cloud/dsl/cable devices", "security devices"} or set(item.get("capability_tags", [])) & {
|
|
433
604
|
"vpn",
|
|
605
|
+
"ipsec",
|
|
606
|
+
"gre",
|
|
607
|
+
"ppp",
|
|
608
|
+
"wan",
|
|
609
|
+
"security_edge",
|
|
610
|
+
"multilayer_switching",
|
|
434
611
|
"nat",
|
|
435
612
|
"acl",
|
|
436
613
|
}:
|
|
437
614
|
tags.add("WAN/security edge")
|
|
615
|
+
capability_tags = set(item.get("capability_tags", []))
|
|
616
|
+
if capability_tags & {"ipv6_slaac", "dhcpv6_stateful", "dhcpv6_stateless", "ipv6_prefix_delegation", "ipv6_dns_aaaa", "ipv6_tunneling", "isatap", "ospfv3", "eigrp_ipv6", "ripng", "hsrp"}:
|
|
617
|
+
tags.add("IPv6/routing")
|
|
618
|
+
if capability_tags & {"dhcp_snooping", "dai", "dot1x", "lldp", "rep", "snmp", "netflow", "span", "qos", "port_security"}:
|
|
619
|
+
tags.add("L2 security/monitoring")
|
|
620
|
+
if capability_tags & {"wlc", "wpa_enterprise", "wep", "guest_wifi", "beamforming", "meraki", "cellular_5g", "bluetooth"}:
|
|
621
|
+
tags.add("advanced wireless")
|
|
622
|
+
if capability_tags & {"network_controller", "python_programming", "javascript_programming", "blockly_programming", "tcp_udp_app", "vm_iox"} or "network controllers" in families:
|
|
623
|
+
tags.add("automation/controller")
|
|
624
|
+
if capability_tags & {"voip", "ip_phone", "call_manager", "linksys_voice"}:
|
|
625
|
+
tags.add("voice/collaboration")
|
|
626
|
+
if capability_tags & {"mqtt", "real_http", "real_websocket", "visual_scripting", "ptp", "profinet", "l2nat", "cyberobserver", "industrial_firewall"}:
|
|
627
|
+
tags.add("industrial IoT")
|
|
438
628
|
return sorted(tags)
|
|
439
629
|
|
|
440
630
|
|
|
@@ -467,6 +657,8 @@ def infer_apply_safety_level(item: dict[str, Any]) -> str:
|
|
|
467
657
|
|
|
468
658
|
def infer_validated_edit_capabilities(item: dict[str, Any]) -> list[str]:
|
|
469
659
|
capabilities: set[str] = {str(tag) for tag in item.get("capability_tags", []) if str(tag).strip()}
|
|
660
|
+
inferred_iot_roles = set(infer_iot_roles(item))
|
|
661
|
+
families = set(item.get("device_families", []) or infer_device_families(item))
|
|
470
662
|
service_capability_map = {
|
|
471
663
|
"dhcp": "server_dhcp",
|
|
472
664
|
"dns": "server_dns",
|
|
@@ -487,12 +679,24 @@ def infer_validated_edit_capabilities(item: dict[str, Any]) -> list[str]:
|
|
|
487
679
|
capabilities.add("wireless_mutation")
|
|
488
680
|
if any(device_family == "end devices" for device_family in item.get("device_families", [])):
|
|
489
681
|
capabilities.add("end_device_mutation")
|
|
490
|
-
if
|
|
682
|
+
if inferred_iot_roles:
|
|
683
|
+
capabilities.add("iot")
|
|
491
684
|
capabilities.add("iot_registration")
|
|
492
|
-
if "server"
|
|
685
|
+
if {"server", "gateway"} & inferred_iot_roles:
|
|
493
686
|
capabilities.add("iot_control")
|
|
687
|
+
if capabilities & {"vpn", "ipsec", "gre"}:
|
|
688
|
+
capabilities.update({"vpn", "ipsec", "gre"})
|
|
689
|
+
if capabilities & {"ppp", "wan"} or families & {"wan/cloud/dsl/cable devices"}:
|
|
690
|
+
capabilities.add("wan")
|
|
691
|
+
if "ppp" in capabilities:
|
|
692
|
+
capabilities.add("ppp")
|
|
693
|
+
if capabilities & {"acl", "nat", "security_edge"} or families & {"security devices"}:
|
|
694
|
+
capabilities.add("security_edge")
|
|
695
|
+
if "multilayer switches" in families or "multilayer_switching" in capabilities:
|
|
696
|
+
capabilities.add("multilayer_switching")
|
|
494
697
|
if item.get("apply_safety_level") == "inventory-supported":
|
|
495
698
|
return []
|
|
699
|
+
capabilities -= REPORT_ONLY_CAPABILITIES
|
|
496
700
|
return sorted(capabilities)
|
|
497
701
|
|
|
498
702
|
|
|
@@ -731,7 +935,8 @@ def enrich_catalog_items(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|
|
731
935
|
new_item.setdefault("model_families", infer_model_families(new_item))
|
|
732
936
|
new_item.setdefault("port_media_types", infer_port_media_types(new_item))
|
|
733
937
|
new_item.setdefault("service_support", infer_service_support(new_item))
|
|
734
|
-
new_item.setdefault("
|
|
938
|
+
new_item.setdefault("iot_roles", infer_iot_roles(new_item))
|
|
939
|
+
new_item["runtime_features"] = sorted(set(new_item.get("runtime_features", [])) | set(infer_runtime_features(new_item)))
|
|
735
940
|
new_item.setdefault("donor_graph_fingerprint", infer_donor_graph_fingerprint(new_item))
|
|
736
941
|
new_item.setdefault("apply_safety_level", infer_apply_safety_level(new_item))
|
|
737
942
|
new_item.setdefault("archetype_tags", infer_archetype_tags(new_item))
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|