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
@@ -24,20 +24,102 @@ 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
+ "ospfv2": ["ospf", "ospfv2", "single-area ospf"],
37
+ "ospfv3": ["ospfv3", "ipv6_ospf"],
29
38
  "eigrp": ["eigrp"],
39
+ "eigrp_ipv4": ["eigrp", "ipv4 eigrp"],
40
+ "eigrp_ipv6": ["ipv6_eigrp"],
30
41
  "rip": ["rip"],
42
+ "ripv2": ["ripv2", "rip"],
43
+ "ripng": ["ripng", "ipv6 rip"],
44
+ "hsrp": ["hsrp"],
31
45
  "nat": ["nat"],
46
+ "nat_static": ["static nat"],
47
+ "nat_dynamic": ["dynamic nat"],
48
+ "pat": ["pat", "overload"],
49
+ "static_route": ["static route", "ip route"],
50
+ "default_route": ["default route"],
51
+ "dhcp_relay": ["dhcp relay", "helper-address"],
52
+ "ssh_ios": ["ssh"],
53
+ "ntp_ios": ["ntp"],
54
+ "syslog_ios": ["syslog"],
32
55
  "acl": ["acl", "access-list"],
56
+ "dai": ["dai", "dynamic arp inspection"],
57
+ "dot1x": ["dot1x", "802.1x", "port-based nac"],
58
+ "lldp": ["lldp"],
59
+ "rep": ["rep_"],
60
+ "snmp": ["snmp"],
61
+ "netflow": ["netflow"],
62
+ "span": ["rspan", "monitor session", "span session"],
63
+ "qos": ["qos"],
64
+ "port_security": ["port security", "port-security"],
65
+ "bgp": ["bgp"],
66
+ "stp": ["stp", "spanning tree", "spanning-tree"],
67
+ "rstp": ["rstp", "rapid-pvst"],
68
+ "etherchannel": ["etherchannel", "port-channel", "channel-group"],
69
+ "lacp": ["lacp"],
70
+ "pagp": ["pagp"],
71
+ "vtp": ["vtp"],
72
+ "dtp": ["dtp"],
33
73
  "vpn": ["vpn", "ipsec", "gre"],
34
74
  "ipsec": ["ipsec", "ike"],
35
75
  "gre": ["gre"],
36
76
  "ppp": ["ppp", "chap", "pap"],
77
+ "wan": ["wan", "cloud", "serial", "pppoe", "ppp"],
78
+ "security_edge": ["asa", "firewall", "security appliance"],
79
+ "asa_acl_nat": ["asa_acl_nat"],
80
+ "asa_service_policy": ["asa_service_policy", "service policy"],
81
+ "clientless_vpn": ["clientless_vpn"],
82
+ "cbac": ["cbac"],
83
+ "zfw": ["zfw", "zone based firewall"],
84
+ "sniffer": ["sniffer"],
37
85
  "multilayer_switching": ["multilayer", "layer 3", "3560", "svi"],
38
86
  "wireless": ["wireless", "wlan", "wlc", "wifi", "ssid", "wpa", "wep", "5g", "bluetooth", "cellular"],
39
87
  "wireless_ap": ["wireless", "wlan", "wlc", "wifi", "ssid", "wpa", "wep"],
40
88
  "wireless_client": ["wireless", "wifi", "tablet", "smartphone", "laptop"],
89
+ "wlc": ["wlc", "wireless lan controller"],
90
+ "wpa_enterprise": ["wpa_radius", "wpa2_enterprise"],
91
+ "wep": ["wep"],
92
+ "guest_wifi": ["guest"],
93
+ "beamforming": ["beamforming"],
94
+ "meraki": ["meraki"],
95
+ "cellular_5g": ["5g", "cellular", "cell tower"],
96
+ "bluetooth": ["bluetooth"],
97
+ "network_controller": ["network controller", "netcon"],
98
+ "python_programming": ["python"],
99
+ "javascript_programming": ["javascript"],
100
+ "blockly_programming": ["blockly"],
101
+ "tcp_udp_app": ["tcp_test_app", "udp_test_app"],
102
+ "vm_iox": ["iox", "uploading_and_running_vm"],
103
+ "voip": ["voip"],
104
+ "ip_phone": ["ip phone", "phone"],
105
+ "call_manager": ["call manager", "telephony", "voip"],
106
+ "linksys_voice": ["voip linksys"],
107
+ "mqtt": ["mqtt"],
108
+ "real_http": ["real-http", "real http"],
109
+ "real_websocket": ["real-websocket", "real websocket"],
110
+ "visual_scripting": ["visual-scripting", "visual scripting"],
111
+ "ptp": ["ptp"],
112
+ "profinet": ["profinet"],
113
+ "l2nat": ["l2nat"],
114
+ "cyberobserver": ["cyberobserver"],
115
+ "industrial_firewall": ["isa3000", "industrial cybersecurity"],
116
+ "coaxial": ["coaxial"],
117
+ "cable_dsl": ["cable modem", "dsl modem"],
118
+ "central_office": ["centraloffice", "central office"],
119
+ "cell_tower": ["cell tower"],
120
+ "power_distribution": ["power distribution"],
121
+ "hot_swappable": ["hotswappable", "hot swappable"],
122
+ "ios_license": ["ios_15", "license"],
41
123
  "ntp": ["ntp"],
