gini-toolkit 6.0.1.dev0__py3-none-any.whl

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 (278) hide show
  1. gini/__init__.py +12 -0
  2. gini/__main__.py +107 -0
  3. gini/_version.py +24 -0
  4. gini/agent/__init__.py +17 -0
  5. gini/agent/agent_gamemaster.py +140 -0
  6. gini/agent/api.py +291 -0
  7. gini/agent/ask.py +123 -0
  8. gini/agent/authoring.py +72 -0
  9. gini/agent/blackboard.py +114 -0
  10. gini/agent/contracts.py +142 -0
  11. gini/agent/domains.py +91 -0
  12. gini/agent/embed.py +123 -0
  13. gini/agent/gamemaster.py +256 -0
  14. gini/agent/kb.py +148 -0
  15. gini/agent/lesson_resolver.py +261 -0
  16. gini/agent/llm/__init__.py +5 -0
  17. gini/agent/llm/backend.py +43 -0
  18. gini/agent/llm/fake.py +25 -0
  19. gini/agent/llm/ollama.py +206 -0
  20. gini/agent/loop.py +258 -0
  21. gini/agent/mcp_server.py +86 -0
  22. gini/agent/meaning.py +225 -0
  23. gini/agent/mission.py +210 -0
  24. gini/agent/mission_controller.py +208 -0
  25. gini/agent/narration.py +116 -0
  26. gini/agent/notifier.py +86 -0
  27. gini/agent/personas.py +79 -0
  28. gini/agent/reasoning.py +172 -0
  29. gini/agent/recall.py +248 -0
  30. gini/agent/session.py +79 -0
  31. gini/agent/teaching_center.py +482 -0
  32. gini/agent/tools/__init__.py +3 -0
  33. gini/agent/tools/registry.py +193 -0
  34. gini/agent/twin/__init__.py +28 -0
  35. gini/agent/twin/authoring.py +71 -0
  36. gini/agent/twin/contracts.py +54 -0
  37. gini/agent/twin/dialectic.py +189 -0
  38. gini/agent/twin/harness.py +93 -0
  39. gini/agent/twin/justify.py +156 -0
  40. gini/agent/twin/learner.py +64 -0
  41. gini/agent/twin/mission.py +60 -0
  42. gini/agent/twin/os_coach.py +79 -0
  43. gini/agent/twin/salience.py +30 -0
  44. gini/agent/understand.py +250 -0
  45. gini/agent/verifiers.py +106 -0
  46. gini/agent/wizard.py +178 -0
  47. gini/agent/xv6_pack.py +74 -0
  48. gini/app/__init__.py +3 -0
  49. gini/app/context.py +368 -0
  50. gini/app/paths.py +121 -0
  51. gini/data/README.md +21 -0
  52. gini/domain/__init__.py +9 -0
  53. gini/domain/assembly.py +209 -0
  54. gini/domain/authoring.py +353 -0
  55. gini/domain/blueprints.py +5 -0
  56. gini/domain/capabilities.py +177 -0
  57. gini/domain/catalog.py +85 -0
  58. gini/domain/certify.py +201 -0
  59. gini/domain/compose.py +413 -0
  60. gini/domain/composition.py +88 -0
  61. gini/domain/concepts.py +383 -0
  62. gini/domain/connection_rules.py +269 -0
  63. gini/domain/constraints.py +153 -0
  64. gini/domain/content.py +59 -0
  65. gini/domain/cpu_journey.py +89 -0
  66. gini/domain/devices.py +747 -0
  67. gini/domain/diagnose.py +201 -0
  68. gini/domain/element_guide.py +327 -0
  69. gini/domain/explain.py +90 -0
  70. gini/domain/fingerprint.py +201 -0
  71. gini/domain/firewall.py +34 -0
  72. gini/domain/flowlog.py +61 -0
  73. gini/domain/flowtable.py +179 -0
  74. gini/domain/fragment_yaml.py +230 -0
  75. gini/domain/fragments.py +169 -0
  76. gini/domain/games/__init__.py +2 -0
  77. gini/domain/games/paging_games.py +119 -0
  78. gini/domain/games/policy_game.py +86 -0
  79. gini/domain/games/process_game.py +48 -0
  80. gini/domain/games/thrash_game.py +75 -0
  81. gini/domain/games/translate_game.py +60 -0
  82. gini/domain/games/trap_game.py +86 -0
  83. gini/domain/grader.py +155 -0
  84. gini/domain/grouping.py +67 -0
  85. gini/domain/legality.py +103 -0
  86. gini/domain/lesson.py +241 -0
  87. gini/domain/lexicon.py +150 -0
  88. gini/domain/machine_state.py +410 -0
  89. gini/domain/missions/networking/basic-lan.yaml +32 -0
  90. gini/domain/missions/networking/cache-in-front.yaml +23 -0
  91. gini/domain/missions/networking/decouple-with-queue.yaml +31 -0
  92. gini/domain/missions/networking/drive-load.yaml +20 -0
  93. gini/domain/missions/networking/fix-the-address.yaml +75 -0
  94. gini/domain/missions/networking/fix-the-lan.yaml +43 -0
  95. gini/domain/missions/networking/inspect-flows.yaml +16 -0
  96. gini/domain/missions/networking/k8s-autoscale.yaml +27 -0
  97. gini/domain/missions/networking/least-privilege.yaml +21 -0
  98. gini/domain/missions/networking/load-balanced-web.yaml +29 -0
  99. gini/domain/missions/networking/observe-it.yaml +24 -0
  100. gini/domain/missions/networking/put-in-vpc.yaml +30 -0
  101. gini/domain/missions/networking/reachability-boundary.yaml +56 -0
  102. gini/domain/missions/networking/sdn-reactive.yaml +35 -0
  103. gini/domain/missions/networking/send-request.yaml +19 -0
  104. gini/domain/missions/networking/serverless-api.yaml +25 -0
  105. gini/domain/missions/networking/service-chain.yaml +33 -0
  106. gini/domain/missions/os/lottery-fix.yaml +19 -0
  107. gini/domain/missions/os/priority-fix.yaml +24 -0
  108. gini/domain/missions.py +111 -0
  109. gini/domain/modulechain.py +36 -0
  110. gini/domain/objectives.py +488 -0
  111. gini/domain/os_zoo.py +79 -0
  112. gini/domain/paging_sim.py +141 -0
  113. gini/domain/pricing.py +199 -0
  114. gini/domain/probes.py +226 -0
  115. gini/domain/profile.py +142 -0
  116. gini/domain/recipes.py +738 -0
  117. gini/domain/riders.py +309 -0
  118. gini/domain/router_modules.py +224 -0
  119. gini/domain/routetable.py +67 -0
  120. gini/domain/scoring.py +76 -0
  121. gini/domain/staging.py +122 -0
  122. gini/domain/syscall_builder.py +144 -0
  123. gini/domain/topic_cloud.py +62 -0
  124. gini/domain/topology.py +213 -0
  125. gini/domain/vocabulary.py +51 -0
  126. gini/domain/xv6.py +808 -0
  127. gini/domain/xv6_fs.py +250 -0
  128. gini/domain/xv6_runner.py +113 -0
  129. gini/domain/xv6_vm.py +385 -0
  130. gini/gloader.py +17 -0
  131. gini/runtime/__init__.py +18 -0
  132. gini/runtime/cloudfabric_agent.py +370 -0
  133. gini/runtime/console.py +68 -0
  134. gini/runtime/control.py +70 -0
  135. gini/runtime/frame.py +138 -0
  136. gini/runtime/gbridge.py +638 -0
  137. gini/runtime/grouter.py +223 -0
  138. gini/runtime/hostsim.py +90 -0
  139. gini/runtime/shuttle.py +348 -0
  140. gini/runtime/switch.py +109 -0
  141. gini/runtime/transport.py +77 -0
  142. gini/runtime/xv6_bridge.py +312 -0
  143. gini/server/__init__.py +22 -0
  144. gini/server/__main__.py +74 -0
  145. gini/server/app.py +140 -0
  146. gini/server/auth.py +82 -0
  147. gini/server/policy.py +57 -0
  148. gini/server/session.py +23 -0
  149. gini/services/__init__.py +15 -0
  150. gini/services/boardflash.py +248 -0
  151. gini/services/boardsetup.py +374 -0
  152. gini/services/cloud_catalog.py +143 -0
  153. gini/services/compiler.py +1858 -0
  154. gini/services/discovery.py +324 -0
  155. gini/services/gloader.py +183 -0
  156. gini/services/orchestrator.py +1460 -0
  157. gini/services/persistence.py +28 -0
  158. gini/services/probe_runner.py +149 -0
  159. gini/services/project.py +217 -0
  160. gini/services/remote.py +93 -0
  161. gini/services/rider_runner.py +96 -0
  162. gini/services/rider_session.py +171 -0
  163. gini/services/shadow_store.py +52 -0
  164. gini/services/terminal.py +45 -0
  165. gini/setup/__init__.py +17 -0
  166. gini/setup/cli.py +109 -0
  167. gini/setup/images.py +33 -0
  168. gini/setup/marker.py +43 -0
  169. gini/setup/runtime.py +69 -0
  170. gini/ui/__init__.py +3 -0
  171. gini/ui/assets/app_icon.icns +0 -0
  172. gini/ui/assets/app_icon.ico +0 -0
  173. gini/ui/assets/app_icon.png +0 -0
  174. gini/ui/assets/app_icon_1024.png +0 -0
  175. gini/ui/assets/cue/_w.txt +1 -0
  176. gini/ui/assets/cue/ai.png +0 -0
  177. gini/ui/assets/cue/canvas.png +0 -0
  178. gini/ui/assets/cue/cloud.png +0 -0
  179. gini/ui/assets/cue/cost.png +0 -0
  180. gini/ui/assets/cue/dark/ai.png +0 -0
  181. gini/ui/assets/cue/dark/canvas.png +0 -0
  182. gini/ui/assets/cue/dark/cloud.png +0 -0
  183. gini/ui/assets/cue/dark/cost.png +0 -0
  184. gini/ui/assets/cue/dark/metrics.png +0 -0
  185. gini/ui/assets/cue/dark/router.png +0 -0
  186. gini/ui/assets/cue/dark/run.png +0 -0
  187. gini/ui/assets/cue/dark/serverless.png +0 -0
  188. gini/ui/assets/cue/dark/settings.png +0 -0
  189. gini/ui/assets/cue/dark/welcome.png +0 -0
  190. gini/ui/assets/cue/dark/wizard.png +0 -0
  191. gini/ui/assets/cue/ginibrand/ai.png +0 -0
  192. gini/ui/assets/cue/ginibrand/canvas.png +0 -0
  193. gini/ui/assets/cue/ginibrand/cloud.png +0 -0
  194. gini/ui/assets/cue/ginibrand/cost.png +0 -0
  195. gini/ui/assets/cue/ginibrand/metrics.png +0 -0
  196. gini/ui/assets/cue/ginibrand/router.png +0 -0
  197. gini/ui/assets/cue/ginibrand/run.png +0 -0
  198. gini/ui/assets/cue/ginibrand/serverless.png +0 -0
  199. gini/ui/assets/cue/ginibrand/settings.png +0 -0
  200. gini/ui/assets/cue/ginibrand/welcome.png +0 -0
  201. gini/ui/assets/cue/ginibrand/wizard.png +0 -0
  202. gini/ui/assets/cue/highcontrast/ai.png +0 -0
  203. gini/ui/assets/cue/highcontrast/canvas.png +0 -0
  204. gini/ui/assets/cue/highcontrast/cloud.png +0 -0
  205. gini/ui/assets/cue/highcontrast/cost.png +0 -0
  206. gini/ui/assets/cue/highcontrast/metrics.png +0 -0
  207. gini/ui/assets/cue/highcontrast/router.png +0 -0
  208. gini/ui/assets/cue/highcontrast/run.png +0 -0
  209. gini/ui/assets/cue/highcontrast/serverless.png +0 -0
  210. gini/ui/assets/cue/highcontrast/settings.png +0 -0
  211. gini/ui/assets/cue/highcontrast/welcome.png +0 -0
  212. gini/ui/assets/cue/highcontrast/wizard.png +0 -0
  213. gini/ui/assets/cue/light/ai.png +0 -0
  214. gini/ui/assets/cue/light/canvas.png +0 -0
  215. gini/ui/assets/cue/light/cloud.png +0 -0
  216. gini/ui/assets/cue/light/cost.png +0 -0
  217. gini/ui/assets/cue/light/metrics.png +0 -0
  218. gini/ui/assets/cue/light/router.png +0 -0
  219. gini/ui/assets/cue/light/run.png +0 -0
  220. gini/ui/assets/cue/light/serverless.png +0 -0
  221. gini/ui/assets/cue/light/settings.png +0 -0
  222. gini/ui/assets/cue/light/welcome.png +0 -0
  223. gini/ui/assets/cue/light/wizard.png +0 -0
  224. gini/ui/assets/cue/metrics.png +0 -0
  225. gini/ui/assets/cue/router.png +0 -0
  226. gini/ui/assets/cue/run.png +0 -0
  227. gini/ui/assets/cue/serverless.png +0 -0
  228. gini/ui/assets/cue/settings.png +0 -0
  229. gini/ui/assets/cue/welcome.png +0 -0
  230. gini/ui/assets/cue/wizard.png +0 -0
  231. gini/ui/assistant.py +2111 -0
  232. gini/ui/author_dialog.py +184 -0
  233. gini/ui/board_dialog.py +247 -0
  234. gini/ui/branding.py +21 -0
  235. gini/ui/canvas.py +2007 -0
  236. gini/ui/chat_panel.py +7 -0
  237. gini/ui/cpu_journey.py +212 -0
  238. gini/ui/cpu_lab.py +306 -0
  239. gini/ui/cue_cards.py +214 -0
  240. gini/ui/dashboard.py +222 -0
  241. gini/ui/diagnose_game.py +336 -0
  242. gini/ui/fingerprint_lab.py +219 -0
  243. gini/ui/flash_dialog.py +244 -0
  244. gini/ui/flow_layout.py +63 -0
  245. gini/ui/fragment_manager.py +1415 -0
  246. gini/ui/game_catalog.py +184 -0
  247. gini/ui/game_renderers.py +340 -0
  248. gini/ui/games_lab.py +90 -0
  249. gini/ui/inspector.py +1055 -0
  250. gini/ui/live_metrics.py +130 -0
  251. gini/ui/machine_lab.py +1412 -0
  252. gini/ui/main_window.py +3153 -0
  253. gini/ui/memory_lab.py +371 -0
  254. gini/ui/mission_panel.py +302 -0
  255. gini/ui/mode_indicator.py +227 -0
  256. gini/ui/palette.py +112 -0
  257. gini/ui/peripherals.py +218 -0
  258. gini/ui/process_tree.py +130 -0
  259. gini/ui/reset_dialog.py +179 -0
  260. gini/ui/router_lab.py +776 -0
  261. gini/ui/run_button.py +183 -0
  262. gini/ui/settings_dialog.py +234 -0
  263. gini/ui/signin_dialog.py +111 -0
  264. gini/ui/storage_lab.py +219 -0
  265. gini/ui/syscall_builder.py +235 -0
  266. gini/ui/syscall_lab.py +152 -0
  267. gini/ui/theme/__init__.py +5 -0
  268. gini/ui/theme/icons.py +145 -0
  269. gini/ui/theme/manager.py +291 -0
  270. gini/ui/theme/tokens.py +194 -0
  271. gini/ui/trap_lab.py +270 -0
  272. gini/ui/worker_host.py +102 -0
  273. gini/ui/zoo_lab.py +112 -0
  274. gini_toolkit-6.0.1.dev0.dist-info/METADATA +77 -0
  275. gini_toolkit-6.0.1.dev0.dist-info/RECORD +278 -0
  276. gini_toolkit-6.0.1.dev0.dist-info/WHEEL +5 -0
  277. gini_toolkit-6.0.1.dev0.dist-info/entry_points.txt +3 -0
  278. gini_toolkit-6.0.1.dev0.dist-info/top_level.txt +1 -0
