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
@@ -0,0 +1,201 @@
1
+ """Process fingerprints — a behavioral signature per process, from REAL kernel telemetry.
2
+
3
+ A fingerprint is a small feature vector over five axes, each derived from data the Machine Lab
4
+ already collects (no simulation, no LLM judgement):
5
+
6
+ cpu — fraction of samples the proc wanted the CPU (RUNNING/RUNNABLE), from procdump
7
+ io_wait — fraction of samples SLEEPING (blocked on IO / wait / pipe), from procdump
8
+ syscalls — syscall rate, from the syscall trace ring (per-pid TRACE entries)
9
+ faults — page-fault rate, from the trap-taxonomy ring (per-pid pagefault traps)
10
+ forks — fork() rate, from the syscall ring (num == SYS_fork)
11
+
12
+ Two layers sit on top, both pure/deterministic and testable:
13
+
14
+ • classify(fp) — a threshold rule mapping a fingerprint to a behavior CLASS. This is the
15
+ baseline the "classify game" grades a student against (or that a student
16
+ tunes as an assignment).
17
+ • confusion_matrix(...) — scores predicted-vs-true classes. GROUND_TRUTH is the oracle's knowledge
18
+ of what each shipped program really is; it grades, it never classifies.
19
+ """
20
+ from __future__ import annotations
21
+
22
+ import math
23
+ from dataclasses import dataclass
24
+
25
+ SYS_FORK = 1 # xv6 syscall number for fork()
26
+ TRAP_PAGEFAULT = 1 # trap-taxonomy kind index for a page fault
27
+
28
+ # the fingerprint axes, in radar order
29
+ FEATURE_AXES = ["cpu", "syscalls", "io_wait", "faults", "forks"]
30
+ AXIS_LABEL = {"cpu": "CPU", "syscalls": "syscalls", "io_wait": "IO wait",
31
+ "faults": "faults", "forks": "forks"}
32
+
33
+ # behavior classes a fingerprint can be sorted into
34
+ CLASSES = ["cpu-bound", "io-bound", "memory", "fork-heavy", "mixed"]
35
+
36
+ # the oracle's ground truth — what each launchable program ACTUALLY is. Used only to grade the
37
+ # classify game; never shown to the classifier. (grind forks + does file/pipe work, so it can
38
+ # honestly be confused with io-bound — that overlap is the lesson.)
39
+ GROUND_TRUTH = {
40
+ "spin": "cpu-bound", "busy": "cpu-bound",
41
+ "alloc": "memory", "writer": "io-bound",
42
+ "forktest": "fork-heavy", "grind": "fork-heavy",
43
+ }
44
+
45
+ # saturation constants: rate at which each axis reaches ~0.5 (via x/(x+k)). Tuned so the shipped
46
+ # programs land in distinct regions; documented so a student can reason about them.
47
+ _K_SYS, _K_FLT, _K_FORK = 20.0, 5.0, 2.0
48
+
49
+ # default classifier thresholds — a student assignment can tune these and watch the confusion move.
50
+ DEFAULT_THRESHOLDS = {"fork": 0.4, "flt": 0.3, "io": 0.35, "sys": 0.4, "cpu": 0.5}
51
+
52
+
53
+ @dataclass
54
+ class ProcFeatures:
55
+ """Raw per-process tallies accumulated over a window. All fields come from real dumps."""
56
+ pid: int
57
+ name: str = ""
58
+ samples: int = 0 # procdump samples observed
59
+ run_samples: int = 0 # ...RUNNING or RUNNABLE (wanted the CPU)
60
+ sleep_samples: int = 0 # ...SLEEPING (blocked)
61
+ syscalls: int = 0 # syscall trace events attributed to this pid
62
+ forks: int = 0 # ...of which fork()
63
+ faults: int = 0 # page-fault traps
64
+ window: float = 1.0 # seconds the tallies span (for rates)
65
+
66
+
67
+ def _sat(x: float, k: float) -> float:
68
+ """Saturating normalizer x/(x+k) -> 0..1: bounded and stable frame to frame."""
69
+ return x / (x + k) if x > 0 else 0.0
70
+
71
+
72
+ def fingerprint(f: ProcFeatures) -> dict:
73
+ """A ProcFeatures -> the 0..1 feature vector (the fingerprint)."""
74
+ w = max(f.window, 1e-6)
75
+ s = max(f.samples, 1)
76
+ return {
77
+ "cpu": min(1.0, f.run_samples / s),
78
+ "io_wait": min(1.0, f.sleep_samples / s),
79
+ "syscalls": _sat(f.syscalls / w, _K_SYS),
80
+ "faults": _sat(f.faults / w, _K_FLT),
81
+ "forks": _sat(f.forks / w, _K_FORK),
82
+ }
83
+
84
+
85
+ def classify(fp: dict, thresholds: dict | None = None) -> str:
86
+ """Map a fingerprint to a behavior class by a simple, inspectable rule. Order matters: the most
87
+ distinctive signal wins first. Thresholds are tunable (the assignment)."""
88
+ th = {**DEFAULT_THRESHOLDS, **(thresholds or {})}
89
+ if fp["forks"] >= th["fork"]:
90
+ return "fork-heavy"
91
+ if fp["faults"] >= th["flt"]:
92
+ return "memory"
93
+ if fp["io_wait"] >= th["io"] or (fp["syscalls"] >= th["sys"] and fp["cpu"] < th["cpu"]):
94
+ return "io-bound"
95
+ if fp["cpu"] >= th["cpu"]:
96
+ return "cpu-bound"
97
+ return "mixed"
98
+
99
+
100
+ def similarity(a: dict, b: dict) -> float:
101
+ """Cosine similarity of two fingerprints (0..1) — 'who behaves like whom' for the board."""
102
+ dot = sum(a[k] * b[k] for k in FEATURE_AXES)
103
+ na = math.sqrt(sum(a[k] ** 2 for k in FEATURE_AXES))
104
+ nb = math.sqrt(sum(b[k] ** 2 for k in FEATURE_AXES))
105
+ return dot / (na * nb) if na > 0 and nb > 0 else 0.0
106
+
107
+
108
+ def scatter_xy(fp: dict) -> tuple[float, float]:
109
+ """Project a fingerprint onto the behavior map: x = CPU-bound(1) <-> IO-bound(0),
110
+ y = compute(0) <-> heavy-kernel(1, syscalls+faults+forks). Both 0..1."""
111
+ x = max(0.0, min(1.0, 0.5 + 0.5 * (fp["cpu"] - fp["io_wait"])))
112
+ y = max(0.0, min(1.0, (fp["syscalls"] + fp["faults"] + fp["forks"]) / 3.0))
113
+ return x, y
114
+
115
+
116
+ def true_class(name: str) -> str | None:
117
+ """The oracle's ground-truth class for a program name (None if unknown)."""
118
+ return GROUND_TRUTH.get((name or "").strip())
119
+
120
+
121
+ def confusion_matrix(pairs, classes=None) -> dict:
122
+ """[(true_class, predicted_class), …] -> {(true, pred): count}. Off-diagonal = confusion."""
123
+ classes = classes or CLASSES
124
+ m = {(t, p): 0 for t in classes for p in classes}
125
+ for true, pred in pairs:
126
+ if (true, pred) in m:
127
+ m[(true, pred)] += 1
128
+ return m
129
+
130
+
131
+ def accuracy(pairs) -> float:
132
+ """Fraction of (true, pred) pairs on the diagonal."""
133
+ pairs = list(pairs)
134
+ if not pairs:
135
+ return 0.0
136
+ return sum(1 for t, p in pairs if t == p) / len(pairs)
137
+
138
+
139
+ class FingerprintAccumulator:
140
+ """Builds per-pid ProcFeatures from a stream of observations. Pure bookkeeping — the UI feeds it
141
+ procdump states each poll plus the NEW syscall/trap ring events since the last poll."""
142
+
143
+ def __init__(self) -> None:
144
+ self.feats: dict[int, ProcFeatures] = {}
145
+
146
+ def reset(self) -> None:
147
+ self.feats.clear()
148
+
149
+ def _get(self, pid: int, name: str = "") -> ProcFeatures:
150
+ f = self.feats.get(pid)
151
+ if f is None:
152
+ f = ProcFeatures(pid=pid, name=name)
153
+ self.feats[pid] = f
154
+ if name:
155
+ f.name = name
156
+ return f
157
+
158
+ def observe(self, procs, sc_events=(), trap_events=(), dt: float = 0.5) -> None:
159
+ """One poll: `procs` are the current [Proc]; `sc_events`/`trap_events` are the NEW ring
160
+ entries since the last poll (SyscallEvent / TrapEvent). `dt` advances every proc's window."""
161
+ for p in procs:
162
+ f = self._get(p.pid, p.name)
163
+ f.samples += 1
164
+ if p.state in ("running", "runnable"):
165
+ f.run_samples += 1
166
+ elif p.state == "sleeping":
167
+ f.sleep_samples += 1
168
+ f.window += dt
169
+ for e in sc_events:
170
+ f = self.feats.get(e.pid)
171
+ if f is not None:
172
+ f.syscalls += 1
173
+ if e.num == SYS_FORK:
174
+ f.forks += 1
175
+ for e in trap_events:
176
+ f = self.feats.get(e.pid)
177
+ if f is not None and e.kind == TRAP_PAGEFAULT:
178
+ f.faults += 1
179
+
180
+ def fingerprints(self, min_samples: int = 3) -> dict:
181
+ """{pid: fingerprint} for procs with enough samples to be meaningful (else 'settling')."""
182
+ return {pid: fingerprint(f) for pid, f in self.feats.items() if f.samples >= min_samples}
183
+
184
+
185
+ def demo_features() -> list:
186
+ """Canned but realistic fingerprints for the shipped programs, so the panel + game work offline
187
+ (and drive the pure tests). Tallies chosen to match GROUND_TRUTH under the default classifier."""
188
+ return [
189
+ ProcFeatures(pid=4, name="spin", samples=20, run_samples=20, sleep_samples=0,
190
+ syscalls=1, forks=0, faults=0, window=10),
191
+ ProcFeatures(pid=5, name="busy", samples=20, run_samples=20, sleep_samples=0,
192
+ syscalls=2, forks=0, faults=0, window=10),
193
+ ProcFeatures(pid=6, name="alloc", samples=20, run_samples=14, sleep_samples=6,
194
+ syscalls=50, forks=0, faults=48, window=10),
195
+ ProcFeatures(pid=7, name="writer", samples=20, run_samples=6, sleep_samples=14,
196
+ syscalls=200, forks=0, faults=0, window=10),
197
+ ProcFeatures(pid=8, name="grind", samples=20, run_samples=10, sleep_samples=10,
198
+ syscalls=180, forks=40, faults=5, window=10),
199
+ ProcFeatures(pid=9, name="forktest", samples=20, run_samples=12, sleep_samples=8,
200
+ syscalls=60, forks=50, faults=0, window=10),
201
+ ]
@@ -0,0 +1,34 @@
1
+ """Firewall rules -> gRouter ACL deploy commands (the firewall face of the Router Lab).
2
+
3
+ A GINI Firewall is a gRouter with an ACL data-plane module. A rule `deny <cidr>` (or a bare
4
+ CIDR) becomes `gpipe add acl <cidr>` on the real router; deploying clears the pipeline then
5
+ adds each rule in order — the same live-deploy path as the Service Function Chain, just a
6
+ firewall-shaped front-end. Pure/text-only, so it's unit-tested without Docker.
7
+ """
8
+ from __future__ import annotations
9
+
10
+ import re
11
+
12
+ _CIDR = re.compile(r"^\d{1,3}(?:\.\d{1,3}){3}(?:/\d{1,2})?$")
13
+
14
+
15
+ def parse_rules(text: str) -> list[tuple[str, str]]:
16
+ """[(action, target)] from a rules block — one `deny <cidr>` (or bare CIDR) per line.
17
+ Blank lines and `#` comments are ignored; unrecognised lines are skipped."""
18
+ out: list[tuple[str, str]] = []
19
+ for ln in (text or "").splitlines():
20
+ s = ln.strip()
21
+ if not s or s.startswith("#"):
22
+ continue
23
+ toks = s.split()
24
+ if toks[0].lower() in ("deny", "drop", "block") and len(toks) >= 2 and _CIDR.match(toks[1]):
25
+ out.append(("deny", toks[1]))
26
+ elif _CIDR.match(toks[0]):
27
+ out.append(("deny", toks[0]))
28
+ return out
29
+
30
+
31
+ def deploy_commands(text: str) -> list[str]:
32
+ """The `gpipe` arg-lines that program this firewall's ACL into the running gRouter:
33
+ `clear`, then `add acl <cidr>` per deny rule, in order."""
34
+ return ["clear"] + [f"add acl {target}" for _action, target in parse_rules(text)]
gini/domain/flowlog.py ADDED
@@ -0,0 +1,61 @@
1
+ """Flow event log — accumulate the controller's flow-rule installs/expiries over time.
2
+
3
+ The live flow table is only a snapshot: a reactive controller installs exact-match rules for
4
+ learned conversations and they expire after an idle timeout, so at any instant you see only a
5
+ few. To show the FULL picture of what the controller programmed, `FlowLog` diffs successive
6
+ snapshots (the dashboard polls `openflow entry all` every ~2.5s): a rule present now but not
7
+ before is an "installed" event; one present before but not now is an "expired" event. This
8
+ captures every rule that lived at least one poll — no backend change needed.
9
+
10
+ Rules are identified by their match+action (the flow's identity), since the numeric slot
11
+ index gets reused. Pure/duck-typed (rows just need `match_summary()`/`action_summary()` and
12
+ `.packets`), so it's unit-tested without Qt or Docker.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import time
17
+ from dataclasses import dataclass
18
+
19
+
20
+ @dataclass
21
+ class FlowEvent:
22
+ kind: str # "installed" | "expired"
23
+ match: str
24
+ action: str
25
+ packets: int | None # at install: ~0; at expire: the final count the rule handled
26
+ when: str # HH:MM:SS
27
+
28
+
29
+ class FlowLog:
30
+ def __init__(self, cap: int = 300) -> None:
31
+ self._prev: dict[str, dict] = {} # key -> {match, action, packets}
32
+ self.events: list[FlowEvent] = [] # oldest first
33
+ self.cap = cap
34
+
35
+ def update(self, rows, now: str | None = None) -> list[FlowEvent]:
36
+ """Fold a fresh snapshot in; return the events newly detected this poll."""
37
+ ts = now or time.strftime("%H:%M:%S")
38
+ cur: dict[str, dict] = {}
39
+ for f in rows or []:
40
+ match, action = f.match_summary(), f.action_summary()
41
+ cur[f"{match} ⇒ {action}"] = {"match": match, "action": action,
42
+ "packets": getattr(f, "packets", None)}
43
+ fresh: list[FlowEvent] = []
44
+ for key, v in cur.items():
45
+ if key not in self._prev:
46
+ fresh.append(FlowEvent("installed", v["match"], v["action"], v["packets"], ts))
47
+ for key, v in self._prev.items():
48
+ if key not in cur: # gone this poll -> expired/removed
49
+ fresh.append(FlowEvent("expired", v["match"], v["action"], v["packets"], ts))
50
+ self._prev = cur
51
+ self.events.extend(fresh)
52
+ if len(self.events) > self.cap:
53
+ self.events = self.events[-self.cap:]
54
+ return fresh
55
+
56
+ def recent(self, n: int = 200) -> list[FlowEvent]:
57
+ """Most-recent events first (for display)."""
58
+ return list(reversed(self.events[-n:]))
59
+
60
+ def clear(self) -> None:
61
+ self._prev, self.events = {}, []
@@ -0,0 +1,179 @@
1
+ """OpenFlow flow-table parsing — turn the gRouter's `openflow …` CLI dumps into rows the
2
+ SDN dashboard can render.
3
+
4
+ The OVS element is the C gRouter in `--openflow` mode; its CLI (reachable at runtime via
5
+ `element_query(ovs, "openflow …")`) prints the live flow table:
6
+
7
+ • `openflow entry all` -> match fields + actions per active entry
8
+ • `openflow stats entry all` -> match + packet/byte counts + duration per active entry
9
+
10
+ Both share the block format (delimited by `=====\nEntry N\n=====`, match fields one tab in,
11
+ actions two tabs in). This module parses either, and `flows(entry_dump, stats_dump)` merges
12
+ them by index into `FlowEntry` rows. Pure/text-only, so it's fully unit-tested without Docker.
13
+ """
14
+ from __future__ import annotations
15
+
16
+ import re
17
+ from dataclasses import dataclass, field
18
+
19
+ _ENTRY_RE = re.compile(r"^Entry\s+(\d+)\s*$")
20
+ _OFPP = {"OFPP_FLOOD": "flood", "OFPP_CONTROLLER": "controller", "OFPP_NORMAL": "normal",
21
+ "OFPP_ALL": "all", "OFPP_IN_PORT": "in_port", "OFPP_LOCAL": "local",
22
+ "OFPP_TABLE": "table", "OFPP_NONE": "none"}
23
+
24
+
25
+ @dataclass
26
+ class FlowEntry:
27
+ index: int
28
+ match: dict = field(default_factory=dict) # e.g. {"Input port": "1", "IP protocol": "TCP"}
29
+ actions: list = field(default_factory=list) # e.g. ["output:2", "flood"]
30
+ priority: int | None = None
31
+ packets: int | None = None
32
+ bytes: int | None = None
33
+ duration: int | None = None
34
+ idle_timeout: int | None = None
35
+ hard_timeout: int | None = None
36
+
37
+ def match_summary(self) -> str:
38
+ m = self.match
39
+ parts = []
40
+ if m.get("Input port"):
41
+ parts.append("in:" + m["Input port"])
42
+ src, dst = m.get("IP source address"), m.get("IP destination address")
43
+ smac, dmac = (m.get("Ethernet source MAC address"),
44
+ m.get("Ethernet destination MAC address"))
45
+ if src or dst:
46
+ parts.append(f"{src or '*'}→{dst or '*'}")
47
+ elif smac or dmac:
48
+ parts.append(f"{smac or '*'}→{dmac or '*'}")
49
+ kind = m.get("IP protocol") or m.get("Ethernet frame type")
50
+ if kind:
51
+ parts.append(kind.lower())
52
+ dport = m.get("TCP destination port") or m.get("UDP destination port")
53
+ if dport:
54
+ parts.append("dport " + dport)
55
+ return " ".join(parts) or "any (wildcard)"
56
+
57
+ def action_summary(self) -> str:
58
+ return ", ".join(self.actions) or "—"
59
+
60
+
61
+ def _int(v):
62
+ try:
63
+ return int(v)
64
+ except (TypeError, ValueError):
65
+ return None
66
+
67
+
68
+ def _split_blocks(text: str):
69
+ """Yield (index, [lines]) per `Entry N` block, keeping raw lines (with tabs)."""
70
+ idx, buf = None, []
71
+ for raw in (text or "").splitlines():
72
+ line = raw.rstrip("\n")
73
+ if set(line.strip()) == {"="} and line.strip(): # a `=====` divider
74
+ continue
75
+ m = _ENTRY_RE.match(line.strip())
76
+ if m:
77
+ if idx is not None:
78
+ yield idx, buf
79
+ idx, buf = int(m.group(1)), []
80
+ elif idx is not None:
81
+ buf.append(line)
82
+ if idx is not None:
83
+ yield idx, buf
84
+
85
+
86
+ def _port_name(v: str) -> str:
87
+ v = v.strip()
88
+ if v in _OFPP:
89
+ return _OFPP[v]
90
+ if v.startswith("OFPP_"):
91
+ return v[5:].lower()
92
+ return f"output:{v}"
93
+
94
+
95
+ def parse(text: str) -> list[FlowEntry]:
96
+ """Parse one dump (`entry all` or `stats entry all`) into active FlowEntry rows."""
97
+ out: list[FlowEntry] = []
98
+ for index, lines in _split_blocks(text):
99
+ if any(l.strip() == "Entry inactive" for l in lines):
100
+ continue
101
+ e = FlowEntry(index=index)
102
+ section = None
103
+ for line in lines:
104
+ depth = len(line) - len(line.lstrip("\t"))
105
+ s = line.strip()
106
+ if not s:
107
+ continue
108
+ if depth == 0:
109
+ if s == "Match:":
110
+ section = "match"; continue
111
+ if s == "Actions:":
112
+ section = "actions"; continue
113
+ section = None # a top-level field ends the block
114
+ key, _, val = s.partition(":")
115
+ key, val = key.strip(), val.strip()
116
+ if key == "Priority":
117
+ e.priority = _int(val)
118
+ elif key == "Packet count":
119
+ e.packets = _int(val)
120
+ elif key == "Byte count":
121
+ e.bytes = _int(val)
122
+ elif key == "Duration (seconds)":
123
+ e.duration = _int(val)
124
+ elif key == "Last matched timeout (seconds)":
125
+ e.idle_timeout = _int(val)
126
+ elif key == "Last modified timeout (seconds)":
127
+ e.hard_timeout = _int(val)
128
+ elif depth == 1 and section == "match":
129
+ key, sep, val = s.partition(":")
130
+ if sep:
131
+ e.match[key.strip()] = val.strip()
132
+ elif depth >= 2 and section == "actions":
133
+ key, sep, val = s.partition(":")
134
+ if sep and key.strip() == "Output port":
135
+ e.actions.append(_port_name(val))
136
+ out.append(e)
137
+ return out
138
+
139
+
140
+ def merge(entries: list, stats: list) -> list[FlowEntry]:
141
+ """Merge an `entry all` parse (match+actions) with a `stats entry all` parse
142
+ (counters), keyed by entry index. Missing fields are filled from whichever dump has them."""
143
+ by_idx: dict[int, FlowEntry] = {e.index: e for e in entries}
144
+ for s in stats:
145
+ e = by_idx.get(s.index)
146
+ if e is None:
147
+ by_idx[s.index] = s
148
+ continue
149
+ e.match = e.match or s.match
150
+ e.actions = e.actions or s.actions
151
+ for attr in ("priority", "packets", "bytes", "duration",
152
+ "idle_timeout", "hard_timeout"):
153
+ if getattr(e, attr) is None and getattr(s, attr) is not None:
154
+ setattr(e, attr, getattr(s, attr))
155
+ return [by_idx[i] for i in sorted(by_idx)]
156
+
157
+
158
+ def parse_table_stats(text: str) -> dict:
159
+ """Parse `openflow stats table` into the table-level counters that prove the switch is
160
+ being programmed: active flows, packets looked up, and packets that matched a flow."""
161
+ out: dict[str, int] = {}
162
+ keymap = {
163
+ "Number of active entries": "active",
164
+ "Number of packets looked up in tables": "lookups",
165
+ "Number of packets that hit tables": "matched",
166
+ "Maximum number of supported entries": "max",
167
+ }
168
+ for line in (text or "").splitlines():
169
+ k, sep, v = line.strip().partition(":")
170
+ if sep and k.strip() in keymap:
171
+ n = _int(v.strip())
172
+ if n is not None:
173
+ out[keymap[k.strip()]] = n
174
+ return out
175
+
176
+
177
+ def flows(entry_dump: str, stats_dump: str = "") -> list[FlowEntry]:
178
+ """Convenience: parse both dumps and merge into the display rows."""
179
+ return merge(parse(entry_dump), parse(stats_dump) if stats_dump else [])