42
124
  "dns": ["dns"],
43
125
  "server_dns": ["dns"],
@@ -54,6 +136,90 @@ CAPABILITY_KEYWORDS = {
54
136
  "printer": ["printer"],
55
137
  }
56
138
 
139
+ REPORT_ONLY_CAPABILITIES = {
140
+ "ipv6_slaac",
141
+ "dhcpv6_stateful",
142
+ "dhcpv6_stateless",
143
+ "ipv6_prefix_delegation",
144
+ "ipv6_dns_aaaa",
145
+ "ipv6_tunneling",
146
+ "isatap",
147
+ "ospfv3",
148
+ "eigrp_ipv6",
149
+ "ripng",
150
+ "hsrp",
151
+ "dhcp_snooping",
152
+ "dai",
153
+ "dot1x",
154
+ "lldp",
155
+ "rep",
156
+ "snmp",
157
+ "netflow",
158
+ "span",
159
+ "qos",
160
+ "port_security",
161
+ "bgp",
162
+ "stp",
163
+ "rstp",
164
+ "etherchannel",
165
+ "lacp",
166
+ "pagp",
167
+ "vtp",
168
+ "dtp",
169
+ "ospfv2",
170
+ "eigrp_ipv4",
171
+ "ripv2",
172
+ "static_route",
173
+ "default_route",
174
+ "dhcp_relay",
175
+ "nat_static",
176
+ "nat_dynamic",
177
+ "pat",
178
+ "ssh_ios",
179
+ "ntp_ios",
180
+ "syslog_ios",
181
+ "asa_acl_nat",
182
+ "asa_service_policy",
183
+ "clientless_vpn",
184
+ "cbac",
185
+ "zfw",
186
+ "sniffer",
187
+ "wlc",
188
+ "wpa_enterprise",
189
+ "wep",
190
+ "guest_wifi",
191
+ "beamforming",
192
+ "meraki",
193
+ "cellular_5g",
194
+ "bluetooth",
195
+ "network_controller",
196
+ "python_programming",
197
+ "javascript_programming",
198
+ "blockly_programming",
199
+ "tcp_udp_app",
200
+ "vm_iox",
201
+ "voip",
202
+ "ip_phone",
203
+ "call_manager",
204
+ "linksys_voice",
205
+ "mqtt",
206
+ "real_http",
207
+ "real_websocket",
208
+ "visual_scripting",
209
+ "ptp",
210
+ "profinet",
211
+ "l2nat",
212
+ "cyberobserver",
213
+ "industrial_firewall",
214
+ "coaxial",
215
+ "cable_dsl",
216
+ "central_office",
217
+ "cell_tower",
218
+ "power_distribution",
219
+ "hot_swappable",
220
+ "ios_license",
221
+ }
222
+
57
223
  TYPE_NORMALIZATION = {
58
224
  "pc": "PC",
59
225
  "pc-pt": "PC",
@@ -76,6 +242,9 @@ TYPE_NORMALIZATION = {
76
242
  "printer": "Printer",
77
243
  "smartphone": "Smartphone",
78
244
  "mcucomponent": "IoT",
245
+ "networkcontroller": "NetworkController",
246
+ "centralofficeserver": "CentralOfficeServer",
247
+ "celltower": "CellTower",
79
248
  }
80
249
 
81
250
  LICENSE_FILENAMES = [
@@ -215,6 +384,7 @@ def infer_capability_tags(item: dict[str, Any]) -> list[str]:
215
384
  rel = item.get("relative_path", "").lower().replace("\\", "/")
216
385
  rel_flat = rel.replace("/", " ")
217
386
  devices = item.get("devices", [])
387
+ raw_types = {str(device.get("type", "")).strip() for device in devices if str(device.get("type", "")).strip()}
218
388
  normalized_types = [normalize_device_type(device.get("type", "")) for device in devices]
219
389
  router_count = normalized_types.count("Router")
220
390
  switch_count = normalized_types.count("Switch")
@@ -232,6 +402,15 @@ def infer_capability_tags(item: dict[str, Any]) -> list[str]:
232
402
  if any(dtype in {"WirelessRouter", "LightWeightAccessPoint"} for dtype in normalized_types):
233
403
  tags.add("wireless")
234
404
  tags.add("wireless_ap")
405
+ if "HomeGateway" in raw_types or any(dtype == "IoT" for dtype in normalized_types):
406
+ tags.add("iot")
407
+ if any(dtype in {"ASA", "Security Appliance"} for dtype in normalized_types) or raw_types & {"ASA", "Security Appliance"}:
408
+ tags.add("security_edge")
409
+ tags.add("acl")
410
+ if any(dtype in {"Cloud", "Cable Modem", "Dsl Modem"} for dtype in normalized_types):
411
+ tags.add("wan")
412
+ if any(dtype == "MultiLayerSwitch" for dtype in normalized_types):
413
+ tags.add("multilayer_switching")
235
414
  if any(dtype in {"Tablet", "Laptop", "Smartphone"} for dtype in normalized_types):
236
415
  tags.add("wireless_client")
237
416
  if any(dtype == "Tablet" for dtype in normalized_types):
@@ -283,8 +462,12 @@ def infer_preferred_roles(item: dict[str, Any]) -> list[str]:
283
462
  roles.append("preferred_vlan")
284
463
  if "dhcp_pool" in tags:
285
464
  roles.append("preferred_dhcp")
286
- if {"ospf", "eigrp", "rip"} & tags:
465
+ if {"ospf", "eigrp", "rip", "bgp", "ospfv2", "eigrp_ipv4", "ripv2", "static_route", "default_route"} & tags:
287
466
  roles.append("preferred_routing")
467
+ if {"dhcp_relay", "nat_static", "nat_dynamic", "pat", "ssh_ios", "ntp_ios", "syslog_ios"} & tags:
468
+ roles.append("preferred_ipv4_management")
469
+ if {"stp", "rstp", "etherchannel", "lacp", "pagp", "vtp", "dtp"} & tags:
470
+ roles.append("preferred_l2_resiliency")
288
471
  if {"nat", "acl", "vpn"} & tags:
289
472
  roles.append("preferred_security")
290
473
  if {"vpn", "ipsec", "gre", "ppp", "multilayer_switching"} & tags:
@@ -314,6 +497,23 @@ def infer_wireless_mode_tags(item: dict[str, Any]) -> list[str]:
314
497
  return sorted(tags)
315
498
 
316
499
 
500
+ def infer_iot_roles(item: dict[str, Any]) -> list[str]:
501
+ roles = {str(role).strip() for role in item.get("iot_roles", []) if str(role).strip()}
502
+ raw_types = {str(device.get("type", "")).strip() for device in item.get("devices", []) if str(device.get("type", "")).strip()}
503
+ normalized_types = {normalize_device_type(device.get("type", "")) for device in item.get("devices", []) if device.get("type")}
504
+ families = set(item.get("device_families", []) or infer_device_families(item))
505
+ has_thing = bool(raw_types & {"MCUComponent", "Board", "Sensor", "Actuator"}) or "iot devices" in families
506
+ has_gateway = "HomeGateway" in raw_types or ("home/wireless routers" in families and has_thing)
507
+ has_server = "Server" in normalized_types or "servers" in families
508
+ if has_thing:
509
+ roles.add("thing")
510
+ if has_gateway:
511
+ roles.add("gateway")
512
+ if has_server:
513
+ roles.add("server")
514
+ return sorted(roles)
515
+
516
+
317
517
  def infer_device_families(item: dict[str, Any]) -> list[str]:
318
518
  family_map = {
319
519
  "Router": "routers",
@@ -325,6 +525,9 @@ def infer_device_families(item: dict[str, Any]) -> list[str]:
325
525
  "Tablet": "end devices",
326
526
  "Smartphone": "end devices",
327
527
  "Printer": "end devices",
528
+ "IpPhone": "end devices",
529
+ "HomeVoip": "end devices",
530
+ "AnalogPhone": "end devices",
328
531
  "LightWeightAccessPoint": "access points",
329
532
  "WirelessRouter": "home/wireless routers",
330
533
  "WirelessRouterNewGeneration": "home/wireless routers",
@@ -336,6 +539,9 @@ def infer_device_families(item: dict[str, Any]) -> list[str]:
336
539
  "Dsl Modem": "wan/cloud/dsl/cable devices",
337
540
  "Security Appliance": "security devices",
338
541
  "ASA": "security devices",
542
+ "NetworkController": "network controllers",
543
+ "CentralOfficeServer": "wan/cloud/dsl/cable devices",
544
+ "CellTower": "wan/cloud/dsl/cable devices",
339
545
  "IoT": "iot devices",
340
546
  "Sensor": "iot devices",
341
547
  "Actuator": "iot devices",
@@ -397,12 +603,28 @@ def infer_service_support(item: dict[str, Any]) -> list[str]:
397
603
 
398
604
  def infer_runtime_features(item: dict[str, Any]) -> list[str]:
399
605
  features: set[str] = set()
606
+ tags = set(item.get("capability_tags", []))
607
+ families = set(item.get("device_families", []) or infer_device_families(item))
400
608
  if item.get("wireless_mode_tags"):
401
609
  features.add("wireless_runtime")
402
610
  if item.get("service_support"):
403
611
  features.add("service_runtime")
404
- if item.get("iot_roles"):
612
+ if infer_iot_roles(item):
405
613
  features.add("iot_runtime")
614
+ if families & {"wan/cloud/dsl/cable devices"} or tags & {"wan", "ppp"}:
615
+ features.add("wan_runtime")
616
+ if families & {"security devices"} or tags & {"security_edge", "acl", "nat"}:
617
+ features.add("security_runtime")
618
+ if tags & {"vpn", "ipsec", "gre"}:
619
+ features.add("tunnel_runtime")
620
+ if tags & {"ipv6_slaac", "dhcpv6_stateful", "dhcpv6_stateless", "ospfv3", "eigrp_ipv6", "ripng", "hsrp"}:
621
+ features.add("ipv6_runtime")
622
+ if tags & {"bgp", "stp", "rstp", "etherchannel", "lacp", "pagp", "vtp", "dtp"}:
623
+ features.add("l2_resiliency_runtime")
624
+ if tags & {"ospfv2", "eigrp_ipv4", "ripv2", "static_route", "default_route", "dhcp_relay", "nat_static", "nat_dynamic", "pat", "ssh_ios", "ntp_ios", "syslog_ios"}:
625
+ features.add("ipv4_routing_management_runtime")
626
+ if families & {"multilayer switches"} or tags & {"multilayer_switching"}:
627
+ features.add("multilayer_runtime")
406
628
  if item.get("workspace_validation"):
407
629
  features.add("workspace_validated")
408
630
  if any(str(link.get("from", "")) and str(link.get("to", "")) for link in item.get("links", [])):
@@ -416,7 +638,7 @@ def infer_archetype_tags(item: dict[str, Any]) -> list[str]:
416
638
  families = set(item.get("device_families", []))
417
639
  services = set(item.get("service_support", []))
418
640
  wireless_modes = set(item.get("wireless_mode_tags", []))
419
- iot_roles = set(item.get("iot_roles", []))
641
+ iot_roles = set(infer_iot_roles(item))
420
642
 
421
643
  if topology & {"chain", "core_access", "department_lan", "router_on_a_stick"} or (
422
644
  families & {"routers", "switches", "multilayer switches"}
@@ -431,10 +653,33 @@ def infer_archetype_tags(item: dict[str, Any]) -> list[str]:
431
653
  tags.add("IoT/home gateway")
432
654
  if families & {"wan/cloud/dsl/cable devices", "security devices"} or set(item.get("capability_tags", [])) & {
433
655
  "vpn",
656
+ "ipsec",
657
+ "gre",
658
+ "ppp",
659
+ "wan",
660
+ "security_edge",
661
+ "multilayer_switching",
434
662
  "nat",
435
663
  "acl",
436
664
  }:
437
665
  tags.add("WAN/security edge")
666
+ capability_tags = set(item.get("capability_tags", []))
667
+ if capability_tags & {"ipv6_slaac", "dhcpv6_stateful", "dhcpv6_stateless", "ipv6_prefix_delegation", "ipv6_dns_aaaa", "ipv6_tunneling", "isatap", "ospfv3", "eigrp_ipv6", "ripng", "hsrp"}:
668
+ tags.add("IPv6/routing")
669
+ if capability_tags & {"dhcp_snooping", "dai", "dot1x", "lldp", "rep", "snmp", "netflow", "span", "qos", "port_security"}:
670
+ tags.add("L2 security/monitoring")
671
+ if capability_tags & {"bgp", "stp", "rstp", "etherchannel", "lacp", "pagp", "vtp", "dtp"}:
672
+ tags.add("L2 resiliency/routing")
673
+ if capability_tags & {"ospfv2", "eigrp_ipv4", "ripv2", "static_route", "default_route", "dhcp_relay", "nat_static", "nat_dynamic", "pat", "ssh_ios", "ntp_ios", "syslog_ios"}:
674
+ tags.add("IPv4 routing/management")
675
+ if capability_tags & {"wlc", "wpa_enterprise", "wep", "guest_wifi", "beamforming", "meraki", "cellular_5g", "bluetooth"}:
676
+ tags.add("advanced wireless")
677
+ if capability_tags & {"network_controller", "python_programming", "javascript_programming", "blockly_programming", "tcp_udp_app", "vm_iox"} or "network controllers" in families:
678
+ tags.add("automation/controller")
679
+ if capability_tags & {"voip", "ip_phone", "call_manager", "linksys_voice"}:
680
+ tags.add("voice/collaboration")
681
+ if capability_tags & {"mqtt", "real_http", "real_websocket", "visual_scripting", "ptp", "profinet", "l2nat", "cyberobserver", "industrial_firewall"}:
682
+ tags.add("industrial IoT")
438
683
  return sorted(tags)
439
684
 
440
685
 
@@ -467,6 +712,8 @@ def infer_apply_safety_level(item: dict[str, Any]) -> str:
467
712
 
468
713
  def infer_validated_edit_capabilities(item: dict[str, Any]) -> list[str]:
469
714
  capabilities: set[str] = {str(tag) for tag in item.get("capability_tags", []) if str(tag).strip()}
715
+ inferred_iot_roles = set(infer_iot_roles(item))
716
+ families = set(item.get("device_families", []) or infer_device_families(item))
470
717
  service_capability_map = {
471
718
  "dhcp": "server_dhcp",
472
719
  "dns": "server_dns",
@@ -487,12 +734,24 @@ def infer_validated_edit_capabilities(item: dict[str, Any]) -> list[str]:
487
734
  capabilities.add("wireless_mutation")
488
735
  if any(device_family == "end devices" for device_family in item.get("device_families", [])):
489
736
  capabilities.add("end_device_mutation")
490
- if item.get("iot_roles"):
737
+ if inferred_iot_roles:
738
+ capabilities.add("iot")
491
739
  capabilities.add("iot_registration")
492
- if "server" in {str(role) for role in item.get("iot_roles", [])}:
740
+ if {"server", "gateway"} & inferred_iot_roles:
493
741
  capabilities.add("iot_control")
742
+ if capabilities & {"vpn", "ipsec", "gre"}:
743
+ capabilities.update({"vpn", "ipsec", "gre"})
744
+ if capabilities & {"ppp", "wan"} or families & {"wan/cloud/dsl/cable devices"}:
745
+ capabilities.add("wan")
746
+ if "ppp" in capabilities:
747
+ capabilities.add("ppp")
748
+ if capabilities & {"acl", "nat", "security_edge"} or families & {"security devices"}:
749
+ capabilities.add("security_edge")
750
+ if "multilayer switches" in families or "multilayer_switching" in capabilities:
751
+ capabilities.add("multilayer_switching")
494
752
  if item.get("apply_safety_level") == "inventory-supported":
495
753
  return []
754
+ capabilities -= REPORT_ONLY_CAPABILITIES
496
755
  return sorted(capabilities)
497
756
 
498
757
 
@@ -731,7 +990,8 @@ def enrich_catalog_items(items: list[dict[str, Any]]) -> list[dict[str, Any]]:
731
990
  new_item.setdefault("model_families", infer_model_families(new_item))
732
991
  new_item.setdefault("port_media_types", infer_port_media_types(new_item))
733
992
  new_item.setdefault("service_support", infer_service_support(new_item))
734
- new_item.setdefault("runtime_features", infer_runtime_features(new_item))
993
+ new_item.setdefault("iot_roles", infer_iot_roles(new_item))
994
+ new_item["runtime_features"] = sorted(set(new_item.get("runtime_features", [])) | set(infer_runtime_features(new_item)))
735
995
  new_item.setdefault("donor_graph_fingerprint", infer_donor_graph_fingerprint(new_item))
736
996
  new_item.setdefault("apply_safety_level", infer_apply_safety_level(new_item))
737
997
  new_item.setdefault("archetype_tags", infer_archetype_tags(new_item))