rocket-engine 0.0.2b1__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 (241) hide show
  1. rocket_engine/__init__.py +8 -0
  2. rocket_engine/__main__.py +12 -0
  3. rocket_engine/adapters/README.md +54 -0
  4. rocket_engine/adapters/__init__.py +7 -0
  5. rocket_engine/adapters/api.py +39 -0
  6. rocket_engine/adapters/base.py +46 -0
  7. rocket_engine/adapters/cace/__init__.py +163 -0
  8. rocket_engine/adapters/circulax/README.md +10 -0
  9. rocket_engine/adapters/circulax/__init__.py +112 -0
  10. rocket_engine/adapters/circulax/_driver.py +190 -0
  11. rocket_engine/adapters/circulax/adapter.py +538 -0
  12. rocket_engine/adapters/circulax/results.py +291 -0
  13. rocket_engine/adapters/cli/README.md +34 -0
  14. rocket_engine/adapters/cli/__init__.py +305 -0
  15. rocket_engine/adapters/connectivity/__init__.py +196 -0
  16. rocket_engine/adapters/connectivity/results.py +214 -0
  17. rocket_engine/adapters/cosim/README.md +9 -0
  18. rocket_engine/adapters/cosim/__init__.py +1390 -0
  19. rocket_engine/adapters/cosim/results.py +214 -0
  20. rocket_engine/adapters/devsim/__init__.py +171 -0
  21. rocket_engine/adapters/digital_flow/README.md +9 -0
  22. rocket_engine/adapters/digital_flow/__init__.py +1121 -0
  23. rocket_engine/adapters/digital_flow/results.py +227 -0
  24. rocket_engine/adapters/digital_flow/state.py +462 -0
  25. rocket_engine/adapters/drc/README.klayout.md +7 -0
  26. rocket_engine/adapters/drc/README.md +7 -0
  27. rocket_engine/adapters/drc/__init__.py +1 -0
  28. rocket_engine/adapters/drc/klayout.py +952 -0
  29. rocket_engine/adapters/drc/magic.py +287 -0
  30. rocket_engine/adapters/drc/results.py +184 -0
  31. rocket_engine/adapters/drc/results_kl.py +188 -0
  32. rocket_engine/adapters/elmer/__init__.py +163 -0
  33. rocket_engine/adapters/envelope.py +120 -0
  34. rocket_engine/adapters/equivalence/__init__.py +156 -0
  35. rocket_engine/adapters/errors.py +181 -0
  36. rocket_engine/adapters/extract_magic/README.md +15 -0
  37. rocket_engine/adapters/extract_magic/__init__.py +425 -0
  38. rocket_engine/adapters/extract_magic/results.py +209 -0
  39. rocket_engine/adapters/formal_sby/README.md +7 -0
  40. rocket_engine/adapters/formal_sby/__init__.py +773 -0
  41. rocket_engine/adapters/formal_sby/formal_edge.py +195 -0
  42. rocket_engine/adapters/formal_sby/results.py +185 -0
  43. rocket_engine/adapters/gdsfactory/__init__.py +194 -0
  44. rocket_engine/adapters/gdsfactory/_driver.py +459 -0
  45. rocket_engine/adapters/gdsfactory/adapter.py +609 -0
  46. rocket_engine/adapters/gdsfactory/results.py +181 -0
  47. rocket_engine/adapters/hdl_sim/README.md +7 -0
  48. rocket_engine/adapters/hdl_sim/__init__.py +369 -0
  49. rocket_engine/adapters/hdl_sim/ghdl.py +155 -0
  50. rocket_engine/adapters/hdl_sim/results.py +178 -0
  51. rocket_engine/adapters/hdl_sim/verilator.py +167 -0
  52. rocket_engine/adapters/http/README.md +15 -0
  53. rocket_engine/adapters/http/__init__.py +458 -0
  54. rocket_engine/adapters/http/events.py +63 -0
  55. rocket_engine/adapters/ipc/README.md +9 -0
  56. rocket_engine/adapters/ipc/__init__.py +111 -0
  57. rocket_engine/adapters/lvs_netgen/README.md +20 -0
  58. rocket_engine/adapters/lvs_netgen/__init__.py +889 -0
  59. rocket_engine/adapters/lvs_netgen/results.py +168 -0
  60. rocket_engine/adapters/mcp/README.md +19 -0
  61. rocket_engine/adapters/mcp/__init__.py +85 -0
  62. rocket_engine/adapters/mcp/api-schema.json +6107 -0
  63. rocket_engine/adapters/mcp/pull.py +246 -0
  64. rocket_engine/adapters/mcp/server.py +794 -0
  65. rocket_engine/adapters/meep/__init__.py +167 -0
  66. rocket_engine/adapters/meow/README.md +7 -0
  67. rocket_engine/adapters/meow/__init__.py +190 -0
  68. rocket_engine/adapters/meow/_driver.py +227 -0
  69. rocket_engine/adapters/meow/_envelope.py +108 -0
  70. rocket_engine/adapters/meow/adapter.py +491 -0
  71. rocket_engine/adapters/meow/results.py +218 -0
  72. rocket_engine/adapters/mutation/__init__.py +192 -0
  73. rocket_engine/adapters/ngspice/README.md +28 -0
  74. rocket_engine/adapters/ngspice/__init__.py +627 -0
  75. rocket_engine/adapters/ngspice/results.py +215 -0
  76. rocket_engine/adapters/postlayout/README.md +28 -0
  77. rocket_engine/adapters/postlayout/__init__.py +17 -0
  78. rocket_engine/adapters/postlayout/adapter.py +1062 -0
  79. rocket_engine/adapters/postlayout/results.py +330 -0
  80. rocket_engine/adapters/postlayout/translate.py +71 -0
  81. rocket_engine/adapters/profile.py +220 -0
  82. rocket_engine/adapters/sax/README.md +23 -0
  83. rocket_engine/adapters/sax/__init__.py +516 -0
  84. rocket_engine/adapters/sax/_driver.py +148 -0
  85. rocket_engine/adapters/sax/results.py +291 -0
  86. rocket_engine/adapters/stdio_console.py +1151 -0
  87. rocket_engine/adapters/syntax/README.md +5 -0
  88. rocket_engine/adapters/syntax/__init__.py +366 -0
  89. rocket_engine/adapters/syntax/results.py +181 -0
  90. rocket_engine/adapters/synth_yosys/README.md +7 -0
  91. rocket_engine/adapters/synth_yosys/__init__.py +655 -0
  92. rocket_engine/adapters/synth_yosys/amaranth.py +120 -0
  93. rocket_engine/adapters/synth_yosys/ghdl.py +128 -0
  94. rocket_engine/adapters/synth_yosys/results.py +173 -0
  95. rocket_engine/adapters/timing_sta/README.md +9 -0
  96. rocket_engine/adapters/timing_sta/__init__.py +726 -0
  97. rocket_engine/adapters/view_sync/__init__.py +185 -0
  98. rocket_engine/adapters/view_sync/results.py +189 -0
  99. rocket_engine/adapters/vlsir/__init__.py +142 -0
  100. rocket_engine/adapters/xschem/__init__.py +440 -0
  101. rocket_engine/adapters/xschem/deck.py +266 -0
  102. rocket_engine/clients/__init__.py +5 -0
  103. rocket_engine/clients/runtime.py +255 -0
  104. rocket_engine/composition.py +2279 -0
  105. rocket_engine/execution/README.md +30 -0
  106. rocket_engine/execution/__init__.py +150 -0
  107. rocket_engine/execution/apig/__init__.py +1 -0
  108. rocket_engine/execution/apig/pool.py +463 -0
  109. rocket_engine/execution/apig/submit.py +124 -0
  110. rocket_engine/execution/block_runs_scope.py +479 -0
  111. rocket_engine/execution/coalesce.py +414 -0
  112. rocket_engine/execution/console/__init__.py +1 -0
  113. rocket_engine/execution/console/apply_session.py +318 -0
  114. rocket_engine/execution/console/lifecycle.py +265 -0
  115. rocket_engine/execution/dispatch.py +205 -0
  116. rocket_engine/execution/errors.py +31 -0
  117. rocket_engine/execution/ports.py +797 -0
  118. rocket_engine/execution/pyexec/__init__.py +1 -0
  119. rocket_engine/execution/pyexec/apply_context.py +547 -0
  120. rocket_engine/execution/pyexec/approvals.py +173 -0
  121. rocket_engine/execution/pyexec/runner.py +404 -0
  122. rocket_engine/execution/pyexec/support.py +464 -0
  123. rocket_engine/execution/python_exec.py +897 -0
  124. rocket_engine/execution/queue.py +85 -0
  125. rocket_engine/execution/replay_safe.py +601 -0
  126. rocket_engine/execution/routing/__init__.py +1 -0
  127. rocket_engine/execution/routing/calls.py +364 -0
  128. rocket_engine/execution/routing/namespace.py +785 -0
  129. rocket_engine/execution/routing/ports.py +102 -0
  130. rocket_engine/execution/routing/tools.py +346 -0
  131. rocket_engine/execution/runner.py +311 -0
  132. rocket_engine/execution/runtime.py +1149 -0
  133. rocket_engine/execution/service.py +279 -0
  134. rocket_engine/execution/worker.py +464 -0
  135. rocket_engine/interfaces/README.md +26 -0
  136. rocket_engine/interfaces/__init__.py +58 -0
  137. rocket_engine/interfaces/apig_assets.py +407 -0
  138. rocket_engine/interfaces/celllink.py +461 -0
  139. rocket_engine/interfaces/documents.py +92 -0
  140. rocket_engine/interfaces/envelopes.py +131 -0
  141. rocket_engine/interfaces/errors.py +16 -0
  142. rocket_engine/interfaces/layout.py +410 -0
  143. rocket_engine/interfaces/layout_schema.yaml +278 -0
  144. rocket_engine/interfaces/paths.py +223 -0
  145. rocket_engine/interfaces/pdks.py +550 -0
  146. rocket_engine/interfaces/ports.py +72 -0
  147. rocket_engine/interfaces/references.py +115 -0
  148. rocket_engine/interfaces/refs.py +85 -0
  149. rocket_engine/interfaces/runparams.py +304 -0
  150. rocket_engine/interfaces/setup.py +552 -0
  151. rocket_engine/interfaces/setup_catalogue.yaml +243 -0
  152. rocket_engine/interfaces/setup_probes.py +66 -0
  153. rocket_engine/interfaces/vocabulary.py +58 -0
  154. rocket_engine/interfaces/yaml_dump.py +31 -0
  155. rocket_engine/library/README.md +22 -0
  156. rocket_engine/library/__init__.py +13 -0
  157. rocket_engine/library/apig_catalog.py +371 -0
  158. rocket_engine/library/catalog.py +185 -0
  159. rocket_engine/library/errors.py +16 -0
  160. rocket_engine/mutate/__init__.py +776 -0
  161. rocket_engine/mutate/operators.py +700 -0
  162. rocket_engine/ops/__init__.py +1 -0
  163. rocket_engine/ops/deferred.py +279 -0
  164. rocket_engine/ops/editors.py +404 -0
  165. rocket_engine/ops/evidence.py +194 -0
  166. rocket_engine/ops/gates.py +262 -0
  167. rocket_engine/ops/ports.py +76 -0
  168. rocket_engine/ops/queries.py +417 -0
  169. rocket_engine/ops/runs.py +553 -0
  170. rocket_engine/ops/submit.py +614 -0
  171. rocket_engine/project/README.md +27 -0
  172. rocket_engine/project/__init__.py +6 -0
  173. rocket_engine/project/canvas/__init__.py +55 -0
  174. rocket_engine/project/canvas/compile.py +109 -0
  175. rocket_engine/project/canvas/integrate.py +48 -0
  176. rocket_engine/project/canvas/schema.py +241 -0
  177. rocket_engine/project/canvas/templates.py +46 -0
  178. rocket_engine/project/canvas/validate.py +603 -0
  179. rocket_engine/project/edit/__init__.py +5 -0
  180. rocket_engine/project/edit/cas.py +87 -0
  181. rocket_engine/project/edit/transactions.py +282 -0
  182. rocket_engine/project/errors.py +16 -0
  183. rocket_engine/project/frontend13.py +685 -0
  184. rocket_engine/project/geometry/__init__.py +23 -0
  185. rocket_engine/project/geometry/ops.py +451 -0
  186. rocket_engine/project/geometry/snap.py +103 -0
  187. rocket_engine/project/ip/__init__.py +1 -0
  188. rocket_engine/project/ip/blocks.py +195 -0
  189. rocket_engine/project/ip/entry.schema.json +78 -0
  190. rocket_engine/project/ip/lifecycle.py +402 -0
  191. rocket_engine/project/ip/verbs.py +123 -0
  192. rocket_engine/project/layers/__init__.py +6 -0
  193. rocket_engine/project/layers/bindings.py +115 -0
  194. rocket_engine/project/layers/catalog.py +228 -0
  195. rocket_engine/project/layers/presets.py +43 -0
  196. rocket_engine/project/ledger/__init__.py +1 -0
  197. rocket_engine/project/ledger/docs.py +167 -0
  198. rocket_engine/project/ledger/entities.py +176 -0
  199. rocket_engine/project/ledger/events.py +29 -0
  200. rocket_engine/project/ledger/runs.py +257 -0
  201. rocket_engine/project/ledger/shared.py +174 -0
  202. rocket_engine/project/ledger/snapshots.py +385 -0
  203. rocket_engine/project/ledger/wires.py +552 -0
  204. rocket_engine/project/process/__init__.py +14 -0
  205. rocket_engine/project/process/model.py +193 -0
  206. rocket_engine/project/process/recipes.py +121 -0
  207. rocket_engine/project/schematic/__init__.py +1 -0
  208. rocket_engine/project/schematic/hierarchy.py +312 -0
  209. rocket_engine/project/schematic/instances.py +393 -0
  210. rocket_engine/project/schematic/sheets.py +198 -0
  211. rocket_engine/project/service.py +2941 -0
  212. rocket_engine/project/symbols/__init__.py +1 -0
  213. rocket_engine/project/symbols/cells.py +324 -0
  214. rocket_engine/project/symbols/linkage.py +356 -0
  215. rocket_engine/project/symbols/symbols.py +352 -0
  216. rocket_engine/project/templates/__init__.py +303 -0
  217. rocket_engine/project/templates/errors.py +25 -0
  218. rocket_engine/project/testbench/__init__.py +49 -0
  219. rocket_engine/project/testbench/blocks.py +619 -0
  220. rocket_engine/project/testbench/templates.py +134 -0
  221. rocket_engine/stores/README.md +12 -0
  222. rocket_engine/stores/__init__.py +6 -0
  223. rocket_engine/stores/ecs.py +1508 -0
  224. rocket_engine/stores/layers.py +1023 -0
  225. rocket_engine/stores/settings.py +1188 -0
  226. rocket_engine/stores/wireplace.py +1324 -0
  227. rocket_engine/workflows/README.md +18 -0
  228. rocket_engine/workflows/__init__.py +53 -0
  229. rocket_engine/workflows/capability_plan.py +102 -0
  230. rocket_engine/workflows/completion.py +228 -0
  231. rocket_engine/workflows/connectgraph.py +734 -0
  232. rocket_engine/workflows/connectgraph_emit.py +336 -0
  233. rocket_engine/workflows/gates.py +144 -0
  234. rocket_engine/workflows/photonic_wire.py +76 -0
  235. rocket_engine/workflows/stages.py +324 -0
  236. rocket_engine/workflows/viewsync.py +399 -0
  237. rocket_engine-0.0.2b1.dist-info/METADATA +200 -0
  238. rocket_engine-0.0.2b1.dist-info/RECORD +241 -0
  239. rocket_engine-0.0.2b1.dist-info/WHEEL +4 -0
  240. rocket_engine-0.0.2b1.dist-info/entry_points.txt +2 -0
  241. rocket_engine-0.0.2b1.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,318 @@
