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,1151 @@
1
+ """Stdio console adapter: MATLAB-esque script console over shared dispatch.
2
+
3
+ A REPL gateway onto the shared op dispatch: cells are Python with the
4
+ engine world prebound (`project/tools/pdk/gf/np/plot`), executed one
5
+ per fresh WORKERS worker with zero setup, while every `tools.<op>`
6
+ call inside a cell compiles to the same op call the batch command
7
+ surface runs — console history replays as command scripts by
8
+ construction. This adapter faces the use-case layer (`execution`
9
+ dispatch, console approvals, workers) directly; it never calls
10
+ another adapter, and no adapter calls it.
11
+
12
+ Framing (what the IDE xterm panel consumes): the interactive loop
13
+ reads `submit` + cell lines until `:end` (multiline cells),
14
+ `submit-file PATH` (script-file cells), `history`, `reset`, `help`,
15
+ `quit`. Human frames print the cell id, verbatim worker stdout,
16
+ harvested op calls as replayable command lines, then the terminal
17
+ execution line. `--json` prints one stable envelope per command.
18
+
19
+ History persists under the console root (`console.yaml`,
20
+ `cells/<execution_id>.py` holding the exact displayed cell,
21
+ `executions/<execution_id>.yaml` holding the terminal record):
22
+ reopening the same root continues numbering. Single console per
23
+ panel: one live handle per root, no tabs, no console-id multiplexing.
24
+
25
+ Approvals are reused from `execution.console`, never reimplemented:
26
+ a human Run requests and admits approval of the exact displayed
27
+ cell in one step (identical binding). Runs are serial and each
28
+ worker run already revokes its credential and confirms its tree-kill
29
+ before its terminal record, so `reset` retires the generation and
30
+ kills every outstanding approval; interrupting a live run is cancel
31
+ territory, not reset. Worker-staged apply intents land through
32
+ `Engine.apply_batch` as one atomic batch per cell.
33
+
34
+ Local-only trust: this console speaks stdio to the local IDE panel
35
+ under OS identity. There is no token check, login, network listener,
36
+ remote execution, or second user to protect against — and deliberately
37
+ no code for any of those. See TRUST_NOTE.
38
+ """
39
+
40
+ import hashlib
41
+ import io
42
+ import json
43
+ import shlex
44
+ import sys
45
+ import threading
46
+ import uuid
47
+ from pathlib import Path
48
+
49
+ from ruamel.yaml import YAML
50
+
51
+ from ..clients.runtime import RuntimeClient
52
+ from ..composition import Engine
53
+ from ..execution.console.apply_session import ConsoleSession
54
+ from ..execution.pyexec.apply_context import session_prelude
55
+ from ..execution.routing.calls import (
56
+ collect_run_ids,
57
+ is_tier2,
58
+ stamp_tier2_call,
59
+ )
60
+ from ..execution.dispatch import OpDispatcher
61
+ from ..execution.errors import ExecutionError, UsageError
62
+ from .errors import error_payload
63
+ from ..execution.worker import CellRunner
64
+
65
+ __all__ = [
66
+ "CELL_PRELUDE_TEMPLATE",
67
+ "EXIT_OK",
68
+ "EXIT_USAGE",
69
+ "LOCAL_ONLY_TRUST",
70
+ "OPS_LOG_NAME",
71
+ "PREBOUND_NAMES",
72
+ "PRELUDE_DIGEST",
73
+ "TRUST_NOTE",
74
+ "USAGE",
75
+ "StdioConsole",
76
+ "build_cell_source",
77
+ "format_op_call",
78
+ "main",
79
+ ]
80
+
81
+ EXIT_OK = 0
82
+ EXIT_USAGE = 2
83
+
84
+ LOCAL_ONLY_TRUST = True
85
+
86
+ TRUST_NOTE = (
87
+ "Local-only trust: this console speaks stdio to the local IDE panel "
88
+ "under OS identity, with no auth step because there is no second party "
89
+ "— no tokens, no broker, no listener, no remote execution, no multi-user. "
90
+ "Human Run on the displayed cell is the approval; sandboxing (not "
91
+ "identity) is the safety boundary."
92
+ )
93
+
94
+ PREBOUND_NAMES = ("project", "tools", "pdk", "gf", "np", "plot")
95
+
96
+ OPS_LOG_NAME = "__console_ops.jsonl"
97
+
98
+ CELL_PRELUDE_TEMPLATE = '''\
99
+ # --- rocket-console prelude: pinned trusted bootstrap (do not edit) ---
100
+ import json as _json
101
+
102
+ __CONSOLE_PROJECT__ = @PROJECT_ID@
103
+ __CONSOLE_OPS_LOG__ = "__console_ops.jsonl"
104
+
105
+
106
+ class _OpRecorder:
107
+ """Prebound `tools`: tools.<op>(**kwargs) records one op call.
108
+
109
+ Calls append {"operation", "arguments"} JSON lines to the worker
110
+ scratch ops log; the host replays each through the shared op
111
+ dispatch after the worker exits, so validation and execution are
112
+ exactly what the batch surface runs. Positional args are rejected:
113
+ op calls are keyword-shaped.
114
+ """
115
+
116
+ def __init__(self):
117
+ self._index = 0
118
+
119
+ def __getattr__(self, operation):
120
+ def call(*args, **kwargs):
121
+ if args:
122
+ raise TypeError(
123
+ "op calls take keyword arguments only: %s." % operation
124
+ )
125
+ entry = {"operation": operation, "arguments": dict(kwargs)}
126
+ with open(__CONSOLE_OPS_LOG__, "a", encoding="utf-8") as handle:
127
+ handle.write(_json.dumps(entry, sort_keys=True) + "\\n")
128
+ self._index += 1
129
+ return {"state": "recorded", "operation": operation,
130
+ "index": self._index}
131
+
132
+ return call
133
+
134
+
135
+ tools = _OpRecorder()
136
+
137
+
138
+ class _Project:
139
+ """Prebound `project`: local identity plus query recording."""
140
+
141
+ def __init__(self, project_id):
142
+ self.id = project_id
143
+
144
+ def top_ref(self):
145
+ return {"project": self.id, "ref": "top"}
146
+
147
+ def snapshot(self):
148
+ return tools.get_project_snapshot()
149
+
150
+
151
+ project = _Project(__CONSOLE_PROJECT__)
152
+
153
+
154
+ class _Pdk:
155
+ """Prebound `pdk`: worker-local view (discovery runs on the host)."""
156
+
157
+ def describe(self):
158
+ return {"project": project.id, "profile": "local",
159
+ "note": "worker-local view;"
160
+ " engine catalog discovery runs on the host."}
161
+
162
+
163
+ pdk = _Pdk()
164
+
165
+
166
+ class _MissingBinding:
167
+ """Honest placeholder: missing profiles report, never fake."""
168
+
169
+ def __init__(self, name, hint):
170
+ self._name = name
171
+ self._hint = hint
172
+
173
+ def __repr__(self):
174
+ return "<%s unavailable in this worker profile: %s>" % (
175
+ self._name, self._hint)
176
+
177
+ def __getattr__(self, item):
178
+ raise RuntimeError(
179
+ "%s unavailable in this worker profile: %s"
180
+ % (self._name, self._hint)
181
+ )
182
+
183
+ def __call__(self, *args, **kwargs):
184
+ raise RuntimeError(
185
+ "%s unavailable in this worker profile: %s"
186
+ % (self._name, self._hint)
187
+ )
188
+
189
+
190
+ try:
191
+ import gdsfactory as gf
192
+ except Exception as _gf_error:
193
+ gf = _MissingBinding("gf", "gdsfactory not installed (%s)" % _gf_error)
194
+
195
+ try:
196
+ import numpy as np
197
+ except Exception as _np_error:
198
+ np = _MissingBinding("np", "numpy not installed (%s)" % _np_error)
199
+
200
+
201
+ class _Plot:
202
+ """Prebound `plot`: availability flag plus figure helper."""
203
+
204
+ def __init__(self):
205
+ try:
206
+ import matplotlib
207
+
208
+ matplotlib.use("Agg")
209
+ from matplotlib import pyplot as _pyplot
210
+ except Exception as _plot_error:
211
+ self.available = False
212
+ self._error = str(_plot_error)
213
+ self._pyplot = None
214
+ else:
215
+ self.available = True
216
+ self._error = ""
217
+ self._pyplot = _pyplot
218
+
219
+ def __repr__(self):
220
+ if self.available:
221
+ return "<plot (matplotlib available)>"
222
+ return "<plot unavailable in this worker profile: %s>" % self._error
223
+
224
+ def figure(self, *args, **kwargs):
225
+ if not self.available:
226
+ raise RuntimeError(
227
+ "plot unavailable in this worker profile: %s" % self._error
228
+ )
229
+ return self._pyplot.figure(*args, **kwargs)
230
+
231
+
232
+ plot = _Plot()
233
+ '''
234
+
235
+ PRELUDE_DIGEST = hashlib.sha256(
236
+ CELL_PRELUDE_TEMPLATE.encode("utf-8")
237
+ ).hexdigest()
238
+
239
+ USAGE = """usage: rocket-console [--project ID] [--actor NAME] [--root DIR] [--json]
240
+
241
+ MATLAB-esque script console, stdio-only (local IDE panel). One console
242
+ per panel: no tabs, no multiplexing. Commands, one per line; blank
243
+ lines and `#` comments are skipped:
244
+
245
+ submit read cell lines until `:end`, then run them
246
+ submit-file PATH run a script file as one cell (history keeps a copy)
247
+ history list persisted executions
248
+ reset retire the generation, kill outstanding approvals
249
+ help print this usage
250
+ quit leave the console (EOF also leaves)
251
+
252
+ Every cell runs in a fresh worker with project/tools/pdk/gf/np/plot
253
+ prebound and zero setup. tools.<op> calls replay through the shared
254
+ op dispatch after the worker exits — the printed op lines rerun as a
255
+ batch script. --json prints one envelope per command instead.
256
+ """
257
+
258
+ _yaml = YAML(typ="safe")
259
+
260
+ _LIVE = set()
261
+ _LIVE_LOCK = threading.Lock()
262
+
263
+
264
+ def _render_preamble(project_id):
265
+ return CELL_PRELUDE_TEMPLATE.replace(
266
+ "@PROJECT_ID@", json.dumps(project_id)
267
+ )
268
+
269
+
270
+ def build_cell_source(code, project_id="default", console_id=None,
271
+ generation=None):
272
+ """Render the worker source: pinned preamble plus the display cell.
273
+
274
+ The project binding renders into the preamble template first, so
275
+ display text is concatenated verbatim afterwards — cell bytes that
276
+ happen to look like the slot stay data. The result ends with the
277
+ exact displayed cell (trailing newline normalized). When a console
278
+ binding is supplied, the apply-context prelude binds between
279
+ preamble and display, so worker cells may stage intents
280
+ (`session.cell(...)`, `session.rect(...)`) that the host lands
281
+ through `Engine.apply_batch`; without it the source is
282
+ byte-identical to preamble plus display.
283
+
284
+ Args:
285
+ code: Display cell text to wrap.
286
+ project_id: Project id rendered into the preamble binding.
287
+ console_id: Console id bound into the apply-context prelude;
288
+ no prelude binds when console_id and generation are both
289
+ omitted.
290
+ generation: Session generation bound into the apply-context
291
+ prelude for replay fencing.
292
+
293
+ Returns:
294
+ Worker source: pinned preamble, optional apply binding, plus
295
+ the display cell.
296
+ """
297
+ display = code if code.endswith("\n") else code + "\n"
298
+ source = _render_preamble(project_id) + "\n"
299
+ if console_id is not None or generation is not None:
300
+ source += (
301
+ "__PYEXEC_OPS_LOG__ = __CONSOLE_OPS_LOG__\n"
302
+ + session_prelude(project_id, console_id or "local",
303
+ generation or "local")
304
+ + "\n"
305
+ )
306
+ return source + display
307
+
308
+
309
+ def format_op_call(operation, arguments):
310
+ """Format one op call as a batch-surface command line (replayable).
311
+
312
+ Values render as JSON (a YAML subset, so types round-trip through
313
+ the one parser of record) and the whole `key=value` token is shell
314
+ quoted when needed. Argument order is sorted for stability.
315
+
316
+ Args:
317
+ operation: Cataloged operation or query name.
318
+ arguments: Operation arguments rendered as key=value pairs.
319
+
320
+ Returns:
321
+ Replayable batch-surface command line for the op call.
322
+ """
323
+ parts = [operation]
324
+ for key in sorted(arguments):
325
+ value_text = json.dumps(arguments[key], sort_keys=True, default=str)
326
+ parts.append(shlex.quote("%s=%s" % (key, value_text)))
327
+ return " ".join(parts)
328
+
329
+
330
+ def _dump_yaml(data):
331
+ buf = io.StringIO()
332
+ _yaml.dump(data, buf)
333
+ return buf.getvalue()
334
+
335
+
336
+ def _load_yaml(text):
337
+ return _yaml.load(text) or {}
338
+
339
+
340
+ class StdioConsole:
341
+ """One stdio console: cells in fresh workers, ops via dispatch.
342
+
343
+ Args:
344
+ engine: Engine ops replay against; constructed from
345
+ project_id/actor when omitted.
346
+ project_id: Project id stamped on every command.
347
+ actor: Actor stamped on every command.
348
+ root: Console state dir; defaults under cwd `.rocket/`.
349
+ cell_runner: Worker runner; constructed when omitted.
350
+ expected_revision: Revision cells bind approvals against.
351
+ """
352
+
353
+ def __init__(self, engine=None, project_id="default", actor="console",
354
+ root=None, cell_runner=None, expected_revision="local"):
355
+ self._engine = (
356
+ engine if engine is not None else Engine(project_id, actor=actor)
357
+ )
358
+ self._project_id = project_id
359
+ self._actor = actor
360
+ self._expected_revision = expected_revision
361
+ self._root = (
362
+ Path(root) if root is not None
363
+ else Path.cwd() / ".rocket" / "consoles" / "stdio"
364
+ )
365
+ key = str(self._root.resolve())
366
+ with _LIVE_LOCK:
367
+ if key in _LIVE:
368
+ raise ExecutionError(
369
+ "console_busy",
370
+ f"One console per panel: {self._root} is already open.",
371
+ )
372
+ _LIVE.add(key)
373
+ self._live_key = key
374
+ try:
375
+ self._root.mkdir(parents=True, exist_ok=True)
376
+ (self._root / "cells").mkdir(exist_ok=True)
377
+ (self._root / "executions").mkdir(exist_ok=True)
378
+ self._dispatch = OpDispatcher(
379
+ self._engine, project_id=project_id, actor=actor
380
+ )
381
+ self._runner = (
382
+ cell_runner if cell_runner is not None
383
+ else CellRunner(artifacts_root=str(self._root / "harvest"))
384
+ )
385
+ self._env_digest = hashlib.sha256(
386
+ _render_preamble(project_id).encode("utf-8")
387
+ ).hexdigest()
388
+ self._load_or_create()
389
+ except Exception:
390
+ self.close()
391
+ raise
392
+
393
+ def _load_or_create(self):
394
+ console_file = self._root / "console.yaml"
395
+ if console_file.is_file():
396
+ data = _load_yaml(console_file.read_text(encoding="utf-8"))
397
+ self._console_id = data.get("console_id") or uuid.uuid4().hex
398
+ self._generation = data.get("generation") or uuid.uuid4().hex
399
+ else:
400
+ self._console_id = uuid.uuid4().hex
401
+ self._generation = uuid.uuid4().hex
402
+ self._session = ConsoleSession(
403
+ self._project_id, self._actor, console_id=self._console_id)
404
+ self._session.generation = self._generation
405
+ self._cursor = len(list((self._root / "executions").glob("*.yaml")))
406
+ self._write_console_yaml()
407
+
408
+ def _write_console_yaml(self):
409
+ (self._root / "console.yaml").write_text(
410
+ _dump_yaml({
411
+ "console_id": self._console_id,
412
+ "generation": self._generation,
413
+ "project_id": self._project_id,
414
+ "actor": self._actor,
415
+ "prelude_digest": PRELUDE_DIGEST,
416
+ "env_digest": self._env_digest,
417
+ "executions": self._cursor,
418
+ }),
419
+ encoding="utf-8",
420
+ )
421
+
422
+ @property
423
+ def console_id(self):
424
+ """Stable console id, persisted across restarts.
425
+
426
+ Returns:
427
+ Console id string.
428
+ """
429
+ return self._console_id
430
+
431
+ @property
432
+ def generation(self):
433
+ """Current approval generation; reset retires it.
434
+
435
+ Returns:
436
+ Generation id string.
437
+ """
438
+ return self._generation
439
+
440
+ @property
441
+ def cursor(self):
442
+ """History cursor: count of persisted executions.
443
+
444
+ Returns:
445
+ Execution count as an int.
446
+ """
447
+ return self._cursor
448
+
449
+ @property
450
+ def root(self):
451
+ """Console state directory.
452
+
453
+ Returns:
454
+ Root path string.
455
+ """
456
+ return str(self._root)
457
+
458
+ @property
459
+ def engine(self):
460
+ """Engine ops replay against.
461
+
462
+ Returns:
463
+ Bound engine.
464
+ """
465
+ return self._engine
466
+
467
+ def runtime_client(self):
468
+ """Serve this console's engine through the shared client.
469
+
470
+ Cell execution is untouched — this binds the same
471
+ engine's integrated runtime (deferred submit, pump,
472
+ wait, cancel, watch) so console clients share one run
473
+ identity with every other transport.
474
+
475
+ Returns:
476
+ RuntimeClient bound to this console's engine.
477
+ """
478
+ return RuntimeClient(self._engine)
479
+
480
+ @property
481
+ def gate(self):
482
+ """Execution-scoped credential gate of the worker runner.
483
+
484
+ Returns:
485
+ Credential gate.
486
+ """
487
+ return self._runner.gate
488
+
489
+ @property
490
+ def admission(self):
491
+ """Worker isolation admission record.
492
+
493
+ Returns:
494
+ Admission mapping copy.
495
+ """
496
+ return dict(self._runner.admission)
497
+
498
+ def _cell_source(self, display):
499
+ """Render the worker source bound to this console generation.
500
+
501
+ Args:
502
+ display: Normalized display cell text.
503
+
504
+ Returns:
505
+ Full worker source with preamble plus the apply-context
506
+ binding for this console and generation.
507
+ """
508
+ return build_cell_source(
509
+ display, project_id=self._project_id,
510
+ console_id=self._console_id, generation=self._generation,
511
+ )
512
+
513
+ @staticmethod
514
+ def _normalize(code):
515
+ if not isinstance(code, str):
516
+ raise ExecutionError(
517
+ "invalid_cell", "Cell code must be UTF-8 text."
518
+ )
519
+ if not code.strip():
520
+ raise ExecutionError("empty_cell", "Cell is empty.")
521
+ return code if code.endswith("\n") else code + "\n"
522
+
523
+ def request_approval(self, code):
524
+ """Approve-later path: bind an approval to one display cell.
525
+
526
+ Args:
527
+ code: Display cell text to bind.
528
+
529
+ Returns:
530
+ Approval record with approval_id and pending state.
531
+
532
+ Raises:
533
+ ExecutionError: invalid_cell for non-text code,
534
+ empty_cell for a blank cell.
535
+ """
536
+ display = self._normalize(code)
537
+ full = self._cell_source(display)
538
+ return self._session.request_cell_approval(
539
+ code=full, history_cursor=self._cursor,
540
+ env_digest=self._env_digest,
541
+ expected_revision=self._expected_revision,
542
+ )
543
+
544
+ def run_approved(self, approval_id, code, artifacts=(), timeout_s=None,
545
+ on_live=None):
546
+ """Run a cell against an earlier approval (tamper raises).
547
+
548
+ Args:
549
+ approval_id: Approval bound by request_approval.
550
+ code: Display cell text; must match the binding.
551
+ artifacts: Scratch-relative paths to harvest.
552
+ timeout_s: Worker budget in seconds; runner default
553
+ when omitted.
554
+ on_live: Optional callback while the worker is live.
555
+
556
+ Returns:
557
+ Persisted execution record with state, stdio, ops
558
+ replay, and artifacts.
559
+
560
+ Raises:
561
+ ExecutionError: approval_invalid when the code drifts
562
+ from its binding, invalid_cell/empty_cell for bad
563
+ code.
564
+ """
565
+ display = self._normalize(code)
566
+ full = self._cell_source(display)
567
+ return self._run_full(approval_id, full, display,
568
+ {"source_file": None}, artifacts, timeout_s,
569
+ on_live)
570
+
571
+ def submit(self, code, artifacts=(), timeout_s=None, on_live=None):
572
+ """Human-Run path: approve and run the exact displayed cell.
573
+
574
+ Args:
575
+ code: Display cell text to approve and run.
576
+ artifacts: Scratch-relative paths to harvest.
577
+ timeout_s: Worker budget in seconds; runner default
578
+ when omitted.
579
+ on_live: Optional callback while the worker is live.
580
+
581
+ Returns:
582
+ Persisted execution record with state, stdio, ops
583
+ replay, and artifacts.
584
+
585
+ Raises:
586
+ ExecutionError: invalid_cell for non-text code,
587
+ empty_cell for a blank cell.
588
+ """
589
+ display = self._normalize(code)
590
+ full = self._cell_source(display)
591
+ approval = self._session.request_cell_approval(
592
+ code=full, history_cursor=self._cursor,
593
+ env_digest=self._env_digest,
594
+ expected_revision=self._expected_revision,
595
+ )
596
+ return self._run_full(approval["approval_id"], full, display,
597
+ {"source_file": None}, artifacts, timeout_s,
598
+ on_live)
599
+
600
+ def submit_file(self, path, artifacts=(), timeout_s=None, on_live=None):
601
+ """Run a script file as one cell; history keeps its own copy.
602
+
603
+ Args:
604
+ path: Script file path to run as one cell.
605
+ artifacts: Scratch-relative paths to harvest.
606
+ timeout_s: Worker budget in seconds; runner default
607
+ when omitted.
608
+ on_live: Optional callback while the worker is live.
609
+
610
+ Returns:
611
+ Persisted execution record with state, stdio, ops
612
+ replay, and artifacts.
613
+
614
+ Raises:
615
+ ExecutionError: cell_unreadable when the file cannot
616
+ be read, invalid_cell/empty_cell for bad content.
617
+ """
618
+ try:
619
+ text = Path(path).read_text(encoding="utf-8")
620
+ except OSError as exc:
621
+ raise ExecutionError(
622
+ "cell_unreadable", f"Cannot read cell file {path}: {exc}."
623
+ ) from exc
624
+ display = self._normalize(text)
625
+ full = self._cell_source(display)
626
+ approval = self._session.request_cell_approval(
627
+ code=full, history_cursor=self._cursor,
628
+ env_digest=self._env_digest,
629
+ expected_revision=self._expected_revision,
630
+ )
631
+ return self._run_full(approval["approval_id"], full, display,
632
+ {"source_file": str(path)}, artifacts,
633
+ timeout_s, on_live)
634
+
635
+ def _run_full(self, approval_id, full, display, extra, artifacts,
636
+ timeout_s, on_live):
637
+ execution_id = uuid.uuid4().hex
638
+ cell_rel = "cells/%s.py" % execution_id
639
+ (self._root / cell_rel).write_text(display, encoding="utf-8")
640
+ base = {
641
+ "execution_id": execution_id,
642
+ "cell": cell_rel,
643
+ "code_sha256": hashlib.sha256(
644
+ display.encode("utf-8")
645
+ ).hexdigest(),
646
+ "history_cursor": self._cursor + 1,
647
+ }
648
+ try:
649
+ result = self._runner.run_cell(
650
+ self._session, approval_id, full,
651
+ history_cursor=self._cursor, env_digest=self._env_digest,
652
+ expected_revision=self._expected_revision,
653
+ artifacts=(OPS_LOG_NAME,) + tuple(artifacts),
654
+ timeout_s=timeout_s, on_live=on_live,
655
+ )
656
+ except ExecutionError as exc:
657
+ if exc.code == "approval_invalid":
658
+ raise
659
+ record = {
660
+ **base,
661
+ "state": "not_run",
662
+ "ok": False,
663
+ "code": exc.code,
664
+ "reason": str(exc),
665
+ "exit_code": None,
666
+ "stdout": "",
667
+ "stderr": "",
668
+ "partial": False,
669
+ "ops": [],
670
+ "op_results": [],
671
+ "ops_ok": True,
672
+ "ops_skipped": 0,
673
+ "apply": None,
674
+ "effects": {"run_ids": [], "replayed": True},
675
+ "cli_script": [],
676
+ "artifacts": [],
677
+ "rejected_artifacts": [],
678
+ "network_isolated": self.admission.get(
679
+ "network_isolated", False),
680
+ "run_id": None,
681
+ **extra,
682
+ }
683
+ self._persist(record)
684
+ return record
685
+ ops, skipped = self._load_harvested_ops(result)
686
+ op_results = self._replay(ops)
687
+ apply_entry = self._land_apply_intents(result, execution_id)
688
+ record = {
689
+ **base,
690
+ "state": result["state"],
691
+ "ok": bool(result["ok"]),
692
+ "reason": result.get("reason"),
693
+ "exit_code": result.get("exit_code"),
694
+ "stdout": result.get("stdout", ""),
695
+ "stderr": result.get("stderr", ""),
696
+ "partial": bool(result.get("partial", False)),
697
+ "ops": ops,
698
+ "op_results": op_results,
699
+ "ops_ok": all(item["ok"] for item in op_results),
700
+ "ops_skipped": skipped,
701
+ "apply": apply_entry,
702
+ "effects": {"run_ids": collect_run_ids(op_results),
703
+ "replayed": True},
704
+ "cli_script": [
705
+ format_op_call(op["operation"], op["arguments"]) for op in ops
706
+ ],
707
+ "artifacts": result.get("artifacts", []),
708
+ "rejected_artifacts": result.get("rejected_artifacts", []),
709
+ "network_isolated": result.get("network_isolated", False),
710
+ "credential_revoked": result.get("credential_revoked", False),
711
+ "tree_killed": result.get("tree_killed", False),
712
+ "run_id": result.get("run_id"),
713
+ **extra,
714
+ }
715
+ self._persist(record)
716
+ return record
717
+
718
+ def _persist(self, record):
719
+ stored = {
720
+ **record,
721
+ "console_id": self._console_id,
722
+ "generation": self._generation,
723
+ "project_id": self._project_id,
724
+ "prelude_digest": PRELUDE_DIGEST,
725
+ "env_digest": self._env_digest,
726
+ "expected_revision": self._expected_revision,
727
+ }
728
+ (self._root / "executions" / ("%s.yaml" % record["execution_id"])
729
+ ).write_text(_dump_yaml(stored), encoding="utf-8")
730
+ self._cursor += 1
731
+ self._write_console_yaml()
732
+
733
+ @staticmethod
734
+ def _load_harvested_ops(result):
735
+ ops, skipped = [], 0
736
+ for entry in result.get("artifacts", []):
737
+ if entry.get("name") != OPS_LOG_NAME:
738
+ continue
739
+ try:
740
+ lines = Path(entry["path"]).read_text(
741
+ encoding="utf-8").splitlines()
742
+ except OSError:
743
+ skipped += 1
744
+ continue
745
+ for line in lines:
746
+ if not line.strip():
747
+ continue
748
+ try:
749
+ item = json.loads(line)
750
+ except ValueError:
751
+ skipped += 1
752
+ continue
753
+ if (
754
+ not isinstance(item, dict)
755
+ or not isinstance(item.get("operation"), str)
756
+ or not isinstance(item.get("arguments"), dict)
757
+ ):
758
+ skipped += 1
759
+ continue
760
+ ops.append({"operation": item["operation"],
761
+ "arguments": dict(item["arguments"])})
762
+ return ops, skipped
763
+
764
+ def _replay(self, ops):
765
+ replayed = []
766
+ try:
767
+ head = self._engine.snapshot()["current_revision"]
768
+ except Exception:
769
+ head = None
770
+ for op in ops:
771
+ operation = op["operation"]
772
+ arguments = op["arguments"]
773
+ request_id = None
774
+ if head is not None and is_tier2(operation):
775
+ arguments, request_id = stamp_tier2_call(
776
+ operation, arguments, head
777
+ )
778
+ try:
779
+ payload = self._dispatch.execute(
780
+ operation, arguments, request_id=request_id
781
+ )
782
+ except Exception as exc: # per-op truth: record, never raise
783
+ replayed.append({
784
+ "operation": operation,
785
+ "ok": False,
786
+ "error": {
787
+ "code": getattr(exc, "code", None)
788
+ or "internal_error",
789
+ "message": str(exc),
790
+ },
791
+ })
792
+ continue
793
+ replayed.append({
794
+ "operation": operation,
795
+ "ok": True,
796
+ "result": json.loads(json.dumps(payload, default=str)),
797
+ })
798
+ return replayed
799
+
800
+ def _land_apply_intents(self, result, execution_id):
801
+ """Land worker-staged apply intents as one atomic batch.
802
+
803
+ The harvested ops log may carry ``{"apply_intent": ...}``
804
+ lines (staged by the apply-context prelude when the cell
805
+ calls `session.cell(...)` or `session.rect(...)`); they
806
+ land through `Engine.apply_batch` exactly like editor
807
+ writes. Op-call replay is untouched — this covers only
808
+ the intent lines. Landing never breaks the cell record:
809
+ a conflict records truthfully beside the completed run.
810
+
811
+ Args:
812
+ result: Terminal worker record with harvested artifacts.
813
+ execution_id: Execution id carried on the batch label.
814
+
815
+ Returns:
816
+ Landing with ok True, the engine receipt, and the
817
+ intent count — or None when nothing was staged or
818
+ the engine cannot land.
819
+ """
820
+ if getattr(self._engine, "apply_batch", None) is None:
821
+ return None
822
+ for entry in result.get("artifacts", []):
823
+ if entry.get("name") != OPS_LOG_NAME:
824
+ continue
825
+ if not entry.get("path"):
826
+ continue
827
+ try:
828
+ landed = self._session.land_worker_intents(
829
+ self._engine, entry["path"],
830
+ "cell %s" % execution_id,
831
+ )
832
+ except Exception as exc: # landing truth, never a raise
833
+ return {
834
+ "ok": False,
835
+ "error": {
836
+ "code": getattr(exc, "code", None)
837
+ or "internal_error",
838
+ "message": str(exc),
839
+ },
840
+ }
841
+ if landed is not None:
842
+ return landed
843
+ return None
844
+
845
+ def history(self):
846
+ """Persisted executions, oldest first (survives restarts).
847
+
848
+ Returns:
849
+ Execution records sorted by history cursor.
850
+ """
851
+ records = []
852
+ for path in (self._root / "executions").glob("*.yaml"):
853
+ records.append(_load_yaml(path.read_text(encoding="utf-8")))
854
+ records.sort(key=lambda item: item.get("history_cursor", 0))
855
+ return records
856
+
857
+ def reset(self):
858
+ """Retire the generation and kill every outstanding approval.
859
+
860
+ Returns:
861
+ Mapping with the fresh generation id.
862
+ """
863
+ out = self._session.reset()
864
+ self._generation = out["generation"]
865
+ self._write_console_yaml()
866
+ return {"generation": self._generation}
867
+
868
+ def close(self):
869
+ """Release this console's single-panel slot (idempotent)."""
870
+ key = getattr(self, "_live_key", None)
871
+ if key is None:
872
+ return
873
+ with _LIVE_LOCK:
874
+ _LIVE.discard(key)
875
+ self._live_key = None
876
+
877
+ def __enter__(self):
878
+ return self
879
+
880
+ def __exit__(self, *exc_info):
881
+ self.close()
882
+ return False
883
+
884
+ def __del__(self):
885
+ try:
886
+ self.close()
887
+ except Exception:
888
+ pass
889
+
890
+
891
+ def _split_flag(token, name):
892
+ prefix = "--%s=" % name
893
+ if token.startswith(prefix):
894
+ return token[len(prefix):]
895
+ return None
896
+
897
+
898
+ def _parse_argv(argv):
899
+ options = {"json": False, "root": None, "project": "default",
900
+ "actor": "console", "help": False}
901
+ rest = []
902
+ i = 0
903
+ while i < len(argv):
904
+ token = argv[i]
905
+ if token == "--json":
906
+ options["json"] = True
907
+ elif token in ("--help", "-h"):
908
+ options["help"] = True
909
+ elif token == "--root":
910
+ i += 1
911
+ if i >= len(argv):
912
+ raise UsageError(
913
+ "invalid_command", "`--root` needs a directory.")
914
+ options["root"] = argv[i]
915
+ elif token == "--project":
916
+ i += 1
917
+ if i >= len(argv):
918
+ raise UsageError("invalid_command", "`--project` needs an ID.")
919
+ options["project"] = argv[i]
920
+ elif token == "--actor":
921
+ i += 1
922
+ if i >= len(argv):
923
+ raise UsageError("invalid_command", "`--actor` needs a NAME.")
924
+ options["actor"] = argv[i]
925
+ elif _split_flag(token, "root") is not None:
926
+ options["root"] = _split_flag(token, "root")
927
+ elif _split_flag(token, "project") is not None:
928
+ options["project"] = _split_flag(token, "project")
929
+ elif _split_flag(token, "actor") is not None:
930
+ options["actor"] = _split_flag(token, "actor")
931
+ elif token == "--":
932
+ rest.extend(argv[i + 1:])
933
+ break
934
+ elif token.startswith("-"):
935
+ raise UsageError("invalid_command", f"Unknown flag: {token}.")
936
+ else:
937
+ rest.extend(argv[i:])
938
+ break
939
+ i += 1
940
+ return options, rest
941
+
942
+
943
+ def _emit_result(command, result, as_json, stdout):
944
+ if as_json:
945
+ stdout.write(json.dumps(
946
+ {"command": command, "ok": True, "result": result},
947
+ sort_keys=True, default=str,
948
+ ) + "\n")
949
+ return
950
+ if command == "history":
951
+ executions = result["executions"]
952
+ stdout.write("--- history (%d) ---\n" % len(executions))
953
+ for item in executions:
954
+ stdout.write("cursor=%s %s %s ok=%s\n" % (
955
+ item.get("history_cursor"), item.get("execution_id"),
956
+ item.get("state"), item.get("ok"),
957
+ ))
958
+ return
959
+ if command == "reset":
960
+ stdout.write("--- reset generation=%s ---\n"
961
+ % result["generation"])
962
+ return
963
+ stdout.write("--- cell %s cursor=%s ---\n" % (
964
+ result["execution_id"], result["history_cursor"]))
965
+ if result.get("stdout"):
966
+ stdout.write(result["stdout"])
967
+ if not result["stdout"].endswith("\n"):
968
+ stdout.write("\n")
969
+ if result.get("stderr"):
970
+ stdout.write("--- stderr ---\n")
971
+ stdout.write(result["stderr"])
972
+ if not result["stderr"].endswith("\n"):
973
+ stdout.write("\n")
974
+ script = result.get("cli_script", [])
975
+ stdout.write("--- ops (%d) ---\n" % len(script))
976
+ if script:
977
+ for line in script:
978
+ stdout.write(line + "\n")
979
+ else:
980
+ stdout.write("(none)\n")
981
+ for item in result.get("op_results", []):
982
+ if not item["ok"]:
983
+ stdout.write("op %s: error [%s] %s\n" % (
984
+ item["operation"], item["error"]["code"],
985
+ item["error"]["message"],
986
+ ))
987
+ if result.get("state") == "not_run":
988
+ stdout.write("--- not run [%s] ---\n%s\n" % (
989
+ result.get("code"), result.get("reason")))
990
+ stdout.write("--- execution %s ok=%s ---\n" % (
991
+ result["state"], "true" if result["ok"] else "false"))
992
+
993
+
994
+ def _emit_error(command, exc, as_json, stdout, stderr):
995
+ payload = error_payload(exc)
996
+ if as_json:
997
+ stdout.write(json.dumps(
998
+ {"command": command, "ok": False, "error": payload},
999
+ sort_keys=True, default=str,
1000
+ ) + "\n")
1001
+ else:
1002
+ line = "error [%s]: %s" % (payload["code"], payload["message"])
1003
+ if payload.get("fields"):
1004
+ line += " (fields: %s)" % ", ".join(payload["fields"])
1005
+ stderr.write(line + "\n")
1006
+
1007
+
1008
+ def _do_submit(console, options, code, stdout, stderr):
1009
+ try:
1010
+ record = console.submit(code)
1011
+ except ExecutionError as exc:
1012
+ _emit_error("submit", exc, options["json"], stdout, stderr)
1013
+ return
1014
+ _emit_result("submit", record, options["json"], stdout)
1015
+
1016
+
1017
+ def _do_submit_file(console, options, path, stdout, stderr):
1018
+ try:
1019
+ record = console.submit_file(path)
1020
+ except ExecutionError as exc:
1021
+ _emit_error("submit-file", exc, options["json"], stdout, stderr)
1022
+ return
1023
+ _emit_result("submit-file", record, options["json"], stdout)
1024
+
1025
+
1026
+ def _do_history(console, options, stdout):
1027
+ _emit_result("history", {"executions": console.history()},
1028
+ options["json"], stdout)
1029
+
1030
+
1031
+ def _do_reset(console, options, stdout):
1032
+ _emit_result("reset", console.reset(), options["json"], stdout)
1033
+
1034
+
1035
+ def _run_session(console, options, stdin, stdout, stderr):
1036
+ interactive = stdin.isatty() if hasattr(stdin, "isatty") else False
1037
+ if interactive:
1038
+ stdout.write("console> ")
1039
+ stdout.flush()
1040
+ while True:
1041
+ line = stdin.readline()
1042
+ if line == "":
1043
+ break
1044
+ stripped = line.strip()
1045
+ if interactive:
1046
+ stdout.write("console> ")
1047
+ stdout.flush()
1048
+ if not stripped or stripped.startswith("#"):
1049
+ continue
1050
+ if stripped in ("exit", "quit"):
1051
+ break
1052
+ if stripped == "help":
1053
+ stdout.write(USAGE)
1054
+ continue
1055
+ if stripped == "submit":
1056
+ cell_lines = []
1057
+ while True:
1058
+ cell_line = stdin.readline()
1059
+ if cell_line == "":
1060
+ break
1061
+ if cell_line.strip() == ":end":
1062
+ break
1063
+ cell_lines.append(cell_line)
1064
+ eof = cell_line == ""
1065
+ code = "".join(cell_lines)
1066
+ if eof and not code.strip():
1067
+ break
1068
+ _do_submit(console, options, code, stdout, stderr)
1069
+ if eof:
1070
+ break
1071
+ continue
1072
+ if stripped == "submit-file" or stripped.startswith("submit-file "):
1073
+ _, _, path = stripped.partition(" ")
1074
+ path = path.strip()
1075
+ if not path:
1076
+ _emit_error("submit-file", UsageError(
1077
+ "invalid_command", "`submit-file` needs a PATH."),
1078
+ options["json"], stdout, stderr)
1079
+ continue
1080
+ _do_submit_file(console, options, path, stdout, stderr)
1081
+ continue
1082
+ if stripped == "history":
1083
+ _do_history(console, options, stdout)
1084
+ continue
1085
+ if stripped == "reset":
1086
+ _do_reset(console, options, stdout)
1087
+ continue
1088
+ _emit_error(stripped.split()[0], UsageError(
1089
+ "invalid_command",
1090
+ f"Unknown command: {stripped} (want submit, submit-file,"
1091
+ " history, reset, help, quit).",
1092
+ ), options["json"], stdout, stderr)
1093
+ return EXIT_OK
1094
+
1095
+
1096
+ def main(argv=None, engine=None, cell_runner=None, stdin=None, stdout=None,
1097
+ stderr=None):
1098
+ """Run the stdio console; return the process exit code (never raise).
1099
+
1100
+ Args:
1101
+ argv: Argument vector without the program name; defaults to
1102
+ sys.argv[1:].
1103
+ engine: Engine to run against; constructed from
1104
+ --project/--actor when omitted.
1105
+ cell_runner: Worker runner; constructed when omitted.
1106
+ stdin: Input stream for the interactive session.
1107
+ stdout: Stream for results and machine envelopes.
1108
+ stderr: Stream for human-readable errors plus usage.
1109
+
1110
+ Returns:
1111
+ Process exit code: 0 session-ok, 2 usage error.
1112
+
1113
+ Caution: Exit zero is not PASS — per-cell verdicts live in the
1114
+ persisted records; read them, never the exit code.
1115
+ """
1116
+ argv = list(sys.argv[1:] if argv is None else argv)
1117
+ stdin = sys.stdin if stdin is None else stdin
1118
+ stdout = sys.stdout if stdout is None else stdout
1119
+ stderr = sys.stderr if stderr is None else stderr
1120
+ try:
1121
+ options, rest = _parse_argv(argv)
1122
+ except UsageError as exc:
1123
+ _emit_error(None, exc, False, stdout, stderr)
1124
+ return EXIT_USAGE
1125
+ if options["help"]:
1126
+ stdout.write(USAGE)
1127
+ return EXIT_OK
1128
+ if rest:
1129
+ _emit_error(None, UsageError(
1130
+ "invalid_command",
1131
+ f"Unknown command: {rest[0]} (the console is interactive).",
1132
+ ), options["json"], stdout, stderr)
1133
+ return EXIT_USAGE
1134
+ if engine is None:
1135
+ engine = Engine(options["project"], actor=options["actor"])
1136
+ try:
1137
+ console = StdioConsole(
1138
+ engine, project_id=options["project"], actor=options["actor"],
1139
+ root=options["root"], cell_runner=cell_runner,
1140
+ )
1141
+ except ExecutionError as exc:
1142
+ _emit_error(None, exc, options["json"], stdout, stderr)
1143
+ return EXIT_USAGE
1144
+ try:
1145
+ return _run_session(console, options, stdin, stdout, stderr)
1146
+ finally:
1147
+ console.close()
1148
+
1149
+
1150
+ if __name__ == "__main__":
1151
+ raise SystemExit(main())