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/xv6_fs.py ADDED
@@ -0,0 +1,250 @@
1
+ """xv6 file-system state model — the Storage face's read side.
2
+
3
+ xv6's on-disk layout is a fixed sequence of regions (boot | super | log | inodes | bitmap |
4
+ data); a file is a `dinode` with up to 12 direct + 1 indirect block pointers; a directory is an
5
+ array of `dirent {inum, name}`; the buffer cache holds recently-used blocks; and crash-safe
6
+ writes go through a small write-ahead LOG (a transaction of block numbers that is committed then
7
+ installed). This module models all of that as pure data, with parsers for the GDB reads that
8
+ produce it (superblock, log header) — so the layout maths, the parsers, and the DemoDisk feed
9
+ are all unit-tested without a disk image. The live reads are Mac-side (GDB + xv6 symbols).
10
+ """
11
+ from __future__ import annotations
12
+
13
+ import re
14
+ from dataclasses import dataclass, field
15
+
16
+ BSIZE = 1024 # xv6 block size
17
+ IPB = BSIZE // 64 # inodes per block (sizeof(dinode) == 64) -> 16
18
+ BPB = BSIZE * 8 # bitmap bits per block -> 8192
19
+ DIRSIZ = 14
20
+ _ITYPE = {0: "free", 1: "dir", 2: "file", 3: "dev"}
21
+
22
+
23
+ @dataclass
24
+ class Superblock:
25
+ size: int = 0 # total blocks
26
+ nblocks: int = 0 # data blocks
27
+ ninodes: int = 0
28
+ nlog: int = 0
29
+ logstart: int = 0
30
+ inodestart: int = 0
31
+ bmapstart: int = 0
32
+
33
+
34
+ @dataclass
35
+ class Region:
36
+ name: str
37
+ start: int
38
+ count: int
39
+
40
+ @property
41
+ def end(self) -> int:
42
+ return self.start + max(self.count, 1) - 1
43
+
44
+
45
+ @dataclass
46
+ class Inode:
47
+ inum: int
48
+ type: str # free|dir|file|dev
49
+ nlink: int = 0
50
+ size: int = 0
51
+ blocks: list = field(default_factory=list) # data block numbers (direct+indirect)
52
+
53
+
54
+ @dataclass
55
+ class Dirent:
56
+ inum: int
57
+ name: str
58
+ is_dir: bool = False
59
+ depth: int = 0 # for a flattened tree view
60
+
61
+
62
+ @dataclass
63
+ class Buf:
64
+ blockno: int
65
+ valid: bool = True
66
+ dirty: bool = False
67
+ refcnt: int = 0
68
+
69
+
70
+ @dataclass
71
+ class LogState:
72
+ start: int = 0
73
+ size: int = 0
74
+ outstanding: int = 0 # syscalls in flight in this group commit
75
+ committing: bool = False
76
+ blocks: list = field(default_factory=list) # dest block numbers in the current transaction
77
+
78
+ @property
79
+ def phase(self) -> str:
80
+ if self.committing:
81
+ return "committing"
82
+ if self.blocks:
83
+ return "building"
84
+ return "idle"
85
+
86
+
87
+ @dataclass
88
+ class FsSnapshot:
89
+ sb: Superblock
90
+ regions: list = field(default_factory=list)
91
+ inodes: list = field(default_factory=list)
92
+ tree: list = field(default_factory=list) # flattened Dirents
93
+ bufs: list = field(default_factory=list)
94
+ log: LogState = field(default_factory=LogState)
95
+ hits: int = 0
96
+ misses: int = 0
97
+ # Provenance so the UI never passes off demo data as real. `source` is "real" (from the
98
+ # running kernel) or "demo" (the DemoDisk stand-in). `ok` is False when a REAL read was
99
+ # attempted but produced nothing — the face must show an error, NOT silently fall to demo.
100
+ source: str = "real"
101
+ ok: bool = True
102
+ # Which real panels this build can actually populate (the rest render "not available (real)"
103
+ # instead of fake data). Superblock + log are dumped today; inodes/dir/bcache are not yet.
104
+ have: tuple = ("layout", "log")
105
+
106
+ @property
107
+ def hit_rate(self) -> float:
108
+ tot = self.hits + self.misses
109
+ return (self.hits / tot) if tot else 0.0
110
+
111
+
112
+ # -- layout maths ----------------------------------------------------------- #
113
+ def layout(sb: Superblock) -> list[Region]:
114
+ """The fixed on-disk region sequence, derived from the superblock."""
115
+ ninode_blocks = (sb.ninodes + IPB - 1) // IPB if sb.ninodes else 0
116
+ nbitmap = (sb.size + BPB - 1) // BPB if sb.size else 0
117
+ data_start = sb.bmapstart + nbitmap
118
+ data_blocks = max(sb.size - data_start, 0)
119
+ return [
120
+ Region("boot", 0, 1),
121
+ Region("super", 1, 1),
122
+ Region("log", sb.logstart, sb.nlog),
123
+ Region("inodes", sb.inodestart, ninode_blocks),
124
+ Region("bitmap", sb.bmapstart, nbitmap),
125
+ Region("data", data_start, data_blocks),
126
+ ]
127
+
128
+
129
+ # -- parsers (GDB struct prints) -------------------------------------------- #
130
+ def _ints(text: str) -> list[int]:
131
+ """All integers in a GDB brace list, expanding `N <repeats K times>`."""
132
+ out: list[int] = []
133
+ for tok in text.split(","):
134
+ tok = tok.strip()
135
+ m = re.match(r"(-?\d+)\s*<repeats\s+(\d+)\s+times>", tok)
136
+ if m:
137
+ out += [int(m.group(1))] * int(m.group(2))
138
+ elif re.match(r"-?\d+$", tok):
139
+ out.append(int(tok))
140
+ return out
141
+
142
+
143
+ def _field(text: str, name: str) -> int | None:
144
+ m = re.search(rf"\b{name}\s*=\s*(-?\d+)", text)
145
+ return int(m.group(1)) if m else None
146
+
147
+
148
+ def parse_superblock(text: str) -> Superblock:
149
+ """GDB `p *sb` -> Superblock (fields matched by name; magic/order-independent)."""
150
+ g = lambda n: _field(text, n) or 0
151
+ return Superblock(size=g("size"), nblocks=g("nblocks"), ninodes=g("ninodes"),
152
+ nlog=g("nlog"), logstart=g("logstart"), inodestart=g("inodestart"),
153
+ bmapstart=g("bmapstart"))
154
+
155
+
156
+ def parse_logheader(text: str, start: int = 0, size: int = 0,
157
+ outstanding: int = 0, committing: bool = False) -> LogState:
158
+ """Parse the write-ahead log state. `n` = blocks in this transaction; `block[]` = dests.
159
+ Also reads `start`/`outstanding`/`committing` from the serial dump if present (the `\\bstart`
160
+ boundary avoids matching the superblock's logstart/inodestart/bmapstart)."""
161
+ n = _field(text, "n") or 0
162
+ m = re.search(r"block\s*=\s*\{([^}]*)\}", text)
163
+ blocks = _ints(m.group(1))[:n] if m else []
164
+ st, out, com = _field(text, "start"), _field(text, "outstanding"), _field(text, "committing")
165
+ return LogState(start=st if st is not None else start, size=size,
166
+ outstanding=out if out is not None else outstanding,
167
+ committing=bool(com) if com is not None else committing, blocks=blocks)
168
+
169
+
170
+ def parse_dinode(text: str, inum: int) -> Inode:
171
+ """GDB `p *ip` (dinode) -> Inode (type/nlink/size + addrs block list)."""
172
+ t = _ITYPE.get(_field(text, "type") or 0, "free")
173
+ m = re.search(r"addrs\s*=\s*\{([^}]*)\}", text)
174
+ blocks = [b for b in _ints(m.group(1))] if m else []
175
+ return Inode(inum=inum, type=t, nlink=_field(text, "nlink") or 0,
176
+ size=_field(text, "size") or 0, blocks=[b for b in blocks if b])
177
+
178
+
179
+ def fs_summary(fs: FsSnapshot) -> str:
180
+ """Compact, LLM-facing summary of the file-system state (for the Ask GINI card, L2)."""
181
+ if fs is None:
182
+ return ""
183
+ sb = fs.sb
184
+ lines = [f"file system: {sb.size} blocks · layout "
185
+ + " | ".join(r.name for r in fs.regions)]
186
+ active = [i for i in fs.inodes if i.type != "free"]
187
+ if active:
188
+ lines.append(" inodes: "
189
+ + ", ".join(f"#{i.inum} {i.type}({i.size}B)" for i in active[:6]))
190
+ lines.append(f" buffer cache: {fs.hits} hits / {fs.misses} miss "
191
+ f"({fs.hit_rate * 100:.0f}% hit)")
192
+ lg = fs.log
193
+ tail = (f" — {len(lg.blocks)} block(s): {', '.join(map(str, lg.blocks))}"
194
+ if lg.blocks else "")
195
+ lines.append(f" write-ahead log: {lg.phase}{tail}")
196
+ return "\n".join(lines)
197
+
198
+
199
+ # -- offline demo ----------------------------------------------------------- #
200
+ class DemoDisk:
201
+ """A deterministic small xv6 file system + a simulated write, so the Storage face is
202
+ explorable and testable without a disk image. Not a real FS — a faithful-shaped stand-in."""
203
+
204
+ def __init__(self) -> None:
205
+ self.sb = Superblock(size=2000, nblocks=1954, ninodes=200, nlog=30,
206
+ logstart=2, inodestart=32, bmapstart=45)
207
+ self._regions = layout(self.sb)
208
+ data0 = self._regions[-1].start
209
+ self.inodes = [
210
+ Inode(1, "dir", 1, 96, [data0]),
211
+ Inode(2, "dev", 1, 0, []),
212
+ Inode(3, "file", 1, 2200, [data0 + 1, data0 + 2, data0 + 3]),
213
+ Inode(4, "dir", 1, 64, [data0 + 4]),
214
+ Inode(5, "file", 2, 900, [data0 + 5]),
215
+ ]
216
+ self.tree = [
217
+ Dirent(1, "/", True, 0),
218
+ Dirent(2, "console", False, 1),
219
+ Dirent(3, "README", False, 1),
220
+ Dirent(4, "usr", True, 1),
221
+ Dirent(5, "usr/sh", False, 2),
222
+ ]
223
+ self.bufs = [Buf(1, True, False, 0), Buf(32, True, False, 1),
224
+ Buf(data0, True, False, 2), Buf(data0 + 1, True, True, 1),
225
+ Buf(45, True, False, 0)]
226
+ self.hits, self.misses = 128, 24
227
+ self.log = LogState(start=self.sb.logstart, size=self.sb.nlog)
228
+
229
+ def snapshot(self) -> FsSnapshot:
230
+ return FsSnapshot(sb=self.sb, regions=list(self._regions), inodes=list(self.inodes),
231
+ tree=list(self.tree), bufs=list(self.bufs), log=self.log,
232
+ hits=self.hits, misses=self.misses, source="demo",
233
+ have=("layout", "log", "inodes", "dir", "bcache"))
234
+
235
+ def simulate_write(self) -> FsSnapshot:
236
+ """Advance the write-ahead log: idle -> building -> committing -> installed(idle)."""
237
+ d0 = self._regions[-1].start
238
+ if self.log.phase == "idle":
239
+ self.log = LogState(self.sb.logstart, self.sb.nlog, outstanding=1,
240
+ blocks=[45, d0 + 1, self.sb.inodestart]) # bitmap, data, inode
241
+ self.hits += 3
242
+ elif self.log.phase == "building":
243
+ self.log = LogState(self.sb.logstart, self.sb.nlog, committing=True,
244
+ blocks=list(self.log.blocks))
245
+ else: # committing -> installed, back to idle
246
+ for b in self.log.blocks:
247
+ self.bufs.append(Buf(b, True, False, 0))
248
+ self.misses += 1
249
+ self.log = LogState(self.sb.logstart, self.sb.nlog)
250
+ return self.snapshot()
@@ -0,0 +1,113 @@
1
+ """Xv6Runner — the OS domain's behavioral-probe runner (the GINI-as-oracle seam for the kernel).
2
+
3
+ The networking oracle answers `reach(A -> B) == ok` against a live topology. The OS oracle answers
4
+ `measure(<what>, <which>) <op> <value>` against the running kernel's telemetry — CPU share, wait
5
+ times, ticket fairness, shadow liveness. Crucially this reuses the EXISTING probe grammar and
6
+ evaluator in `probes.py` unchanged: we only add a runner that knows how to compute OS metrics, the
7
+ same way `services/probe_runner.py` computes networking ones. GINI, never the model, gives the
8
+ verdict.
9
+
10
+ Pure: it reads a `Snapshot` (procs + sched fields), a `SchedTimeline` (who ran, CPU share), and the
11
+ shadow manifest ({name: ShadowStatus}); a `FakeRunner`/hand-built telemetry makes every predicate
12
+ unit-testable offline.
13
+
14
+ OS metrics understood by `measure(what, which)`:
15
+ cpu_share, highest_priority -> the highest-priority proc's fraction of recent CPU
16
+ cpu_share, <pid> -> that pid's fraction of recent CPU
17
+ max_wait_slices, any -> longest any RUNNABLE proc has waited (aging counter)
18
+ share_ratio, tickets -> max deviation of observed share from the ticket ratio
19
+ every_runnable_runs, any -> 1.0 if every runnable proc ran in the window, else 0.0
20
+ shadow_active, <name> -> 1.0 if that shadow is active, else 0.0
21
+ shadow_faults, <name> -> how many times that shadow crashed back to the primary
22
+ """
23
+ from __future__ import annotations
24
+
25
+
26
+ class Xv6Runner:
27
+ """A `probes.Runner` for the OS domain. Only `available()` + `measure()` are meaningful here
28
+ (OS win-conditions are all `measure(...)`); the networking methods are harmless no-ops so the
29
+ same evaluator can call either runner."""
30
+
31
+ def __init__(self, snapshot=None, timeline=None, shadows=None, window: int = 60) -> None:
32
+ self.snapshot = snapshot # domain.xv6.Snapshot (procs + sched fields)
33
+ self.timeline = timeline # domain.xv6.SchedTimeline (who ran / shares)
34
+ self.shadows = shadows or {} # {name: ShadowStatus}
35
+ self.window = window
36
+
37
+ # -- probes.Runner protocol --------------------------------------------- #
38
+ def available(self) -> bool:
39
+ return self.snapshot is not None
40
+
41
+ def reach(self, src, dst, port=None) -> bool: # not used by OS probes
42
+ return False
43
+
44
+ def http(self, src, dst, port) -> bool:
45
+ return False
46
+
47
+ def backends(self, lb) -> int:
48
+ return 0
49
+
50
+ def flow(self, ovs, match) -> bool:
51
+ return False
52
+
53
+ def measure(self, what: str, which: str):
54
+ """Return the OS metric value, or None when it can't be read (probe treats None as
55
+ not-satisfied, never a crash)."""
56
+ if self.snapshot is None:
57
+ return None
58
+ procs = list(getattr(self.snapshot, "procs", []) or [])
59
+ handler = getattr(self, f"_m_{what}", None)
60
+ return handler(which, procs) if handler else None
61
+
62
+ # -- individual metrics -------------------------------------------------- #
63
+ def _shares(self) -> dict:
64
+ return self.timeline.shares(self.window) if self.timeline is not None else {}
65
+
66
+ def _highest_priority_pid(self, procs):
67
+ """The proc with the numerically-lowest priority = highest scheduling priority."""
68
+ cand = [p for p in procs if p.priority is not None]
69
+ if not cand:
70
+ return None
71
+ return min(cand, key=lambda p: (p.priority, p.pid)).pid
72
+
73
+ def _m_cpu_share(self, which, procs):
74
+ shares = self._shares()
75
+ if which == "highest_priority":
76
+ pid = self._highest_priority_pid(procs)
77
+ return None if pid is None else float(shares.get(pid, 0.0))
78
+ try:
79
+ return float(shares.get(int(which), 0.0))
80
+ except (TypeError, ValueError):
81
+ return None
82
+
83
+ def _m_max_wait_slices(self, which, procs):
84
+ waits = [p.wait_ticks for p in procs
85
+ if p.state == "runnable" and p.wait_ticks is not None]
86
+ return float(max(waits)) if waits else None
87
+
88
+ def _m_share_ratio(self, which, procs):
89
+ """Max absolute deviation of each ticketed proc's observed CPU share from its ticket share
90
+ (0 = share tracks tickets exactly; the lottery win-condition asserts this is small)."""
91
+ ticketed = [p for p in procs if p.tickets]
92
+ total = sum(p.tickets for p in ticketed)
93
+ if not ticketed or total <= 0:
94
+ return None
95
+ shares = self._shares()
96
+ return float(max(abs(shares.get(p.pid, 0.0) - p.tickets / total) for p in ticketed))
97
+
98
+ def _m_every_runnable_runs(self, which, procs):
99
+ runnable = {p.pid for p in procs if p.state in ("runnable", "running")}
100
+ if not runnable:
101
+ return 1.0
102
+ if self.timeline is None:
103
+ return None
104
+ ran = {s.pid for s in self.timeline.recent(self.window)}
105
+ return 1.0 if runnable <= ran else 0.0
106
+
107
+ def _m_shadow_active(self, which, procs):
108
+ s = self.shadows.get(which)
109
+ return 1.0 if (s is not None and s.active) else 0.0
110
+
111
+ def _m_shadow_faults(self, which, procs):
112
+ s = self.shadows.get(which)
113
+ return None if s is None else float(s.faults)