1
+ """Approval-bound console cells on the atomic apply-context.
2
+
3
+ CONSOLE-SESSION-MIGRATE (ADR-016 follow-up): the old in-memory
4
+ `ConsoleSession` (approved code plus history cursor, environment
5
+ digest, and revision binding, with reset killing every outstanding
6
+ approval) now rides `ApplySession` instead of standing beside it.
7
+ One object owns both halves: the approval binding admits one
8
+ worker run, and any worker-staged intents the run leaves behind
9
+ land as exactly one atomic batch through `Engine.apply_batch` —
10
+ the same path the editor surface uses, so console writes are
11
+ never a side-channel writer.
12
+
13
+ Identity follows ADR-016: one live instance per (console,
14
+ generation). `reset` retires the apply side (dropping buffered
15
+ and staged intents), rotates the generation, kills every
16
+ outstanding approval, and rearms the context for the next
17
+ generation. `retire` from `ApplySession` is permanent and is
18
+ only used inside `reset`, never by callers.
19
+ """
20
+
21
+ import uuid
22
+
23
+ from ..errors import ExecutionError
24
+ from ..pyexec.apply_context import ApplySession
25
+
26
+ CONTRACT_VERSION = "rocketeda/tooling/v0.1"
27
+
28
+ _HELP = {
29
+ "run_drc": {
30
+ "signature": "run_drc(target, deck, profile?)",
31
+ "example": "op = run_drc(target='amplifier', deck='sky130A/decks.drc')",
32
+ },
33
+ "run_spice": {
34
+ "signature": "run_spice(netlist, benches, corners?)",
35
+ "example": "op = run_spice(netlist='amp_tb', benches=['ac', 'tran'])",
36
+ },
37
+ "python_execute": {
38
+ "signature": "python_execute(code_ref, profile)",
39
+ "example": "cell = python_execute(code_ref='analysis/fit.py', profile='design')",
40
+ },
41
+ }
42
+
43
+
44
+ def get_help(topic):
45
+ """Return versioned help metadata. Pure data — executes nothing.
46
+
47
+ Args:
48
+ topic: Help topic name (run_drc, run_spice, python_execute).
49
+
50
+ Returns:
51
+ Help entry with topic, contract version, signature, and
52
+ example.
53
+
54
+ Raises:
55
+ ExecutionError: unknown_topic for an unlisted topic.
56
+ """
57
+ try:
58
+ entry = _HELP[topic]
59
+ except KeyError:
60
+ raise ExecutionError("unknown_topic", f"Unknown help topic: {topic}.") from None
61
+ return {"topic": topic, "version": CONTRACT_VERSION, **entry}
62
+
63
+
64
+ class ConsoleSession(ApplySession):
65
+ """Approval-bound console cells on one atomic apply-context.
66
+
67
+ The approval half pins one cell plus its context (full worker
68
+ source, history cursor, environment digest, expected revision);
69
+ admitting re-checks every field and any drift fails instead
70
+ of running a stranger's cell. The apply half stages worker
71
+ intent lines and emits one batch per outermost clean exit;
72
+ `land_worker_intents` drains a worker ops log and lands the
73
+ emitted batch through `Engine.apply_batch`.
74
+
75
+ Args:
76
+ project_id: Project this session approves cells for.
77
+ actor: Actor recorded on approvals.
78
+ console_id: Console owning this context; minted when
79
+ omitted.
80
+ pdk_name: PDK auto-activated at first block entry.
81
+ activate_pdk: Callable mapping the PDK name to proof;
82
+ a no-op activator when omitted.
83
+ resolve_layer: Callable mapping a layer spec to its lid;
84
+ the identity mapping when omitted.
85
+ emit: Callable receiving each outermost-exit batch;
86
+ batches accumulate on `emitted` when omitted.
87
+ """
88
+
89
+ def __init__(self, project_id, actor, console_id=None,
90
+ pdk_name="sky130", activate_pdk=None,
91
+ resolve_layer=None, emit=None):
92
+ self._actor = actor
93
+ self._approvals = {}
94
+ self._cursor = 0
95
+ self._emitted = []
96
+ super().__init__(
97
+ emit=emit if emit is not None else self._emitted.append,
98
+ project_id=project_id,
99
+ console_id=console_id or uuid.uuid4().hex,
100
+ generation=uuid.uuid4().hex,
101
+ pdk_name=pdk_name,
102
+ activate_pdk=(activate_pdk if activate_pdk is not None
103
+ else (lambda name: {"pdk": name,
104
+ "activated": True})),
105
+ resolve_layer=(resolve_layer if resolve_layer is not None
106
+ else (lambda spec: spec)),
107
+ )
108
+
109
+ @property
110
+ def console_id(self):
111
+ """Console owning this context (fixed for its lifetime).
112
+
113
+ Returns:
114
+ Console id string.
115
+ """
116
+ return self._console_id
117
+
118
+ @property
119
+ def generation(self):
120
+ """Current approval generation; reset retires it.
121
+
122
+ Reads and writes the generation the apply-context
123
+ stamps on every emitted batch, so approval fencing and
124
+ batch identity can never disagree.
125
+
126
+ Returns:
127
+ Generation id string.
128
+ """
129
+ return self._generation
130
+
131
+ @generation.setter
132
+ def generation(self, value):
133
+ """Sync the generation (for example, after a restart reload).
134
+
135
+ Args:
136
+ value: Generation id to fence on from here.
137
+ """
138
+ self._generation = value
139
+
140
+ @property
141
+ def emitted(self):
142
+ """Batches emitted so far, oldest first (evidence only).
143
+
144
+ Returns:
145
+ Emitted batch mappings in emission order.
146
+ """
147
+ return list(self._emitted)
148
+
149
+ def request_cell_approval(self, code, history_cursor, env_digest,
150
+ expected_revision):
151
+ """Bind a pending approval to one cell plus its context.
152
+
153
+ Args:
154
+ code: Full worker source the approval pins.
155
+ history_cursor: History position the approval pins.
156
+ env_digest: Environment digest the approval pins.
157
+ expected_revision: Revision the approval pins.
158
+
159
+ Returns:
160
+ Approval record with approval_id and pending state.
161
+ """
162
+ approval_id = uuid.uuid4().hex
163
+ self._approvals[approval_id] = {
164
+ "approval_id": approval_id,
165
+ "code": code,
166
+ "history_cursor": history_cursor,
167
+ "env_digest": env_digest,
168
+ "expected_revision": expected_revision,
169
+ "state": "pending_approval",
170
+ }
171
+ return {"approval_id": approval_id, "state": "pending_approval"}
172
+
173
+ def _binding(self, approval_id):
174
+ """Return the live binding for one approval.
175
+
176
+ Args:
177
+ approval_id: Approval bound by request_cell_approval.
178
+
179
+ Returns:
180
+ Live approval record.
181
+
182
+ Raises:
183
+ ExecutionError: approval_invalid for an unknown or
184
+ expired approval.
185
+ """
186
+ try:
187
+ return self._approvals[approval_id]
188
+ except KeyError:
189
+ raise ExecutionError(
190
+ "approval_invalid", "Unknown or expired approval."
191
+ ) from None
192
+
193
+ def admit_cell(self, approval_id, code, history_cursor, env_digest,
194
+ expected_revision):
195
+ """Admit a cell whose context still matches its binding.
196
+
197
+ Any drift in code, cursor, env, or revision fails instead
198
+ of running a stranger's cell. This is the seam the worker
199
+ lane (`CellRunner.run_cell`) admits through.
200
+
201
+ Args:
202
+ approval_id: Approval bound by request_cell_approval.
203
+ code: Full worker source to re-check against the binding.
204
+ history_cursor: History position to re-check.
205
+ env_digest: Environment digest to re-check.
206
+ expected_revision: Revision to re-check.
207
+
208
+ Returns:
209
+ Approval record with approval_id and approved state.
210
+
211
+ Raises:
212
+ ExecutionError: approval_invalid for an unknown approval
213
+ or any binding drift.
214
+ """
215
+ binding = self._binding(approval_id)
216
+ if (
217
+ binding["code"] != code
218
+ or binding["history_cursor"] != history_cursor
219
+ or binding["env_digest"] != env_digest
220
+ or binding["expected_revision"] != expected_revision
221
+ ):
222
+ raise ExecutionError(
223
+ "approval_invalid", "Approval no longer matches its binding."
224
+ )
225
+ binding["state"] = "approved"
226
+ return {"approval_id": approval_id, "state": "approved"}
227
+
228
+ def execute_cell(self, approval_id, code):
229
+ """Record a cell execution without running code.
230
+
231
+ The worker run itself happens in the worker lane (admitted
232
+ through `admit_cell`); landing worker-staged intents is
233
+ `land_worker_intents`, not this method.
234
+
235
+ Args:
236
+ approval_id: Approval whose code must match.
237
+ code: Cell code to re-check against the binding.
238
+
239
+ Returns:
240
+ Record with the advanced cursor and recorded state.
241
+
242
+ Raises:
243
+ ExecutionError: approval_invalid for an unknown approval
244
+ or code that differs from the approved code.
245
+ """
246
+ binding = self._binding(approval_id)
247
+ if binding["code"] != code:
248
+ raise ExecutionError(
249
+ "approval_invalid", "Cell code differs from the approved code."
250
+ )
251
+ self._cursor += 1
252
+ return {"cursor": self._cursor, "state": "recorded"}
253
+
254
+ def land_worker_intents(self, engine, ops_log_path, label):
255
+ """Drain worker intent lines and land one atomic batch.
256
+
257
+ Lines shaped ``{"apply_intent": {...}}`` in the worker ops
258
+ log join the session; anything else in the log is not ours
259
+ and stays unread. The staged intents emit as one batch at
260
+ the outermost exit and land through
261
+ `Engine.apply_batch` — one transaction, one revision.
262
+
263
+ Args:
264
+ engine: Engine landing the batch; when it has no
265
+ `apply_batch`, nothing is collected and None
266
+ returns.
267
+ ops_log_path: Worker ops-log path to drain.
268
+ label: Batch label carried on the envelope.
269
+
270
+ Returns:
271
+ Landing with ok True, the engine receipt, and the
272
+ intent count — or None when nothing was staged
273
+ or the engine cannot land.
274
+
275
+ Raises:
276
+ ProjectError: retired_session on a retired session;
277
+ invalid_command for an unknown sink kind;
278
+ revision_conflict when the head moved under the
279
+ batch.
280
+ """
281
+ land = getattr(engine, "apply_batch", None)
282
+ if land is None:
283
+ return None
284
+ staged = self.collect_ops(ops_log_path)
285
+ if staged == 0 and not self._staged:
286
+ return None
287
+ with self.edit(label):
288
+ pass
289
+ batch = self.last_batch
290
+ if batch is None:
291
+ return None
292
+ receipt = land(batch)
293
+ return {"ok": True, "receipt": receipt,
294
+ "intents": len(batch["intents"]), "label": batch["label"]}
295
+
296
+ def reset(self):
297
+ """Retire the generation and kill every outstanding approval.
298
+
299
+ The apply side retires too (buffered and staged intents
300
+ drop), then the context rearms under a fresh generation:
301
+ in-flight blocks must never span a reset — blocks are
302
+ synchronous, so a reset lands between cells, never inside
303
+ one.
304
+
305
+ Returns:
306
+ Mapping with the fresh generation id.
307
+ """
308
+ self.retire()
309
+ self._generation = uuid.uuid4().hex
310
+ self._approvals = {}
311
+ self._cursor = 0
312
+ self._retired = False
313
+ self._depth = 0
314
+ self._buffer = []
315
+ self._span = []
316
+ self._staged = []
317
+ self._pdk_active = False
318
+ return {"generation": self._generation}
@@ -0,0 +1,265 @@
1
+ """Console lifecycle: open/status/reset/close on the shared envelope.
2
+
3
+ Identity plus generation, per the BWRAP-STUDY ruling: the console
4
+ is a plain python console on trusted-local user permissions, every
5
+ cell runs in a confined worker, and where bwrap is unprovable the
6
+ surface still records generations and approvals but admits
7
+ nothing. This module owns the recording half only — identity,
8
+ generation rotation, workspace binding — and never spawns a
9
+ worker, never touches the runner, and never changes sandbox
10
+ behavior.
11
+
12
+ Scoping decision (CONSOLE-LIFECYCLE): `stdio_console` keeps its
13
+ local-panel REPL protocol (`submit`/`history`/`reset` over stdio);
14
+ the Engine-side lifecycle operations (`python_open`,
15
+ `python_execute`, `python_reset`) travel as shared-envelope
16
+ command texts rendered here, so the adapt lane wires the three
17
+ operations through `console_envelope`/`parse_console_envelope`
18
+ without inventing a second envelope shape. The operations
19
+ themselves are vocabulary/composition wiring (adapt lane); this
20
+ module pins the envelope contract they must speak.
21
+
22
+ Seam: `session_for` exposes the underlying `ConsoleSession` to the
23
+ worker lane (`CellRunner.admit_cell`) and to tests; Engine wiring
24
+ calls `open_console`/`reset_console` and never reaches past them.
25
+ """
26
+
27
+ import uuid
28
+
29
+ from ruamel.yaml import YAML
30
+
31
+ from ..errors import ExecutionError
32
+ from ..routing.calls import admission_envelope
33
+ from .apply_session import ConsoleSession
34
+
35
+ #: Lifecycle operations converging on the shared envelope. The
36
+ #: Engine does not wire them yet (adapt lane); the envelope shape
37
+ #: here is the contract that wiring must speak.
38
+ CONSOLE_OPERATIONS = frozenset(
39
+ {"python_open", "python_execute", "python_reset"}
40
+ )
41
+
42
+ _yaml = YAML(typ="safe")
43
+
44
+
45
+ def console_envelope(operation, arguments):
46
+ """Render one console lifecycle command on the shared envelope.
47
+
48
+ The text uses the identical YAML dumper the Engine uses for
49
+ every admission, so equal calls admit with equal identity and
50
+ console history replays as command scripts by construction.
51
+
52
+ Args:
53
+ operation: Console lifecycle operation (python_open,
54
+ python_execute, python_reset).
55
+ arguments: Operation arguments mapping.
56
+
57
+ Returns:
58
+ Envelope command text for the call.
59
+
60
+ Raises:
61
+ ExecutionError: invalid_command for an operation outside
62
+ the console lifecycle set or unserializable arguments.
63
+ """
64
+ if operation not in CONSOLE_OPERATIONS:
65
+ raise ExecutionError(
66
+ "invalid_command",
67
+ f"Not a console lifecycle operation: {operation!r}.",
68
+ )
69
+ if not isinstance(arguments, dict):
70
+ raise ExecutionError(
71
+ "invalid_command",
72
+ f"Envelope needs mapping arguments, got {arguments!r}.",
73
+ )
74
+ try:
75
+ return admission_envelope(operation, arguments)
76
+ except ExecutionError as exc:
77
+ raise ExecutionError("invalid_command", str(exc)) from exc
78
+
79
+
80
+ def parse_console_envelope(text):
81
+ """Parse one console envelope back to operation + arguments.
82
+
83
+ Args:
84
+ text: Envelope command text from `console_envelope`.
85
+
86
+ Returns:
87
+ Mapping with operation and arguments.
88
+
89
+ Raises:
90
+ ExecutionError: invalid_command for unparsable text, a
91
+ non-mapping envelope, or an operation outside the
92
+ console lifecycle set.
93
+ """
94
+ if not isinstance(text, str) or not text.strip():
95
+ raise ExecutionError(
96
+ "invalid_command", "Console envelope must be text."
97
+ )
98
+ try:
99
+ document = _yaml.load(text)
100
+ except Exception as exc:
101
+ raise ExecutionError(
102
+ "invalid_command",
103
+ f"Console envelope does not parse: {exc}.",
104
+ ) from exc
105
+ if not isinstance(document, dict):
106
+ raise ExecutionError(
107
+ "invalid_command",
108
+ f"Console envelope must be a mapping, got {document!r}.",
109
+ )
110
+ operation = document.get("operation")
111
+ arguments = document.get("arguments")
112
+ if operation not in CONSOLE_OPERATIONS or not isinstance(
113
+ arguments, dict
114
+ ):
115
+ raise ExecutionError(
116
+ "invalid_command",
117
+ f"Not a console lifecycle envelope: {document!r}.",
118
+ )
119
+ return {"operation": operation, "arguments": dict(arguments)}
120
+
121
+
122
+ class ConsoleLifecycle:
123
+ """Console identity + generation registry (lifecycle only).
124
+
125
+ `open_console` mints one console per call — or reuses the live
126
+ handle when the same workspace root opens again (single
127
+ console per panel). `reset_console` retires the generation and
128
+ kills every outstanding approval through the session; live
129
+ workers are cancel territory, never reset. Nothing here
130
+ executes: admission and execution stay in the worker lane.
131
+
132
+ Args:
133
+ None.
134
+ """
135
+
136
+ def __init__(self):
137
+ self._consoles = {}
138
+ self._roots = {}
139
+
140
+ def open_console(self, project_id, actor, workspace_root=None):
141
+ """Open a console, reusing the live one for its workspace.
142
+
143
+ Args:
144
+ project_id: Project the console approves cells for.
145
+ actor: Actor recorded on approvals.
146
+ workspace_root: Panel workspace binding; one live
147
+ handle per root, unbound when omitted.
148
+
149
+ Returns:
150
+ Opening with console_id, project_id, generation, and
151
+ the workspace_root binding (None when unbound).
152
+ """
153
+ if workspace_root is not None:
154
+ live = self._roots.get(str(workspace_root))
155
+ if live is not None and live in self._consoles:
156
+ record = self._consoles[live]
157
+ return self._describe(record)
158
+ console_id = uuid.uuid4().hex
159
+ session = ConsoleSession(project_id, actor)
160
+ record = {
161
+ "console_id": console_id,
162
+ "project_id": project_id,
163
+ "actor": actor,
164
+ "workspace_root": (
165
+ None if workspace_root is None
166
+ else str(workspace_root)
167
+ ),
168
+ "session": session,
169
+ }
170
+ self._consoles[console_id] = record
171
+ if record["workspace_root"] is not None:
172
+ self._roots[record["workspace_root"]] = console_id
173
+ return self._describe(record)
174
+
175
+ def status(self, console_id):
176
+ """Read one console's lifecycle state (never executes).
177
+
178
+ Args:
179
+ console_id: Console from `open_console`.
180
+
181
+ Returns:
182
+ Status with console_id, project_id, generation, and
183
+ the workspace_root binding.
184
+
185
+ Raises:
186
+ ExecutionError: unknown_console for an id never
187
+ opened or already closed.
188
+ """
189
+ return self._describe(self._record(console_id))
190
+
191
+ def session_for(self, console_id):
192
+ """Return the live session for worker-lane admission.
193
+
194
+ Worker-lane seam: `CellRunner.admit_cell` binds through
195
+ the session; Engine wiring never reaches past
196
+ `open_console`/`reset_console`.
197
+
198
+ Args:
199
+ console_id: Console from `open_console`.
200
+
201
+ Returns:
202
+ Live `ConsoleSession` for the console.
203
+
204
+ Raises:
205
+ ExecutionError: unknown_console for an id never
206
+ opened or already closed.
207
+ """
208
+ return self._record(console_id)["session"]
209
+
210
+ def reset_console(self, console_id):
211
+ """Retire the generation; kill every outstanding approval.
212
+
213
+ Args:
214
+ console_id: Console from `open_console`.
215
+
216
+ Returns:
217
+ Mapping with the console_id and the fresh generation.
218
+
219
+ Raises:
220
+ ExecutionError: unknown_console for an id never
221
+ opened or already closed.
222
+ """
223
+ record = self._record(console_id)
224
+ outcome = record["session"].reset()
225
+ return {
226
+ "console_id": console_id,
227
+ "generation": outcome["generation"],
228
+ }
229
+
230
+ def close_console(self, console_id):
231
+ """Close a console and release its workspace binding.
232
+
233
+ Args:
234
+ console_id: Console from `open_console`.
235
+
236
+ Returns:
237
+ Closing with console_id and closed True.
238
+
239
+ Raises:
240
+ ExecutionError: unknown_console for an id never
241
+ opened or already closed.
242
+ """
243
+ record = self._record(console_id)
244
+ del self._consoles[console_id]
245
+ root = record["workspace_root"]
246
+ if root is not None and self._roots.get(root) == console_id:
247
+ del self._roots[root]
248
+ return {"console_id": console_id, "closed": True}
249
+
250
+ def _record(self, console_id):
251
+ try:
252
+ return self._consoles[console_id]
253
+ except KeyError:
254
+ raise ExecutionError(
255
+ "unknown_console", f"Unknown console: {console_id!r}."
256
+ ) from None
257
+
258
+ @staticmethod
259
+ def _describe(record):
260
+ return {
261
+ "console_id": record["console_id"],
262
+ "project_id": record["project_id"],
263
+ "generation": record["session"].generation,
264
+ "workspace_root": record["workspace_root"],
265
+ }