gini/domain/devices.py ADDED
@@ -0,0 +1,747 @@
1
+ """GINI device / element taxonomy.
2
+
3
+ A single registry describes every kind of element a user can place on the canvas,
4
+ spanning classic computer-networking devices and the new cloud-computing primitives.
5
+ This is pure data with no Qt dependency so it can be driven by the UI, the compiler,
6
+ the persistence layer, and the AI agent layer alike.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from enum import Enum
12
+
13
+
14
+ class Category(str, Enum):
15
+ """Palette sections, in the order they appear.
16
+
17
+ MACHINES exists because the single most valuable comparison GINI can make was invisible: the
18
+ things that RUN YOUR CODE were scattered across three sections (Machine and Instance under
19
+ "Compute", Container under "Containers & Kubernetes", xv6 under "xv6"), so a student hunting
20
+ for "something to run this on" had to look in three places — and never saw that they were
21
+ looking at one family. They are: a container, a container with your image, a cloud VM, a real
22
+ microVM with its own kernel (Kata), and a real kernel on QEMU (xv6). Listed lightest-to-heaviest
23
+ (see PALETTE_RANK), the section itself teaches the isolation/weight tradeoff before the student
24
+ reads a word — and the startup-time stamp and cost meter then make it measurable.
25
+ """
26
+ NETWORKING = "Networking"
27
+ SDN = "Software-Defined Networking"
28
+ MACHINES = "Machines"
29
+ XV6 = "xv6 Peripherals" # attach only to an xv6 Machine (no networking)
30
+ CONTAINERS = "Containers & Kubernetes"
31
+ CLOUD_NETWORK = "Cloud Networking"
32
+ STORAGE = "Storage & Data"
33
+ STREAMING = "Streaming & Messaging"
34
+ OBSERVABILITY = "Observability"
35
+ WORKLOAD = "Workload & Testing"
36
+ SOURCE = "Sources" # stimulus riders — inject inputs into a donor element
37
+ SINK = "Sinks" # observer riders — read outputs off a donor element
38
+ SERVERLESS = "Serverless"
39
+ EXTERNAL = "External"
40
+ OS_ZOO = "OS Zoo" # play with real historical OSes (emulated, embedded noVNC)
41
+
42
+
43
+ # Within the Machines section, order is the ISOLATION LADDER — lightest first. This is the lesson:
44
+ # they all run your code, and they differ in how much of a machine they actually are.
45
+ PALETTE_RANK: dict[str, int] = {
46
+ "host": 1, # a Linux container on the fabric
47
+ "desktop": 2, # …the same, but it also runs a graphical desktop (headful)
48
+ "container": 3, # …the same, but you supply the image
49
+ "instance": 4, # a cloud VM (as the cloud presents it)
50
+ "kinstance": 5, # a REAL microVM — its own kernel (Kata)
51
+ "xv6": 6, # a real teaching kernel on QEMU-RISC-V
52
+ }
53
+
54
+
55
+ # Color-category keys; the theme maps each to a concrete accent color per theme.
56
+ class Accent(str, Enum):
57
+ BLUE = "blue" # routing / L3
58
+ GREEN = "green" # switching / L2
59
+ PURPLE = "purple" # compute / hosts
60
+ TEAL = "teal" # sdn
61
+ CYAN = "cyan" # containers
62
+ INDIGO = "indigo" # cloud networking
63
+ AMBER = "amber" # storage
64
+ PINK = "pink" # serverless
65
+ SLATE = "slate" # external / generic
66
+ ORANGE = "orange" # observability / monitoring
67
+ RED = "red" # workload / testing
68
+
69
+
70
+ @dataclass(frozen=True)
71
+ class DeviceType:
72
+ key: str
73
+ label: str
74
+ category: Category
75
+ icon: str
76
+ accent: Accent
77
+ description: str
78
+ backend_kind: str | None = None # compiler element, e.g. "vr", "vs", "vm"
79
+ is_container: bool = False # can visually/logically contain children
80
+ default_properties: dict[str, str] = field(default_factory=dict)
81
+ # properties that should render as a dropdown in the inspector: name -> choices
82
+ property_choices: dict[str, tuple[str, ...]] = field(default_factory=dict)
83
+ # Properties the element REPORTS rather than accepts: shown in the inspector but
84
+ # not editable, because the value is observed from the real world (e.g. a GINI32
85
+ # board's radio channel, which APSTA forces to match the uplink).
86
+ readonly_properties: tuple[str, ...] = ()
87
+ max_links: int | None = None # None = unlimited
88
+ hidden: bool = False # kept in the registry but off the palette
89
+ # Is this a CLOUD element (a managed service you rent) rather than a networking primitive you
90
+ # build? Stated explicitly, not inferred from the palette category — otherwise reorganising a UI
91
+ # section silently changes what the AI is told about an element, which is how a Message Queue
92
+ # ends up "not a cloud thing" because someone moved it next to Pub/Sub.
93
+ is_cloud: bool | None = None # None = fall back to the category default
94
+ # --- rider elements (Sources / Sinks) ------------------------------------ #
95
+ # A rider has NO container of its own: it runs as a process INSIDE a donor element (a Machine,
96
+ # Router, OVS…) via the donor's runtime. On the canvas it hangs off its donor by a dotted
97
+ # *attach* edge, not a network cable. `role` is stated, not inferred from the palette section.
98
+ rider: bool = False # True = runs on a donor, spawns no container
99
+ role: str = "" # "source" (injects input) | "sink" (reads output)
100
+ attaches_to: tuple[str, ...] = () # donor type_keys this rider may ride
101
+ driver: str = "" # how it runs: "docker-exec" | "grouter-cli" | "qemu-serial"
102
+
103
+ @property
104
+ def cloud(self) -> bool:
105
+ if self.is_cloud is not None:
106
+ return self.is_cloud
107
+ return self.category in (
108
+ Category.CONTAINERS,
109
+ Category.CLOUD_NETWORK,
110
+ Category.STORAGE,
111
+ Category.STREAMING,
112
+ Category.OBSERVABILITY,
113
+ Category.WORKLOAD,
114
+ Category.SERVERLESS,
115
+ )
116
+
117
+
118
+ # --------------------------------------------------------------------------- #
119
+ # The registry
120
+ # --------------------------------------------------------------------------- #
121
+
122
+ _DEVICES: list[DeviceType] = [
123
+ # ---- Classic networking -------------------------------------------------
124
+ DeviceType(
125
+ "router", "Router", Category.NETWORKING, "router", Accent.BLUE,
126
+ "Virtual router (Layer-3 forwarding between subnets).",
127
+ backend_kind="vr",
128
+ default_properties={"Name": "", "Forwarding": "true"},
129
+ ),
130
+ DeviceType(
131
+ "switch", "Switch", Category.NETWORKING, "switch", Accent.GREEN,
132
+ "Layer-2 learning switch.",
133
+ backend_kind="vs",
134
+ default_properties={"Name": "", "Priority": "100", "MAC": ""},
135
+ ),
136
+ DeviceType(
137
+ "hub", "Hub", Category.NETWORKING, "hub", Accent.GREEN,
138
+ "Layer-1 repeater hub (broadcasts to all ports).",
139
+ backend_kind="vs",
140
+ default_properties={"Name": ""},
141
+ ),
142
+ DeviceType(
143
+ "host", "Machine", Category.MACHINES, "host", Accent.PURPLE,
144
+ "Virtual host / end machine — a Linux container on the fabric.",
145
+ backend_kind="vm",
146
+ # Toolkit = which image this host is built from. LEAN (Alpine) is the default and the one
147
+ # to prefer: it has everything a student actually types (ip, ping, traceroute, tcpdump,
148
+ # dig, curl, nc, socat, iperf3, nmap) and is ~10x smaller, so topologies build and boot
149
+ # far faster on a modest laptop. Switch to FULL only for the experiments that need the
150
+ # heavy servers — bind9 (DNS), postfix (mail), ettercap/dsniff (spoofing), haproxy.
151
+ # NOTE: unrelated to the element's SIZE tier, which sets CPU and cost, not contents.
152
+ default_properties={"Name": "", "OS": "linux", "Interfaces": "1", "Toolkit": "lean"},
153
+ property_choices={"Toolkit": ("lean", "full", "security")},
154
+ is_cloud=False, # stated, not inferred from the palette section
155
+ ),
156
+ DeviceType(
157
+ "desktop", "Desktop", Category.MACHINES, "host", Accent.PINK,
158
+ "A HEADFUL machine — a real Linux host on the fabric (pingable, routable, all the usual "
159
+ "networking tools) that also runs a light graphical desktop (fluxbox, a file manager, a "
160
+ "terminal, and the Dillo browser). Double-click to open its screen in an embedded window "
161
+ "over noVNC. Use it when you want a GUI in the topology — e.g. browse a web server another "
162
+ "machine is serving. Heavier than a plain Machine (it carries an X stack), so reach for it "
163
+ "when you actually want the desktop.",
164
+ backend_kind="vm",
165
+ default_properties={"Name": "", "OS": "linux", "Interfaces": "1"},
166
+ is_cloud=False,
167
+ ),
168
+ DeviceType(
169
+ "xv6", "xv6 Machine", Category.MACHINES, "host", Accent.RED,
170
+ "A real teaching kernel: xv6 (MIT 6.1810) running on QEMU-RISC-V. Not a container — a "
171
+ "genuine OS you can watch and steer. Double-click it to open the Machine Lab: observe the "
172
+ "scheduler, process table, CPU registers, memory and kernel stack live, and slow the "
173
+ "time-slice to watch context switches. Runs standalone; xv6 has no networking, so instead "
174
+ "of network links you attach peripherals — a Terminal and a Storage Volume.",
175
+ backend_kind="xv6",
176
+ # vCPU count is the Size tier (S/M/L/XL -> -smp harts, capped at 2); no separate CPUs
177
+ # property (it was vestigial and contradicted the Size).
178
+ default_properties={"Name": "", "Timeslice": "1"},
179
+ property_choices={"Timeslice": ("1", "5", "10", "100")},
180
+ is_cloud=False, # stated, not inferred from the palette section
181
+ ),
182
+ # --- xv6 peripherals (software devices attached to the xv6 Machine) -------
183
+ DeviceType(
184
+ "terminal", "Terminal", Category.XV6, "dashboard", Accent.RED,
185
+ "A console for an xv6 Machine — one shell terminal (a screen and keyboard in one, like a "
186
+ "real tty). Connect it to an xv6 Machine and double-click to open it: type xv6 commands "
187
+ "(ls, cat, echo, spin 10 &, …) and watch their output inline. Up-arrow recalls history; "
188
+ "`help` lists what you can run.",
189
+ default_properties={"Name": ""},
190
+ max_links=1,
191
+ ),
192
+ DeviceType(
193
+ "storage_volume", "Storage Volume", Category.XV6, "database", Accent.RED,
194
+ "The xv6 disk. Connect it to an xv6 Machine and double-click to open the Storage view — "
195
+ "the on-disk layout, inodes, buffer cache and write-ahead log. (xv6 has a single custom "
196
+ "file system; alternate file systems are an advanced student project.)",
197
+ default_properties={"Name": "", "File system": "xv6fs"},
198
+ property_choices={"File system": ("xv6fs",)},
199
+ max_links=1,
200
+ ),
201
+ DeviceType(
202
+ "firewall", "Firewall", Category.NETWORKING, "firewall", Accent.BLUE,
203
+ "Packet-filtering firewall node.",
204
+ backend_kind="vr",
205
+ default_properties={"Name": "", "Policy": "default-deny"},
206
+ ),
207
+ DeviceType(
208
+ "vnf", "VNF (Service Function)", Category.NETWORKING, "controller", Accent.TEAL,
209
+ "A Virtualized Network Function: a container that runs a network function (firewall, "
210
+ "IDS, cache, shaper) and is inserted INLINE in the forwarding path — wire it between "
211
+ "two elements and traffic flows through it. Pick the function in 'Kind'; give its "
212
+ "config in 'Rules' (e.g. firewall: 'deny 10.0.3.0/24'; block: '10.0.3.5'). Chain "
213
+ "several in series (host → firewall → IDS → NAT) for a Service Function Chain (SFC).",
214
+ default_properties={"Name": "", "Kind": "firewall", "Rules": "deny 10.0.3.0/24"},
215
+ property_choices={"Kind": ("firewall", "block", "ids", "cache", "shaper")},
216
+ ),
217
+ DeviceType(
218
+ "wap", "Access Point", Category.NETWORKING, "wifi", Accent.GREEN,
219
+ "Wireless access point / mobile gateway.",
220
+ default_properties={"Name": "", "SSID": "gini"},
221
+ ),
222
+ DeviceType(
223
+ "cloud", "Internet", Category.EXTERNAL, "cloud", Accent.SLATE,
224
+ "External network / the Internet.",
225
+ # DNS lives HERE rather than on the things that use it, because name resolution
226
+ # is a property of "the outside world is reachable" — which is precisely what
227
+ # this element represents. Remove the element and DNS goes with it, which is the
228
+ # honest behaviour: there is nothing left to resolve names against.
229
+ # Handed to real devices on a GINI32 board's hotspot via DHCP; editable because
230
+ # some campus networks block public resolvers.
231
+ default_properties={"Name": "Internet", "DNS": "8.8.8.8"},
232
+ ),
233
+ # A real ESP32 board (GINI32) running the gBridge firmware: the one element on
234
+ # the palette that is not emulated at all. It stands for a physical radio on
235
+ # your desk that carries real devices — a phone, a Raspberry Pi, a sensor —
236
+ # into the drawn topology. The board finds the lab through the `gbridge` relay
237
+ # and is handed its fabric address from here, so the canvas stays the source of
238
+ # truth. BoardID must match the id flashed into the board (serial: `set id`).
239
+ DeviceType(
240
+ "gini32", "GINI32 Board", Category.EXTERNAL, "gini32", Accent.GREEN,
241
+ "A real ESP32 gateway board: carries physical devices into the emulated "
242
+ "topology over Wi-Fi (Ethernet-in-UDP via the gbridge relay).",
243
+ # BoardID names a PHYSICAL object, so it is never auto-generated: it is the
244
+ # sticker on the board (`gini32 provision --id gini-5`). Empty by default so
245
+ # two boards cannot silently collide on a shared default — an unset id is a
246
+ # visible error rather than one board vanishing from the relay's table.
247
+ # Everything else is per-RUN and assigned by the canvas: blank PhysicalSubnet
248
+ # and ApSSID mean "allocate me one". Channel is REPORTED, not set — in APSTA
249
+ # the hotspot is forced onto the uplink's channel.
250
+ default_properties={"Name": "", "BoardID": "", "Mode": "routed",
251
+ "ApSSID": "", "ApPassword": "gini12345",
252
+ "PhysicalSubnet": "", "Channel": ""},
253
+ property_choices={"Mode": ("routed", "nat")},
254
+ readonly_properties=("Channel",),
255
+ max_links=1,
256
+ is_cloud=False,
257
+ ),
258
+
259
+ # ---- Software-defined networking ----------------------------------------
260
+ DeviceType(
261
+ "ovs", "OpenVSwitch", Category.SDN, "ovs", Accent.TEAL,
262
+ "Open vSwitch programmable bridge.",
263
+ backend_kind="vs",
264
+ default_properties={"Name": "", "Datapath": "", "Protocol": "OpenFlow13"},
265
+ ),
266
+ DeviceType(
267
+ "controller", "OpenFlow Controller", Category.SDN, "controller", Accent.TEAL,
268
+ "SDN controller managing OpenFlow switches.",
269
+ default_properties={"Name": "", "Port": "6633",
270
+ "App": "gini.samples.switch"},
271
+ # the POX app that gives the switch its personality. The gini.samples.*
272
+ # apps are GINI's own (they clear the Flow Switch's match-all -> NORMAL
273
+ # default first, so the controller actually sees packet-ins); the
274
+ # forwarding.* / misc.* apps are stock POX. Runs in the controller container.
275
+ property_choices={"App": ("gini.samples.switch", "gini.samples.packet_loss",
276
+ "gini.samples.port_knock", "gini.samples.l4_lb",
277
+ "gini.samples.ids", "gini.samples.redirect",
278
+ "forwarding.l2_learning", "forwarding.hub",
279
+ "misc.of_tutorial")},
280
+ ),
281
+
282
+ # ---- Containers & Kubernetes --------------------------------------------
283
+ DeviceType(
284
+ "container", "Container", Category.MACHINES, "container", Accent.CYAN,
285
+ "A single Docker/OCI container.",
286
+ backend_kind="vm",
287
+ default_properties={"Name": "", "Image": "alpine:latest", "Command": ""},
288
+ is_cloud=True, # stated, not inferred from the palette section
289
+ ),
290
+ DeviceType(
291
+ "pod", "Pod", Category.CONTAINERS, "pod", Accent.CYAN,
292
+ "A Kubernetes workload — a Deployment of an image, run as N pod replicas. "
293
+ "Connect it to a K8s Cluster to deploy it there.",
294
+ is_container=True,
295
+ default_properties={"Name": "", "Image": "nginxdemos/hello:latest",
296
+ "Replicas": "2", "Port": "80"},
297
+ ),
298
+ DeviceType(
299
+ "k8s_node", "K8s Node", Category.CONTAINERS, "k8s_node", Accent.CYAN,
300
+ "A Kubernetes worker node (a k3s agent). v1 clusters are single-node; nodes are "
301
+ "shown for the model — multi-node scheduling is a follow-on.",
302
+ default_properties={"Name": "", "Role": "worker"},
303
+ # hidden from the palette for v1: a single-node cluster makes a separate Node
304
+ # element confusing next to 'K8s Cluster'. The type is retained so older saved
305
+ # projects still load and the compiler role keeps working. Re-expose with
306
+ # multi-node scheduling.
307
+ hidden=True,
308
+ ),
309
+ DeviceType(
310
+ "k8s_cluster", "K8s Cluster", Category.CONTAINERS, "k8s_cluster", Accent.CYAN,
311
+ "A real Kubernetes cluster (k3s in a container). Connect Pods to deploy them; add "
312
+ "a Pod Autoscaler (HPA) on a Pod to scale its replicas.",
313
+ is_container=True,
314
+ default_properties={"Name": "", "Version": "1.30"},
315
+ ),
316
+ DeviceType(
317
+ "registry", "Container Registry", Category.CONTAINERS, "registry", Accent.CYAN,
318
+ "Image registry serving container images.",
319
+ default_properties={"Name": "", "Endpoint": ""},
320
+ ),
321
+
322
+ # ---- Cloud networking (VPC / SG / LB) -----------------------------------
323
+ DeviceType(
324
+ "vpc", "VPC", Category.CLOUD_NETWORK, "vpc", Accent.INDIGO,
325
+ "Virtual private cloud — an isolated cloud network.",
326
+ is_container=True,
327
+ default_properties={"Name": "", "CIDR": "10.0.0.0/16", "Region": "us-east-1"},
328
+ ),
329
+ DeviceType(
330
+ "cloud_subnet", "Cloud Subnet", Category.CLOUD_NETWORK, "cloud_subnet", Accent.INDIGO,
331
+ "A subnet inside a VPC. Drop elements in it. A *public* subnet's members reach the "
332
+ "internet (and their consoles are reachable); a *private* subnet's members stay "
333
+ "inside the VPC only — reachable by other VPC members, but with no internet.",
334
+ is_container=True,
335
+ default_properties={"Name": "", "CIDR": "10.0.1.0/24", "Tier": "private"},
336
+ property_choices={"Tier": ("private", "public")},
337
+ ),
338
+ DeviceType(
339
+ "security_group", "Security Group", Category.CLOUD_NETWORK, "security_group", Accent.INDIGO,
340
+ "A stateful, default-deny firewall. Connect it to the workloads/datastores it "
341
+ "protects, then list inbound rules in Ingress (one per line): '<port> from <source>', "
342
+ "where source is a CIDR, 'anywhere', or another Security Group's name — e.g. "
343
+ "'80 from anywhere' or '5432 from app-sg'. Only listed ports open; outbound is allowed.",
344
+ default_properties={"Name": "", "Ingress": "80 from anywhere", "Egress": "allow-all"},
345
+ ),
346
+ DeviceType(
347
+ "gateway", "Gateway", Category.CLOUD_NETWORK, "gateway", Accent.INDIGO,
348
+ "Internet / NAT gateway connecting a VPC to the outside.",
349
+ default_properties={"Name": "", "Type": "internet"},
350
+ ),
351
+ DeviceType(
352
+ "load_balancer", "Load Balancer", Category.CLOUD_NETWORK, "load_balancer", Accent.INDIGO,
353
+ "Distributes traffic across backend targets.",
354
+ default_properties={"Name": "", "Scheme": "round-robin", "Listener": "80"},
355
+ property_choices={"Scheme": ("round-robin", "least_conn", "ip_hash")},
356
+ ),
357
+
358
+ # ---- Compute & autoscaling ----------------------------------------------
359
+ DeviceType(
360
+ "instance", "Instance", Category.MACHINES, "instance", Accent.PURPLE,
361
+ "Cloud compute instance (VM).",
362
+ backend_kind="vm",
363
+ default_properties={"Name": "", "Type": "t3.micro", "Image": "ubuntu-22.04"},
364
+ is_cloud=True, # stated, not inferred from the palette section
365
+ ),
366
+ DeviceType(
367
+ "kinstance", "Kata Instance (VM)", Category.MACHINES, "instance", Accent.PURPLE,
368
+ "A VM-isolated workload (Kata Containers): your container runs inside a lightweight "
369
+ "microVM with its own guest kernel — stronger isolation than a normal container, at "
370
+ "the cost of boot time, memory and I/O overhead. Use it to compare VM-vs-container "
371
+ "trade-offs. Needs a Kata-enabled GINI server backend (Settings - Backend).",
372
+ backend_kind="vm",
373
+ default_properties={"Name": "", "Image": "ubuntu:22.04", "Command": ""},
374
+ is_cloud=True, # stated, not inferred from the palette section
375
+ ),
376
+ DeviceType(
377
+ "instance_group", "Pod Autoscaler (HPA)", Category.CONTAINERS, "instance_group", Accent.CYAN,
378
+ "A Kubernetes Horizontal Pod Autoscaler. Connect it to a Pod to scale that "
379
+ "Deployment's replicas between Min and Max to hold a target CPU%. (This is the "
380
+ "HPA — different from the Cluster Autoscaler, which adds Nodes.)",
381
+ is_container=True,
382
+ default_properties={"Name": "", "Min": "1", "Max": "5", "TargetCPU": "60"},
383
+ ),
384
+ DeviceType(
385
+ "region", "Region / Zone", Category.CLOUD_NETWORK, "region", Accent.PURPLE,
386
+ "A cloud region or availability zone boundary.",
387
+ is_container=True,
388
+ default_properties={"Name": "us-east-1", "Zones": "a,b,c"},
389
+ ),
390
+
391
+ # ---- Storage & data ------------------------------------------------------
392
+ DeviceType(
393
+ "object_store", "Object Storage", Category.STORAGE, "object_store", Accent.AMBER,
394
+ "Object storage bucket (S3-style).",
395
+ default_properties={"Name": "", "Versioning": "off"},
396
+ ),
397
+ DeviceType(
398
+ "block_volume", "Block Volume", Category.STORAGE, "block_volume", Accent.AMBER,
399
+ "Attachable block storage volume.",
400
+ default_properties={"Name": "", "SizeGB": "20"},
401
+ ),
402
+ DeviceType(
403
+ "database", "Managed Database", Category.STORAGE, "database", Accent.AMBER,
404
+ "Managed relational / NoSQL database.",
405
+ default_properties={"Name": "", "Engine": "postgres", "Replicas": "0"},
406
+ ),
407
+
408
+ # ---- Serverless ----------------------------------------------------------
409
+ DeviceType(
410
+ "function", "Function", Category.SERVERLESS, "function", Accent.PINK,
411
+ "A serverless function (FaaS). Runs your handler on demand in a shared runtime — "
412
+ "no server to manage, scales per request, billed per invocation. Reachable over "
413
+ "HTTP at /<name>; front it with an API Gateway and drive it with a Load Generator.",
414
+ default_properties={"Name": "", "Runtime": "python3.12", "Handler": "echo",
415
+ "Code": ""},
416
+ property_choices={"Handler": ("echo", "transform", "slow", "fail", "counter",
417
+ "custom")},
418
+ ),
419
+ DeviceType(
420
+ "api_gateway", "API Gateway", Category.SERVERLESS, "api_gateway", Accent.PINK,
421
+ "The front door for your functions — a real Traefik edge router that maps a URL "
422
+ "path to each connected Function (/<name>). Connect it to Functions and it routes "
423
+ "automatically; open its dashboard to watch requests.",
424
+ default_properties={"Name": "", "Stage": "prod"},
425
+ ),
426
+ DeviceType(
427
+ "queue", "Message Queue", Category.STREAMING, "queue", Accent.PINK,
428
+ "Managed message queue / event bus.",
429
+ default_properties={"Name": "", "Type": "fifo"},
430
+ ),
431
+
432
+ # ---- Edge & traffic (proxies / web) -------------------------------------
433
+ DeviceType(
434
+ "proxy", "Reverse Proxy", Category.CLOUD_NETWORK, "proxy", Accent.INDIGO,
435
+ "Traefik reverse proxy / edge router with a live dashboard.",
436
+ default_properties={"Name": "", "Dashboard": "on"},
437
+ ),
438
+ DeviceType(
439
+ "web_app", "Web App", Category.CONTAINERS, "web_app", Accent.PURPLE,
440
+ "A small demo web backend that reports which instance served the request.",
441
+ default_properties={"Name": ""},
442
+ ),
443
+
444
+ # ---- Streaming & messaging ----------------------------------------------
445
+ DeviceType(
446
+ "stream", "Event Stream", Category.STREAMING, "stream", Accent.CYAN,
447
+ "Kafka-compatible event streaming log (Redpanda).",
448
+ default_properties={"Name": "", "Partitions": "1"},
449
+ ),
450
+ DeviceType(
451
+ "messaging", "Pub/Sub", Category.STREAMING, "messaging", Accent.CYAN,
452
+ "Lightweight NATS publish/subscribe messaging server.",
453
+ default_properties={"Name": ""},
454
+ ),
455
+
456
+ # ---- Storage: cache & NoSQL ---------------------------------------------
457
+ DeviceType(
458
+ "cache", "Cache", Category.STORAGE, "cache", Accent.AMBER,
459
+ "Redis in-memory key/value cache and store.",
460
+ default_properties={"Name": ""},
461
+ ),
462
+ DeviceType(
463
+ "nosql", "NoSQL Database", Category.STORAGE, "nosql", Accent.AMBER,
464
+ "MongoDB document database.",
465
+ default_properties={"Name": "", "Database": "app"},
466
+ ),
467
+
468
+ # ---- Observability ------------------------------------------------------
469
+ # Observability elements are SINKS (they observe outputs) — they live in the Sinks section now,
470
+ # but unlike the rider sinks they are real services with their own container (rider=False).
471
+ DeviceType(
472
+ "metrics", "Metrics", Category.SINK, "metrics", Accent.ORANGE,
473
+ "Prometheus metrics collection + PromQL query UI. A Sink: it scrapes and stores the numbers "
474
+ "your services emit. Its own container (not a rider) — wire it to the targets it scrapes.",
475
+ role="sink", is_cloud=True,
476
+ default_properties={"Name": ""},
477
+ ),
478
+ DeviceType(
479
+ "dashboard", "Dashboards", Category.SINK, "dashboard", Accent.ORANGE,
480
+ "Grafana dashboards over metrics and logs. A Sink that visualizes what Metrics collected — "
481
+ "wire it to a Metrics source. Its own container (not a rider).",
482
+ role="sink", is_cloud=True,
483
+ default_properties={"Name": ""},
484
+ ),
485
+ DeviceType(
486
+ "tracing", "Tracing", Category.SINK, "tracing", Accent.ORANGE,
487
+ "Jaeger distributed tracing (request timelines across services). A Sink: it observes request "
488
+ "flow across services. Its own container (not a rider).",
489
+ role="sink", is_cloud=True,
490
+ default_properties={"Name": ""},
491
+ ),
492
+
493
+ # ---- Load generation (a heavyweight Source: its own container) ----------
494
+ DeviceType(
495
+ "load_generator", "Load Generator", Category.SOURCE, "load_generator", Accent.RED,
496
+ "Fortio HTTP/gRPC load generator with a web UI to launch experiments. A Source that injects "
497
+ "sustained traffic — heavier than the HTTP Probe rider, with its own container. Wire it to "
498
+ "the backend/gateway you want to load.",
499
+ role="source", is_cloud=True,
500
+ default_properties={"Name": "", "QPS": "100", "Connections": "8"},
501
+ ),
502
+
503
+ # ---- Sources (stimulus riders — run ON a donor, no container of their own) ----
504
+ DeviceType(
505
+ "ping_probe", "Ping Probe", Category.SOURCE, "load_generator", Accent.RED,
506
+ "A ping (ICMP) stimulus. Attach it to a Machine or Router — its donor — and give it a "
507
+ "Target; double-click to start pinging INSIDE the donor (live RTT / loss in its Live tab), "
508
+ "double-click again to stop. It rides the donor (dotted attach edge, not a cable). "
509
+ "Count 0 = ping continuously until stopped; Count N = send N and stop.",
510
+ rider=True, role="source", driver="docker-exec",
511
+ attaches_to=("host", "router", "instance", "container"),
512
+ default_properties={"Name": "", "Target": "", "Count": "0"},
513
+ ),
514
+ DeviceType(
515
+ "http_probe", "HTTP Probe", Category.SOURCE, "load_generator", Accent.RED,
516
+ "An HTTP request stimulus (curl). Attach it to a Machine / Router donor and give it a "
517
+ "Target and Path; double-click to start requesting from inside the donor (live 2xx + "
518
+ "latency), double-click again to stop. Count 0 = request continuously; Count N = do N. "
519
+ "Rides the donor — no container of its own. For heavy sustained load use the Load Generator.",
520
+ rider=True, role="source", driver="docker-exec",
521
+ attaches_to=("host", "router", "instance", "container"),
522
+ default_properties={"Name": "", "Target": "", "Path": "/", "Count": "0"},
523
+ ),
524
+
525
+ # ---- Sinks (observer riders — run ON a donor, render its output) ----
526
+ DeviceType(
527
+ "packet_view", "Packet View", Category.SINK, "tracing", Accent.ORANGE,
528
+ "A live packet capture (tcpdump). Attach it to a Machine or Router — its donor; double-click "
529
+ "to start sniffing the donor's interface (live packet stream in its Live tab), double-click "
530
+ "again to stop. Count 0 = capture until stopped; Count N = stop after N packets. Captures "
531
+ "the GINI overlay (gini0) by default — set Interface to eth0/any for the Docker bridge. "
532
+ "Rides the donor — no container of its own.",
533
+ rider=True, role="sink", driver="docker-exec",
534
+ attaches_to=("host", "router", "ovs", "instance", "container"),
535
+ default_properties={"Name": "", "Interface": "gini0", "Filter": "", "Count": "0"},
536
+ ),
537
+ DeviceType(
538
+ "dns_probe", "DNS Probe", Category.SOURCE, "load_generator", Accent.RED,
539
+ "A name-resolution stimulus. Attach it to a Machine/Router donor, put the hostname to "
540
+ "resolve in Target (e.g. 'M2' or 'web'), and double-click to start; it resolves the name "
541
+ "from inside the donor — over the DRAWN network (gini0), because GINI writes peer names into "
542
+ "/etc/hosts — and reports the answer + resolve rate. Count 0 = query continuously; Count N = "
543
+ "do N. Rides the donor.",
544
+ rider=True, role="source", driver="docker-exec",
545
+ attaches_to=("host", "router", "instance", "container"),
546
+ default_properties={"Name": "", "Target": "", "Count": "0"},
547
+ ),
548
+ DeviceType(
549
+ "traceroute_probe", "Traceroute", Category.SOURCE, "load_generator", Accent.RED,
550
+ "A path-discovery stimulus (traceroute). Attach it to a Machine/Router donor and give it a "
551
+ "Target; it traces the hops to the target from inside the donor and reports the hop count. "
552
+ "Rides the donor — a good pair with a Packet View to watch each hop.",
553
+ rider=True, role="source", driver="docker-exec",
554
+ attaches_to=("host", "router", "instance", "container"),
555
+ default_properties={"Name": "", "Target": ""},
556
+ ),
557
+ DeviceType(
558
+ "iperf_client", "iPerf Client", Category.SOURCE, "load_generator", Accent.RED,
559
+ "A throughput generator (iperf3 -c). Attach it to a Machine donor and point Target at a "
560
+ "donor running an iPerf Server; it drives traffic and reports the measured bandwidth. "
561
+ "Seconds sets the test length; Bitrate caps the rate (e.g. 100M) so it doesn't saturate the "
562
+ "software fabric — set '0' for unlimited. Rides the donor.",
563
+ rider=True, role="source", driver="docker-exec",
564
+ attaches_to=("host", "instance", "container"),
565
+ default_properties={"Name": "", "Target": "", "Seconds": "10", "Bitrate": "100M"},
566
+ ),
567
+ DeviceType(
568
+ "iperf_server", "iPerf Server", Category.SINK, "tracing", Accent.ORANGE,
569
+ "A throughput endpoint (iperf3 -s). Attach it to a Machine donor; it listens for an iPerf "
570
+ "Client and reports the bandwidth it receives. Runs until stopped. Rides the donor.",
571
+ rider=True, role="sink", driver="docker-exec",
572
+ attaches_to=("host", "instance", "container"),
573
+ default_properties={"Name": ""},
574
+ ),
575
+ DeviceType(
576
+ "iface_stats", "Interface Stats", Category.SINK, "metrics", Accent.ORANGE,
577
+ "A live interface counter (reads /proc/net/dev). Attach it to a Machine/Router donor; it "
578
+ "streams rx/tx packet and byte counts so you can watch traffic volume. Rides the donor.",
579
+ rider=True, role="sink", driver="docker-exec",
580
+ attaches_to=("host", "router", "instance", "container"),
581
+ default_properties={"Name": ""},
582
+ ),
583
+
584
+ # ---- xv6 Sources (OS course) — ride the xv6 Machine over its console -----
585
+ DeviceType(
586
+ "xv6_shell", "Shell Probe", Category.SOURCE, "load_generator", Accent.RED,
587
+ "Launch a custom command into an xv6 Machine. Attach it to an xv6 Machine, put the command "
588
+ "in Command (e.g. 'ls', 'echo hi', 'cat README'); it types the command into the kernel's "
589
+ "console and streams the output back. The OS-course counterpart of the HTTP Probe.",
590
+ rider=True, role="source", driver="qemu-serial",
591
+ attaches_to=("xv6",),
592
+ default_properties={"Name": "", "Command": ""},
593
+ ),
594
+ DeviceType(
595
+ "xv6_workload", "Workload", Category.SOURCE, "load_generator", Accent.RED,
596
+ "Spawn a program on an xv6 Machine to drive the scheduler (the OS-course load generator). "
597
+ "Attach it to an xv6 Machine; set Program (e.g. 'spin', 'forktest', 'usertests') and Args. "
598
+ "Background = run with '&' so several can compete. Watch the effect in the Machine Lab.",
599
+ rider=True, role="source", driver="qemu-serial",
600
+ attaches_to=("xv6",),
601
+ default_properties={"Name": "", "Program": "spin", "Args": "", "Background": "true"},
602
+ property_choices={"Background": ("true", "false")},
603
+ ),
604
+
605
+ # ---- OS Zoo — real historical OSes under emulation, embedded via noVNC ------
606
+ DeviceType(
607
+ "freedos", "FreeDOS", Category.OS_ZOO, "host", Accent.ORANGE,
608
+ "The open, still-maintained MS-DOS-compatible OS — the command-line PC of the DOS era. "
609
+ "Boots out of the box; double-click to use it in an embedded screen.",
610
+ default_properties={"Name": "", "Persist": "false"},
611
+ property_choices={"Persist": ("false", "true")},
612
+ ),
613
+ DeviceType(
614
+ "kolibri", "KolibriOS", Category.OS_ZOO, "host", Accent.ORANGE,
615
+ "A tiny GUI operating system written entirely in assembly — the whole thing boots from a "
616
+ "single 1.44 MB floppy to a full graphical desktop in seconds, even under emulation. The "
617
+ "fast one to reach for.",
618
+ default_properties={"Name": "", "Persist": "false"},
619
+ property_choices={"Persist": ("false", "true")},
620
+ ),
621
+ DeviceType(
622
+ "menuet", "MenuetOS", Category.OS_ZOO, "host", Accent.ORANGE,
623
+ "The assembly GUI OS that KolibriOS forked from — a complete graphical desktop with apps "
624
+ "on a single 1.44 MB floppy (the 32-bit build is open source). Boots in seconds under "
625
+ "emulation, just like KolibriOS.",
626
+ default_properties={"Name": "", "Persist": "false"},
627
+ property_choices={"Persist": ("false", "true")},
628
+ ),
629
+ DeviceType(
630
+ "msdos", "MS-DOS 6.22", Category.OS_ZOO, "host", Accent.PURPLE,
631
+ "The real Microsoft MS-DOS 6.22, booted from a disk image under QEMU (so it's the genuine "
632
+ "article — `VER` reports MS-DOS, not a clone). Drag it on and Run — GINI downloads a public "
633
+ "pre-installed MS-DOS 6.22 disk on first boot (GINI ships nothing proprietary) and boots to "
634
+ "the C:\\> prompt. Put it next to FreeDOS to compare the original with the open re-creation. "
635
+ "The Image URL is pre-filled and editable. Ephemeral unless Persist is on.",
636
+ default_properties={
637
+ "Name": "", "Emulator": "qemu", "Arch": "x86",
638
+ "Image": "https://archive.org/download/pre-installed-ms-dos-622-disk-image/"
639
+ "Installed%20MS-DOS%206.22.img",
640
+ "Persist": "false"},
641
+ property_choices={"Persist": ("false", "true")},
642
+ ),
643
+ DeviceType(
644
+ "mac7", "Mac System 7", Category.OS_ZOO, "host", Accent.PURPLE,
645
+ "Classic Macintosh System 7 on an emulated 68k Mac (Basilisk II). Just drag it on and Run — "
646
+ "GINI downloads a Quadra ROM and a bootable System 7.5.3 disk from a public archive on first "
647
+ "boot (GINI ships nothing proprietary) and boots to the Mac desktop. The Image/Rom URLs are "
648
+ "pre-filled; change them to point at files you prefer. Ephemeral unless Persist is on.",
649
+ default_properties={
650
+ "Name": "", "Emulator": "basilisk", "Arch": "68k",
651
+ "Image": "https://archive.org/download/system-753/System753.dsk",
652
+ "Rom": "https://archive.org/download/mac_rom_archive_-_as_of_8-19-2011/"
653
+ "mac_rom_archive_-_as_of_8-19-2011.zip/"
654
+ "F1ACAD13%20-%20Quadra%20610%2C650%2Cmaybe%20800.ROM",
655
+ "Persist": "false"},
656
+ property_choices={"Persist": ("false", "true")},
657
+ ),
658
+ DeviceType(
659
+ "win31", "Windows 3.11", Category.OS_ZOO, "host", Accent.PURPLE,
660
+ "Windows for Workgroups 3.11 under DOSBox — the fast vintage-Windows path. Just drag it on "
661
+ "and Run — GINI downloads a public, pre-installed Windows 3.11, mounts it as C:, and starts "
662
+ "Windows (GINI ships nothing proprietary). The Image URL is pre-filled; change it to point "
663
+ "at a folder or zip you prefer. Ephemeral unless Persist is on.",
664
+ default_properties={
665
+ "Name": "", "Emulator": "dosbox", "Arch": "x86",
666
+ "Image": "https://archive.org/download/win3_stock/win311-stock.zip",
667
+ "Persist": "false"},
668
+ property_choices={"Persist": ("false", "true")},
669
+ ),
670
+ DeviceType(
671
+ "oszoo_byo", "Classic OS (your image)", Category.OS_ZOO, "host", Accent.SLATE,
672
+ "Bring your own OS: run a proprietary classic OS that GINI can't legally ship. Pick the "
673
+ "emulator — QEMU (Windows 95/98, or any bootable x86 disk), DOSBox (DOS and Windows 3.x, "
674
+ "the fast path), or Basilisk II (classic 68k Mac — System 7 / Mac OS 8). Set 'Image' to a "
675
+ "disk image (or folder for DOSBox) you own, and 'Rom' to a Macintosh ROM for Basilisk. "
676
+ "GINI provides the emulator and hosts nothing — you supply the image.",
677
+ default_properties={"Name": "", "Emulator": "qemu", "Image": "", "Rom": "", "Arch": "x86"},
678
+ property_choices={"Emulator": ("qemu", "dosbox", "basilisk"),
679
+ "Arch": ("x86", "x86_64", "68k")},
680
+ ),
681
+ ]
682
+
683
+ # Lookup table
684
+ REGISTRY: dict[str, DeviceType] = {d.key: d for d in _DEVICES}
685
+
686
+ # Auto-name prefixes per element type (R1, S1, M1, …). Curated to be short and
687
+ # collision-free (e.g. Metrics is PROM, not M, so it never clashes with Machine).
688
+ # Users can override the popular ones in Settings → Naming.
689
+ DEFAULT_PREFIXES: dict[str, str] = {
690
+ # networking
691
+ "router": "R", "switch": "S", "hub": "H", "host": "M", "firewall": "FW",
692
+ "wap": "AP", "cloud": "NET", "vnf": "VNF", "gini32": "GB",
693
+ # sdn
694
+ "ovs": "OVS", "controller": "OFC",
695
+ # compute / containers
696
+ "instance": "I", "container": "CT", "web_app": "WA", "pod": "POD",
697
+ "k8s_node": "KN", "k8s_cluster": "K8S", "registry": "REG",
698
+ "instance_group": "HPA", "region": "RGN",
699
+ # cloud networking
700
+ "vpc": "VPC", "cloud_subnet": "CSUB", "security_group": "SG",
701
+ "gateway": "GW", "load_balancer": "LB", "proxy": "PXY",
702
+ # storage & data
703
+ "object_store": "OBJ", "block_volume": "VOL", "database": "DB",
704
+ "cache": "CA", "nosql": "NDB",
705
+ # streaming & messaging
706
+ "stream": "STR", "messaging": "MSG", "queue": "Q",
707
+ # observability
708
+ "metrics": "PROM", "dashboard": "GRAF", "tracing": "JGR",
709
+ # workload / serverless
710
+ "load_generator": "LG", "function": "FN", "api_gateway": "AGW",
711
+ # sources / sinks (riders)
712
+ "ping_probe": "PING", "http_probe": "HTTP", "packet_view": "PCAP",
713
+ "dns_probe": "DNS", "traceroute_probe": "TRACE", "iperf_client": "IPERFC",
714
+ "iperf_server": "IPERFS", "iface_stats": "IFSTAT",
715
+ "xv6_shell": "SH", "xv6_workload": "WL",
716
+ }
717
+
718
+
719
+ def default_prefix(type_key: str) -> str:
720
+ dt = REGISTRY.get(type_key)
721
+ if type_key in DEFAULT_PREFIXES:
722
+ return DEFAULT_PREFIXES[type_key]
723
+ if dt is not None: # fallback: capitals of the label, e.g. VPC
724
+ return "".join(c for c in dt.label if c.isupper()) or dt.label[:2].upper()
725
+ return "N"
726
+
727
+
728
+ def all_devices() -> list[DeviceType]:
729
+ return list(_DEVICES)
730
+
731
+
732
+ def get(key: str) -> DeviceType:
733
+ return REGISTRY[key]
734
+
735
+
736
+ def by_category() -> dict[Category, list[DeviceType]]:
737
+ """The palette, section by section. Sections come out in Category order; within a section,
738
+ PALETTE_RANK wins (that's what puts Machines in the isolation ladder — container first, real
739
+ kernel last), and anything unranked keeps its registry order."""
740
+ out: dict[Category, list[DeviceType]] = {c: [] for c in Category}
741
+ for d in _DEVICES:
742
+ if d.hidden: # retained in REGISTRY but kept off the palette
743
+ continue
744
+ out[d.category].append(d)
745
+ for items in out.values():
746
+ items.sort(key=lambda d: PALETTE_RANK.get(d.key, 99)) # stable: unranked keep their order
747
+ return {c: items for c, items in out.items() if items}