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/runtime/switch.py ADDED
@@ -0,0 +1,109 @@
1
+ """User-space learning switch.
2
+
3
+ One UDP port per link. Learns source MAC -> ingress port; floods broadcast/multicast
4
+ and unknown unicast to all other ports; unicasts known destinations. This is the
5
+ "multiplexed fabric switch" from the plan, here as a standalone process for the spike.
6
+
7
+ Run as a process: SWITCH_CONFIG='{"name":"s1","ports":[{...}]}' python -m dataplane.switch
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import json
12
+ import os
13
+ import sys
14
+
15
+ from .control import maybe_start
16
+ from .frame import is_multicast_mac, parse_eth
17
+ from .transport import Port, run_loop
18
+
19
+
20
+ class LearningSwitch:
21
+ def __init__(self, cfg: dict) -> None:
22
+ self.name = cfg["name"]
23
+ self.ports = [Port.from_cfg(p, name=f"p{i}") for i, p in enumerate(cfg["ports"])]
24
+ self.table: dict[str, Port] = {} # mac -> port
25
+ self.log = cfg.get("log", False)
26
+ self._ctrl = maybe_start(self.name, self._control, f"switch {self.name}")
27
+
28
+ def _control(self, cmd: str) -> str:
29
+ cmd = cmd.lower()
30
+ if cmd in ("help", "?", "h"):
31
+ return "commands: mactable, ports, help, exit"
32
+ if cmd in ("mactable", "mac", "table"):
33
+ if not self.table:
34
+ return "(mac table empty)"
35
+ return "\n".join(f" {m} -> {p.name}" for m, p in self.table.items())
36
+ if cmd == "ports":
37
+ return ", ".join(p.name for p in self.ports)
38
+ return f"unknown command: {cmd} (try 'help')"
39
+
40
+ def handle(self, inport: Port, frame: bytes) -> None:
41
+ dst, src, _etype, _payload = parse_eth(frame)
42
+ self.table[src] = inport
43
+ if dst == "ff:ff:ff:ff:ff:ff" or is_multicast_mac(dst):
44
+ self._flood(inport, frame)
45
+ return
46
+ out = self.table.get(dst)
47
+ if out is None:
48
+ self._flood(inport, frame)
49
+ elif out is not inport:
50
+ out.send(frame)
51
+ if self.log:
52
+ print(f"[{self.name}] {src} -> {dst} in={inport.name}", file=sys.stderr)
53
+
54
+ def _flood(self, inport: Port, frame: bytes) -> None:
55
+ for p in self.ports:
56
+ if p is not inport:
57
+ p.send(frame)
58
+
59
+ def run(self) -> None:
60
+ print(f"[{self.name}] learning switch up, {len(self.ports)} ports", file=sys.stderr)
61
+ run_loop(self.ports, self.handle)
62
+
63
+
64
+ class Hub(LearningSwitch):
65
+ """A Layer-1 repeater — the device a switch replaced.
66
+
67
+ A hub is "dumb wire with more ports": every frame that arrives is repeated out of
68
+ every *other* port, with no MAC learning and no filtering. All ports therefore share
69
+ one collision domain and one broadcast domain. Pedagogically this is the foil to the
70
+ Switch: send a unicast h1->h2 through a hub and h3 still sees it (repeated to all);
71
+ through a switch, once it has learned, only h2 does. The control console makes the
72
+ difference explicit — a hub has no MAC table to show."""
73
+
74
+ def handle(self, inport: Port, frame: bytes) -> None:
75
+ self._flood(inport, frame) # repeat everything; never learn, never filter
76
+ if self.log:
77
+ dst, src, _etype, _payload = parse_eth(frame)
78
+ print(f"[{self.name}] repeat {src} -> {dst} in={inport.name} (flood all)",
79
+ file=sys.stderr)
80
+
81
+ def _control(self, cmd: str) -> str:
82
+ cmd = cmd.lower()
83
+ if cmd in ("help", "?", "h"):
84
+ return ("commands: ports, help, exit "
85
+ "(a hub has no MAC table — it repeats every frame to all ports)")
86
+ if cmd in ("mactable", "mac", "table"):
87
+ return "(a hub has no MAC table — it repeats every frame out all other ports)"
88
+ if cmd == "ports":
89
+ return ", ".join(p.name for p in self.ports)
90
+ return f"unknown command: {cmd} (try 'help')"
91
+
92
+ def run(self) -> None:
93
+ print(f"[{self.name}] hub up (L1 repeater), {len(self.ports)} ports", file=sys.stderr)
94
+ run_loop(self.ports, self.handle)
95
+
96
+
97
+ def make_switch(cfg: dict):
98
+ """Build the right L2 node for a fabric entry: a Hub (flood-all repeater) when the
99
+ compiler marked it `hub`, else a learning Switch."""
100
+ return Hub(cfg) if cfg.get("hub") else LearningSwitch(cfg)
101
+
102
+
103
+ def main() -> None:
104
+ cfg = json.loads(os.environ["SWITCH_CONFIG"])
105
+ make_switch(cfg).run()
106
+
107
+
108
+ if __name__ == "__main__":
109
+ main()
@@ -0,0 +1,77 @@
1
+ """Ethernet-in-UDP transport: a Port is a UDP socket carrying one frame per datagram.
2
+
3
+ Same abstraction works two ways:
4
+ * Docker: bind 0.0.0.0:P, peer_host = a service name (Docker DNS), peer_port = P.
5
+ * Loopback test: bind 127.0.0.1 on distinct ports per endpoint.
6
+
7
+ A "port" on a switch/router/host is exactly this. Internal fabric links would later
8
+ swap this for an in-memory ring; the node code wouldn't change.
9
+ """
10
+ from __future__ import annotations
11
+
12
+ import selectors
13
+ import socket
14
+
15
+
16
+ class Port:
17
+ def __init__(self, bind_port: int, peer_host: str, peer_port: int,
18
+ bind_host: str = "0.0.0.0", name: str = "") -> None:
19
+ self.name = name
20
+ self.peer_host = peer_host
21
+ self.peer_port = peer_port
22
+ self._peer_ip: str | None = None
23
+ self.sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
24
+ self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
25
+ self.sock.bind((bind_host, bind_port))
26
+ self.sock.setblocking(False)
27
+
28
+ @classmethod
29
+ def from_cfg(cls, cfg: dict, name: str = "") -> "Port":
30
+ return cls(cfg["bind_port"], cfg["peer_host"], cfg["peer_port"],
31
+ cfg.get("bind_host", "0.0.0.0"), name)
32
+
33
+ def _resolve(self) -> str | None:
34
+ if self._peer_ip is None:
35
+ try:
36
+ self._peer_ip = socket.gethostbyname(self.peer_host)
37
+ except OSError:
38
+ return None # DNS not ready yet (Docker startup) — caller drops
39
+ return self._peer_ip
40
+
41
+ def send(self, frame: bytes) -> None:
42
+ ip = self._resolve()
43
+ if ip is None:
44
+ return
45
+ try:
46
+ self.sock.sendto(frame, (ip, self.peer_port))
47
+ except OSError:
48
+ pass
49
+
50
+ def recv(self) -> bytes | None:
51
+ try:
52
+ data, _ = self.sock.recvfrom(65535)
53
+ return data
54
+ except BlockingIOError:
55
+ return None
56
+
57
+
58
+ def run_loop(ports: list[Port], handler, tick=None, tick_interval: float = 0.5) -> None:
59
+ """Select over all ports; call handler(port, frame) for each frame.
60
+
61
+ handler receives the Port the frame arrived on, so a node can identify its
62
+ ingress port without any per-frame addressing.
63
+ """
64
+ sel = selectors.DefaultSelector()
65
+ for p in ports:
66
+ sel.register(p.sock, selectors.EVENT_READ, p)
67
+ while True:
68
+ events = sel.select(timeout=tick_interval)
69
+ for key, _ in events:
70
+ port: Port = key.data
71
+ while True:
72
+ frame = port.recv()
73
+ if frame is None:
74
+ break
75
+ handler(port, frame)
76
+ if tick is not None:
77
+ tick()
@@ -0,0 +1,312 @@
1
+ """xv6 live bridge (Mac side) — fills MachineState from the in-container agent over HTTP.
2
+
3
+ The gdb reads happen INSIDE the gini-xv6 container (backend/xv6/gini_agent.py), where gdb and
4
+ the kernel symbols live; this bridge just makes plain HTTP calls to the agent's published port
5
+ and parses the returned text with the already-tested domain parsers. It is a drop-in for the
6
+ offline DemoScheduler: snapshot()/step()/set_timeslice()/set_policy() plus `.vm`/`.fs` readers,
7
+ so MachineState, the Machine Lab, and the Ask GINI card use it unchanged.
8
+
9
+ The HTTP calls are injectable (`get=`/`post=`) so the compose + parsing logic is unit-tested
10
+ without a running container; the VM/FS readers fall back to the demo shape on any error, so the
11
+ Memory/Storage dialogs always open even if the live read momentarily fails.
12
+ """
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import urllib.request
17
+
18
+ from ..domain.xv6 import (
19
+ Snapshot, apply_proc_sched, parse_backtrace, parse_cpu_lines, parse_cpu_regs, parse_csr,
20
+ parse_modetime, parse_policies, parse_procdump, parse_registers, parse_regs_line, parse_sched,
21
+ parse_shadow_manifest, running_pid,
22
+ )
23
+ from ..domain.xv6_fs import FsSnapshot, Superblock, layout, parse_logheader, parse_superblock
24
+ from ..domain.xv6_vm import VmSnapshot, parse_faults, parse_vmall, parse_vmprint
25
+
26
+ # must match gini_pick() in gini_patch.py: 0=round-robin 1=priority 2=lottery. Custom student
27
+ # policies (MLFQ, stride, …) get their own ids when added via the Scheduler Builder.
28
+ POLICY_ID = {"round-robin": 0, "priority": 1, "lottery": 2}
29
+ # each shadow overrides the policy at this index (the kernel toggles the CURRENT policy's shadow).
30
+ SHADOW_POLICY = {"rr_sched": 0, "prio_sched": 1, "lottery_sched": 2}
31
+
32
+
33
+ class AgentClient:
34
+ """HTTP client for the in-container agent. `get`/`post` are injectable for tests."""
35
+
36
+ # longer than the agent's own 6s gdb timeout, so a slow proc-walk under CPU load returns a
37
+ # result (even an empty one we can ignore) instead of the client abandoning it mid-read.
38
+ def __init__(self, base_url: str, get=None, post=None, timeout: float = 10.0) -> None:
39
+ self.base = base_url.rstrip("/")
40
+ self.timeout = timeout
41
+ self._get = get or self._http_get
42
+ self._post = post or self._http_post
43
+
44
+ def _http_get(self, url: str) -> str: # pragma: no cover - real network
45
+ with urllib.request.urlopen(url, timeout=self.timeout) as r:
46
+ return r.read().decode(errors="replace")
47
+
48
+ def _http_post(self, url: str, body: str = "") -> str: # pragma: no cover - real network
49
+ req = urllib.request.Request(url, data=body.encode() if body else None, method="POST")
50
+ with urllib.request.urlopen(req, timeout=self.timeout) as r:
51
+ return r.read().decode(errors="replace")
52
+
53
+ def get_json(self, path: str) -> dict:
54
+ try:
55
+ return json.loads(self._get(self.base + path))
56
+ except Exception:
57
+ return {}
58
+
59
+ def get_text(self, path: str) -> str:
60
+ try:
61
+ return self._get(self.base + path)
62
+ except Exception:
63
+ return ""
64
+
65
+ def post(self, path: str, body: str = "") -> str:
66
+ try:
67
+ return self._post(self.base + path, body) if body else self._post(self.base + path)
68
+ except TypeError:
69
+ return self._post(self.base + path) # injected posts without a body arg
70
+ except Exception:
71
+ return ""
72
+
73
+
74
+ class _VmReader:
75
+ def __init__(self, agent: AgentClient) -> None:
76
+ self.agent = agent
77
+
78
+ def snapshot(self):
79
+ # REAL only: return the live page table, or an explicit no-data VmSnapshot. Never fake.
80
+ # The region map + physical-allocator bar aren't dumped for real yet, so they're left
81
+ # empty (not in `have`) rather than borrowed from the demo.
82
+ try:
83
+ vm = parse_vmprint(self.agent.get_text("/vm"))
84
+ if vm.leaves: # got real mappings
85
+ vm.source, vm.ok, vm.have = "real", True, ("pagetable",)
86
+ return vm
87
+ except Exception:
88
+ pass
89
+ return VmSnapshot(source="real", ok=False, have=())
90
+
91
+ def all_procs(self):
92
+ """Every user process's page table (gini_vmdump_all) -> {pid: ProcVm}, for the two-proc
93
+ COW / sharing view. REAL only: empty dict if the read fails (the face shows an error)."""
94
+ try:
95
+ procs = parse_vmall(self.agent.get_text("/vmall"))
96
+ if procs:
97
+ return procs
98
+ except Exception:
99
+ pass
100
+ return {}
101
+
102
+ def faults(self):
103
+ """The live page-fault ring (gini_faultdump) -> [PageFault]. REAL only: empty list if the
104
+ read fails. Classification happens in the Memory face against the all-procs picture."""
105
+ try:
106
+ f = parse_faults(self.agent.get_text("/faults"))
107
+ if f:
108
+ return f
109
+ except Exception:
110
+ pass
111
+ return []
112
+
113
+
114
+ class _FsReader:
115
+ def __init__(self, agent: AgentClient) -> None:
116
+ self.agent = agent
117
+
118
+ def snapshot(self):
119
+ # REAL only: superblock + log are dumped for real today; inodes/dir/bcache are NOT, so
120
+ # they stay empty and out of `have` (the face marks them "not available", never fakes).
121
+ # A failed read returns ok=False so the face shows an error instead of demo data.
122
+ try:
123
+ txt = self.agent.get_text("/fs") # gini_fsdump over the serial (no gdb)
124
+ sb = parse_superblock(txt)
125
+ if sb.size:
126
+ log = parse_logheader(txt, start=sb.logstart, size=sb.nlog)
127
+ return FsSnapshot(sb=sb, regions=layout(sb), log=log,
128
+ source="real", ok=True, have=("layout", "log"))
129
+ except Exception:
130
+ pass
131
+ return FsSnapshot(sb=Superblock(), source="real", ok=False, have=())
132
+
133
+
134
+ class Xv6Bridge:
135
+ """The live provider (talks HTTP to the in-container agent)."""
136
+
137
+ def __init__(self, agent: AgentClient, quantum: int = 1) -> None:
138
+ self.agent = agent
139
+ self.timeslice = int(quantum)
140
+ self.policy = "round-robin"
141
+ self.vm = _VmReader(agent)
142
+ self.fs = _FsReader(agent)
143
+ self._seq = 0
144
+ self._last_cpu = None # register/stack detail (from gdb) — reused between fast reads
145
+ self._last_stack: list = []
146
+ self.kernel_quantum = None # the kernel's ACTUAL sched_quantum (from the SCHED line)
147
+ self.kernel_policy = None # the kernel's ACTUAL sched_policy id (from the SCHED line)
148
+ self.kernel_policies = {} # {id: name} roster (POLICY lines) — drives the UI selector
149
+
150
+ def snapshot(self) -> Snapshot:
151
+ """The Run/observe read: gini_dump() over the serial gives BOTH the process table AND
152
+ the running process's live registers in one shot — FAST and NO halt, so scheduling keeps
153
+ running and registers (sp/satp change per process!) update live. The kernel stack (bt)
154
+ still needs gdb, so it's only refreshed on Step; here we reuse the last one."""
155
+ self._seq += 1
156
+ txt = self.agent.get_text("/procs")
157
+ procs = apply_proc_sched(parse_procdump(txt), txt) # procs + priority/tickets/level/aging
158
+ sched = parse_sched(txt) # the kernel's ACTUAL quantum + policy
159
+ if sched:
160
+ self.kernel_quantum = sched.get("quantum")
161
+ self.kernel_policy = sched.get("policy")
162
+ roster = parse_policies(txt) # {id: name}; drives the UI policy selector
163
+ if roster:
164
+ self.kernel_policies = roster
165
+ cpu = parse_regs_line(txt) # live registers from the same no-halt dump
166
+ if cpu.regs:
167
+ self._last_cpu = cpu
168
+ else:
169
+ cpu = self._last_cpu # CPU idle / no REGS line -> keep last
170
+ return Snapshot(procs=procs, running_pid=running_pid(procs), ticks=self._seq,
171
+ cpu=cpu, stack=self._last_stack, cpus=parse_cpu_lines(txt),
172
+ cpu_regs=parse_cpu_regs(txt),
173
+ modetime=parse_modetime(txt), csr=parse_csr(txt))
174
+
175
+ def _detail_snapshot(self) -> Snapshot:
176
+ """A full gdb read (halts the guest briefly): registers + kernel stack + a proc walk.
177
+ Used on open and on Step, where a momentary halt is expected."""
178
+ d = self.agent.get_json("/snapshot")
179
+ self._last_cpu = parse_registers(d.get("registers", ""))
180
+ self._last_stack = parse_backtrace(d.get("bt", ""))
181
+ procs = parse_procdump(d.get("procs", ""))
182
+ return Snapshot(procs=procs, running_pid=running_pid(procs), ticks=self._seq,
183
+ cpu=self._last_cpu, stack=self._last_stack)
184
+
185
+ def step(self) -> Snapshot:
186
+ self.agent.post("/step")
187
+ self._seq += 1
188
+ return self._detail_snapshot() # halted at swtch -> full frozen detail
189
+
190
+ def set_timeslice(self, ticks: int) -> None:
191
+ self.timeslice = int(ticks)
192
+ self.agent.post(f"/control?quantum={int(ticks)}")
193
+
194
+ def set_policy(self, policy: str) -> None:
195
+ self.policy = policy
196
+ # map the display name -> id via the live kernel roster (so a NEW policy works), falling
197
+ # back to the built-in ids for the shipped three.
198
+ by_name = {name: pid for pid, name in self.kernel_policies.items()}
199
+ pid = by_name.get(policy, POLICY_ID.get(policy, 0))
200
+ self.agent.post(f"/control?policy={pid}")
201
+
202
+ # -- shadows + control-plane operations (NOT system calls) --------------------- #
203
+ def shadows(self) -> dict:
204
+ """The shadow manifest -> {name: ShadowStatus}: which student shadows are present, enabled,
205
+ active, and fault-free. The liveness signal the assignment oracle checks first."""
206
+ return parse_shadow_manifest(self.agent.get_text("/shadows"))
207
+
208
+ def set_shadow(self, name: str, on: bool) -> None:
209
+ """Enable/disable a shadow. The kernel only knows how to toggle the CURRENT policy's shadow,
210
+ so we make that policy active, then toggle iff its state doesn't already match — all the
211
+ naming/targeting logic lives here (testable) and the kernel stays dumb."""
212
+ idx = SHADOW_POLICY.get(name)
213
+ if idx is None:
214
+ return
215
+ self.agent.post(f"/control?policy={idx}") # make that policy active
216
+ cur = self.shadows().get(name)
217
+ if cur is None or bool(cur.enabled) != bool(on):
218
+ self.agent.post("/shadow/toggle") # flip the current policy's shadow
219
+
220
+ def set_priority(self, pid: int, value: int) -> None:
221
+ """Control-plane op (not a syscall): set a process's scheduling priority so the workload
222
+ has real differences to schedule on."""
223
+ self.agent.post(f"/control/priority?pid={int(pid)}&v={int(value)}")
224
+
225
+ def set_tickets(self, pid: int, n: int) -> None:
226
+ """Control-plane op (not a syscall): set a process's lottery ticket count."""
227
+ self.agent.post(f"/control/tickets?pid={int(pid)}&n={int(n)}")
228
+
229
+ def load(self) -> tuple[bool, str]:
230
+ """The Load loop: rebuild the kernel in the container (incremental make) and restart QEMU
231
+ with the new kernel. Returns (ok, log) — log carries compile errors scoped to the student's
232
+ shadow file on failure."""
233
+ try:
234
+ d = json.loads(self.agent.post("/rebuild"))
235
+ return bool(d.get("ok")), str(d.get("log", ""))
236
+ except Exception as e:
237
+ return False, f"load failed: {e}"
238
+
239
+ def revert(self) -> tuple[bool, str]:
240
+ """Restore the shipped stub shadow and rebuild — one click back to a known-good kernel."""
241
+ try:
242
+ d = json.loads(self.agent.post("/revert"))
243
+ return bool(d.get("ok")), str(d.get("log", ""))
244
+ except Exception as e:
245
+ return False, f"revert failed: {e}"
246
+
247
+ # -- programs (launch / kill / console) via the in-container agent's serial ---- #
248
+ def programs(self) -> list:
249
+ return self.agent.get_json("/programs").get("programs", [])
250
+
251
+ def run(self, prog: str) -> bool:
252
+ try:
253
+ return bool(json.loads(self.agent.post(f"/run?prog={prog}")).get("ok"))
254
+ except Exception:
255
+ return False
256
+
257
+ def kill(self, pid: int) -> None:
258
+ self.agent.post(f"/kill?pid={int(pid)}")
259
+
260
+ def sc(self) -> str:
261
+ """Raw gini_scdump text (SC counts + TRACE ring) for the histogram + strace panels."""
262
+ return self.agent.get_text("/sc")
263
+
264
+ def traps(self) -> str:
265
+ """Raw gini_trapdump text (TC per-kind counters + TR trap ring) for the Traps face."""
266
+ return self.agent.get_text("/traps")
267
+
268
+ def catch_trap(self, kind: str = "any"):
269
+ """Freeze the next live user trap (gdb /trapcatch) and parse it into a TrapFrame — the
270
+ real scause/sepc/stval + saved user registers that seed the CPU journey. `kind` conditions
271
+ the breakpoint (pagefault/syscall/timer/illegal/device). Returns a not-ok TrapFrame on
272
+ timeout/idle, so the journey falls back to its authored captions."""
273
+ from ..domain.xv6 import parse_trapframe
274
+ raw = self.agent.post(f"/trapcatch?kind={kind}")
275
+ txt = ""
276
+ try:
277
+ txt = json.loads(raw).get("out", "")
278
+ except Exception:
279
+ txt = raw or ""
280
+ return parse_trapframe(txt)
281
+
282
+ def alarms(self) -> str:
283
+ """Raw gini_dump text (contains the per-proc `ALARM …` lines) for the sigalarm-lab strip."""
284
+ return self.agent.get_text("/procs")
285
+
286
+ def console(self) -> str:
287
+ return self.agent.get_text("/console")
288
+
289
+ def console_since(self, since: int = 0) -> tuple[str, int]:
290
+ """Append-only console delta for the Terminal: (new text after `since`, new cursor). The
291
+ Terminal keeps the cursor and appends, so output streams in instead of re-rendering."""
292
+ d = self.agent.get_json(f"/console/stream?since={int(since)}")
293
+ return d.get("text", ""), int(d.get("next", since))
294
+
295
+ def clear_console(self) -> None:
296
+ """Blank the Screen: tell the agent to move its console baseline to 'now', so the tail
297
+ starts fresh (the kernel keeps running; this only affects what the console view shows)."""
298
+ self.agent.post("/console/clear")
299
+
300
+ def send_input(self, text: str) -> None:
301
+ self.agent.post("/input", body=text)
302
+
303
+ def interrupt(self) -> None:
304
+ """Break a hung foreground program (xv6 has no Ctrl-C/SIGINT). Sends the kernel's break
305
+ control-char over the serial; the console driver handles it directly (not sh), so it works
306
+ even while a foreground process has the shell blocked in wait()."""
307
+ self.agent.post("/interrupt")
308
+
309
+
310
+ def connect(agent_port: int, host: str = "127.0.0.1", quantum: int = 1) -> Xv6Bridge:
311
+ """Build a live bridge to a running gini-xv6 container's agent (published `agent_port`)."""
312
+ return Xv6Bridge(AgentClient(f"http://{host}:{agent_port}"), quantum=quantum)
@@ -0,0 +1,22 @@
1
+ """GINI server — an authenticated broker in front of Docker/Kata.
2
+
3
+ Students never get a Docker socket or SSH. gBuilder sends a *topology* (the device/link
4
+ model, not a compose file) to this server, which runs on the Kata host; the server compiles
5
+ it with GINI's own trusted compiler, enforces policy (allowed images, Kata/runc runtimes
6
+ only, no privileged or host mounts, per-student resource caps + namespacing), and runs it.
7
+ The constrained API is the only surface students can reach — Docker never sees a
8
+ student-supplied command.
9
+
10
+ Pieces:
11
+ * auth.py — flat user file (PBKDF2 hashes) + HMAC-signed session tokens.
12
+ * policy.py — validate/sanitize a compiled RuntimeConfig before it can run.
13
+ * session.py — per-student project name + workdir (multi-tenant isolation).
14
+ * app.py — the request router (GiniServer.handle) + a stdlib HTTP wrapper.
15
+ """
16
+ from .app import GiniServer
17
+ from .auth import Tokens, UserStore, hash_password
18
+ from .policy import PolicyError, enforce
19
+ from .session import SessionManager
20
+
21
+ __all__ = ["GiniServer", "UserStore", "Tokens", "hash_password",
22
+ "PolicyError", "enforce", "SessionManager"]
@@ -0,0 +1,74 @@
1
+ """Run the GINI server, or manage its flat user file.
2
+
3
+ python -m gini.server adduser users.json s_jane
4
+ python -m gini.server run --users users.json --base ./gini-labs --port 10000
5
+
6
+ The server secret comes from $GINI_SERVER_SECRET (else a random one each start, which just
7
+ invalidates old tokens on restart — fine for a class).
8
+ """
9
+ from __future__ import annotations
10
+
11
+ import getpass
12
+ import json
13
+ import os
14
+ import sys
15
+ from pathlib import Path
16
+
17
+
18
+ def _adduser(argv: list[str]) -> int:
19
+ if len(argv) < 2:
20
+ print("usage: adduser <userfile> <username>")
21
+ return 2
22
+ from .auth import hash_password
23
+ path, user = Path(argv[0]), argv[1]
24
+ users = json.loads(path.read_text()) if path.exists() else {}
25
+ users[user] = hash_password(getpass.getpass(f"password for {user}: "))
26
+ path.write_text(json.dumps(users, indent=2))
27
+ print(f"saved {user} to {path}")
28
+ return 0
29
+
30
+
31
+ def _run(argv: list[str]) -> int:
32
+ import argparse
33
+ ap = argparse.ArgumentParser(prog="gini.server run")
34
+ ap.add_argument("--users", default="users.json")
35
+ ap.add_argument("--base", default="./gini-labs")
36
+ ap.add_argument("--host", default="0.0.0.0")
37
+ ap.add_argument("--port", type=int, default=10000)
38
+ ap.add_argument("--max-cpus", type=float, default=2.0)
39
+ a = ap.parse_args(argv)
40
+
41
+ from .. import runtime as _rt
42
+ from ..services.orchestrator import Orchestrator
43
+ from .app import GiniServer, serve
44
+ from .auth import Tokens, UserStore
45
+ from .session import SessionManager
46
+
47
+ runtime_dir = Path(_rt.__file__).parent
48
+ secret = os.environ.get("GINI_SERVER_SECRET", os.urandom(32).hex()).encode()
49
+
50
+ def factory(project: str, workdir):
51
+ return Orchestrator(runtime_dir, project=project)
52
+
53
+ srv = GiniServer(UserStore.from_file(a.users), Tokens(secret),
54
+ SessionManager(a.base), factory, max_cpus=a.max_cpus)
55
+ serve(srv, a.host, a.port)
56
+ return 0
57
+
58
+
59
+ def main(argv: list[str] | None = None) -> int:
60
+ argv = list(sys.argv[1:] if argv is None else argv)
61
+ if not argv:
62
+ print("usage: python -m gini.server {run|adduser} ...")
63
+ return 2
64
+ cmd, rest = argv[0], argv[1:]
65
+ if cmd == "adduser":
66
+ return _adduser(rest)
67
+ if cmd == "run":
68
+ return _run(rest)
69
+ print(f"unknown command: {cmd}")
70
+ return 2
71
+
72
+
73
+ if __name__ == "__main__":
74
+ raise SystemExit(main())