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,464 @@
1
+ """Worker substrate pieces: prelude, constants, cell helpers."""
2
+
3
+ import hashlib
4
+ import json
5
+ import os
6
+ import re
7
+ import time
8
+ from pathlib import PurePosixPath
9
+
10
+ from ...interfaces.paths import PathError, join_contained
11
+ from ..errors import ExecutionError
12
+ from ..runner import pgid_survivors
13
+
14
+
15
+ #: Worker entry filename inside the scratch (tracebacks cite this).
16
+ CELL_ENTRY = "cell.py"
17
+
18
+ #: Scratch-relative ops log the prelude appends; harvested every run.
19
+ OPS_LOG_NAME = "__pyexec_ops.jsonl"
20
+
21
+ #: Worker budget when the call omits one (matches the console runner).
22
+ DEFAULT_TIMEOUT_S = 120
23
+
24
+ #: Explicit overrides stay within these walls.
25
+ MIN_TIMEOUT_S = 1
26
+
27
+ MAX_TIMEOUT_S = 1800
28
+
29
+ #: Inline scripts are bounded text, never unbounded blobs.
30
+ MAX_LINES_BYTES = 65536
31
+
32
+ #: Project-relative script paths are bounded text too.
33
+ MAX_PATH_CHARS = 512
34
+
35
+ #: Declared harvest artifacts per run (plus the ops log itself).
36
+ MAX_ARTIFACTS = 16
37
+
38
+ #: Deepest traceback frames kept on the exception record.
39
+ EXCEPTION_FRAMES = 8
40
+
41
+ #: Exception message characters kept on the exception record.
42
+ EXCEPTION_MESSAGE_CHARS = 2000
43
+
44
+ _POLL_SLICE_S = 0.05
45
+
46
+ _KILL_CONFIRM_GRACE_S = 2.0
47
+
48
+ _KILL_CONFIRM_STEP_S = 0.05
49
+
50
+ _FRAME_RE = re.compile(r'^\s*File "(.+)", line (\d+)(?:, in (\S+))?')
51
+
52
+ PRELUDE_TEMPLATE = '''\
53
+ # --- rocket-pyexec prelude: pinned trusted bootstrap (do not edit) ---
54
+ import json as _json
55
+
56
+ __PYEXEC_PROJECT__ = @PROJECT_ID@
57
+ __PYEXEC_OPS_LOG__ = "@OPS_LOG@"
58
+
59
+
60
+ class _OpRecorder:
61
+ """Prebound `tools`: tools.<op>(**kwargs) records one op call.
62
+
63
+ Calls append {"operation", "arguments"} JSON lines to the worker
64
+ scratch ops log; the host replays each through the shared op
65
+ dispatch after the worker exits, so validation and execution are
66
+ exactly what the batch surface runs. Positional args are rejected:
67
+ op calls are keyword-shaped.
68
+ """
69
+
70
+ def __init__(self):
71
+ self._index = 0
72
+
73
+ def __getattr__(self, operation):
74
+ def call(*args, **kwargs):
75
+ if args:
76
+ raise TypeError(
77
+ "op calls take keyword arguments only: %s." % operation
78
+ )
79
+ entry = {"operation": operation, "arguments": dict(kwargs)}
80
+ with open(__PYEXEC_OPS_LOG__, "a", encoding="utf-8") as handle:
81
+ handle.write(_json.dumps(entry, sort_keys=True) + "\\n")
82
+ self._index += 1
83
+ return {"state": "recorded", "operation": operation,
84
+ "index": self._index}
85
+
86
+ return call
87
+
88
+
89
+ tools = _OpRecorder()
90
+
91
+
92
+ class _Project:
93
+ """Prebound `project`: local identity plus query recording."""
94
+
95
+ def __init__(self, project_id):
96
+ self.id = project_id
97
+
98
+ def top_ref(self):
99
+ return {"project": self.id, "ref": "top"}
100
+
101
+ def snapshot(self):
102
+ return tools.get_project_snapshot()
103
+
104
+
105
+ project = _Project(__PYEXEC_PROJECT__)
106
+
107
+
108
+ class _Pdk:
109
+ """Prebound `pdk`: worker-local view (discovery runs on the host)."""
110
+
111
+ def describe(self):
112
+ return {"project": project.id, "profile": "local",
113
+ "note": "worker-local view;"
114
+ " engine catalog discovery runs on the host."}
115
+
116
+
117
+ pdk = _Pdk()
118
+
119
+
120
+ class _MissingBinding:
121
+ """Honest placeholder: missing profiles report, never fake."""
122
+
123
+ def __init__(self, name, hint):
124
+ self._name = name
125
+ self._hint = hint
126
+
127
+ def __repr__(self):
128
+ return "<%s unavailable in this worker profile: %s>" % (
129
+ self._name, self._hint)
130
+
131
+ def __getattr__(self, item):
132
+ raise RuntimeError(
133
+ "%s unavailable in this worker profile: %s"
134
+ % (self._name, self._hint)
135
+ )
136
+
137
+ def __call__(self, *args, **kwargs):
138
+ raise RuntimeError(
139
+ "%s unavailable in this worker profile: %s"
140
+ % (self._name, self._hint)
141
+ )
142
+
143
+
144
+ try:
145
+ import gdsfactory as gf
146
+ except Exception as _gf_error:
147
+ gf = _MissingBinding("gf", "gdsfactory not installed (%s)" % _gf_error)
148
+
149
+ try:
150
+ import numpy as np
151
+ except Exception as _np_error:
152
+ np = _MissingBinding("np", "numpy not installed (%s)" % _np_error)
153
+
154
+
155
+ class _Plot:
156
+ """Prebound `plot`: availability flag plus figure helper."""
157
+
158
+ def __init__(self):
159
+ try:
160
+ import matplotlib
161
+
162
+ matplotlib.use("Agg")
163
+ from matplotlib import pyplot as _pyplot
164
+ except Exception as _plot_error:
165
+ self.available = False
166
+ self._error = str(_plot_error)
167
+ self._pyplot = None
168
+ else:
169
+ self.available = True
170
+ self._error = ""
171
+ self._pyplot = _pyplot
172
+
173
+ def __repr__(self):
174
+ if self.available:
175
+ return "<plot (matplotlib available)>"
176
+ return "<plot unavailable in this worker profile: %s>" % self._error
177
+
178
+ def figure(self, *args, **kwargs):
179
+ if not self.available:
180
+ raise RuntimeError(
181
+ "plot unavailable in this worker profile: %s" % self._error
182
+ )
183
+ return self._pyplot.figure(*args, **kwargs)
184
+
185
+
186
+ plot = _Plot()
187
+ '''
188
+
189
+ PRELUDE_DIGEST = hashlib.sha256(PRELUDE_TEMPLATE.encode("utf-8")).hexdigest()
190
+
191
+
192
+ def _render_preamble(project_id):
193
+ return PRELUDE_TEMPLATE.replace("@OPS_LOG@", OPS_LOG_NAME).replace(
194
+ "@PROJECT_ID@", json.dumps(project_id)
195
+ )
196
+
197
+
198
+ def build_worker_source(code, project_id="default"):
199
+ """Render the worker source: pinned preamble plus the display cell.
200
+
201
+ The project binding renders into the preamble template first, so
202
+ display text is concatenated verbatim afterwards — cell bytes that
203
+ happen to look like the slot stay data. The result ends with the
204
+ exact displayed cell (trailing newline normalized).
205
+
206
+ Args:
207
+ code: Display cell text to wrap.
208
+ project_id: Project id rendered into the preamble binding.
209
+
210
+ Returns:
211
+ Worker source: pinned preamble plus the display cell.
212
+ """
213
+ display = code if code.endswith("\n") else code + "\n"
214
+ return _render_preamble(project_id) + "\n" + display
215
+
216
+
217
+ def prelude_digest_for(project_id):
218
+ """Environment digest: the rendered preamble, hashed.
219
+
220
+ Approval bindings pin this value; a preamble change invalidates
221
+ outstanding approvals instead of running under a new env.
222
+
223
+ Args:
224
+ project_id: Project id rendered into the preamble binding.
225
+
226
+ Returns:
227
+ Hex sha256 of the rendered preamble.
228
+ """
229
+ return hashlib.sha256(_render_preamble(project_id).encode("utf-8")).hexdigest()
230
+
231
+
232
+ def not_run_verdict(reason):
233
+ """The only verdict python_execute ever carries: NOT_RUN.
234
+
235
+ Scripted execution has no qualified evaluator, so a terminal
236
+ script is never PASS and never FAIL — the execution record
237
+ (`state`/`ok`/effects) holds the truth, not the verdict.
238
+
239
+ Args:
240
+ reason: Why no check verdict exists (points at the record).
241
+
242
+ Returns:
243
+ NOT_RUN verdict mapping with the reason.
244
+ """
245
+ return {"status": "NOT_RUN", "reason": reason}
246
+
247
+
248
+ def resolve_script_path(project_root, path):
249
+ """Resolve a project-relative script path, contained or loud.
250
+
251
+ Root-join only: absolute inputs, `..` segments, and symlink
252
+ escapes fail closed; the survivor must be a regular file.
253
+
254
+ Args:
255
+ project_root: Project directory the path joins under.
256
+ path: Project-relative script path text.
257
+
258
+ Returns:
259
+ Resolved contained path to the script file.
260
+
261
+ Raises:
262
+ ExecutionError: invalid_command without a configured root
263
+ or for non-text/empty/oversize input, path_escape for
264
+ absolute inputs, `..` segments, or symlink escapes,
265
+ invalid_path when no regular file survives.
266
+ """
267
+ if project_root is None:
268
+ raise ExecutionError(
269
+ "invalid_command",
270
+ "path mode needs a configured project root; none is set.",
271
+ )
272
+ if not isinstance(path, str) or not path.strip():
273
+ raise ExecutionError(
274
+ "invalid_command",
275
+ f"Script path must be non-empty text, got {path!r}.",
276
+ )
277
+ if len(path) > MAX_PATH_CHARS:
278
+ raise ExecutionError(
279
+ "invalid_command",
280
+ f"Script path exceeds {MAX_PATH_CHARS} characters.",
281
+ )
282
+ try:
283
+ resolved = join_contained(project_root, path)
284
+ except PathError as exc:
285
+ raise ExecutionError(exc.code, str(exc)) from exc
286
+ if not resolved.is_file():
287
+ raise ExecutionError(
288
+ "invalid_path", f"Script file not found: {path}."
289
+ )
290
+ return resolved
291
+
292
+
293
+ def parse_exception(stderr, entry_name=CELL_ENTRY):
294
+ """Parse the last traceback into a bounded exception location.
295
+
296
+ Frames cite file/line/function; frames from the worker entry
297
+ file are flagged so callers can point at cell code rather than
298
+ library internals. Bounds: the deepest frames and a truncated
299
+ message survive; anything else is dropped, never dumped whole.
300
+
301
+ Args:
302
+ stderr: Cleaned worker stderr text to parse.
303
+ entry_name: Worker entry filename flagging cell frames.
304
+
305
+ Returns:
306
+ Exception record with type, message, frames, and a location
307
+ line, or None when no traceback is present.
308
+ """
309
+ if not stderr:
310
+ return None
311
+ lines = stderr.splitlines()
312
+ starts = [
313
+ index for index, line in enumerate(lines)
314
+ if line.strip() == "Traceback (most recent call last):"
315
+ ]
316
+ block = lines[starts[-1] + 1:] if starts else lines
317
+ frames = []
318
+ for line in block:
319
+ match = _FRAME_RE.match(line)
320
+ if match:
321
+ frames.append(match)
322
+ tail = [line for line in block if line.strip()]
323
+ if not tail:
324
+ return None
325
+ parsed = re.match(r"^([A-Za-z_][\w.]*)(?:: (.*))?$", tail[-1].strip())
326
+ if parsed is None or (not starts and not frames):
327
+ return None
328
+ kept = []
329
+ for match in frames[-EXCEPTION_FRAMES:]:
330
+ name, lineno, function = match.groups()
331
+ kept.append({
332
+ "file": name[:200],
333
+ "line": int(lineno),
334
+ "function": function,
335
+ "in_cell": os.path.basename(name) == entry_name,
336
+ })
337
+ cell_frames = [frame for frame in kept if frame["in_cell"]]
338
+ deepest = cell_frames[-1] if cell_frames else (kept[-1] if kept else None)
339
+ if deepest is None:
340
+ location = None
341
+ elif deepest["function"]:
342
+ location = "%s:%d in %s" % (
343
+ deepest["file"], deepest["line"], deepest["function"])
344
+ else:
345
+ location = "%s:%d" % (deepest["file"], deepest["line"])
346
+ return {
347
+ "type": parsed.group(1)[:120],
348
+ "message": (parsed.group(2) or "")[:EXCEPTION_MESSAGE_CHARS],
349
+ "frames": kept,
350
+ "location": location,
351
+ }
352
+
353
+
354
+ def _check_timeout(value):
355
+ if isinstance(value, bool) or not isinstance(value, int):
356
+ raise ExecutionError(
357
+ "invalid_command",
358
+ f"timeout_s must be an int, got {value!r}.",
359
+ )
360
+ if not MIN_TIMEOUT_S <= value <= MAX_TIMEOUT_S:
361
+ raise ExecutionError(
362
+ "invalid_command",
363
+ f"timeout_s must be {MIN_TIMEOUT_S}..{MAX_TIMEOUT_S}, "
364
+ f"got {value}.",
365
+ )
366
+ return value
367
+
368
+
369
+ def _validate_artifacts(artifacts):
370
+ if len(list(artifacts)) > MAX_ARTIFACTS:
371
+ raise ExecutionError(
372
+ "invalid_command",
373
+ f"At most {MAX_ARTIFACTS} artifacts per run.",
374
+ )
375
+ names = []
376
+ for name in artifacts:
377
+ parsed = PurePosixPath(name)
378
+ if (
379
+ parsed.is_absolute()
380
+ or ".." in parsed.parts
381
+ or not parsed.parts
382
+ or str(name) != parsed.as_posix()
383
+ ):
384
+ raise ExecutionError(
385
+ "invalid_reference",
386
+ f"Artifact escapes the scratch: {name}.",
387
+ )
388
+ names.append(parsed.as_posix())
389
+ return names
390
+
391
+
392
+ def _display_exception(parsed, cell_name, cell_line_offset):
393
+ """Remap in_cell frames onto display coordinates.
394
+
395
+ Worker tracebacks cite the deleted scratch path plus the
396
+ trusted preamble's line numbers; callers need the submitted
397
+ cell's file and lines instead. Library frames pass through
398
+ untouched.
399
+
400
+ Args:
401
+ parsed: Exception record from parse_exception, if any.
402
+ cell_name: Display filename in_cell frames report.
403
+ cell_line_offset: Worker lines preceding the display cell.
404
+
405
+ Returns:
406
+ Remapped exception record, or None when no traceback.
407
+ """
408
+ if parsed is None:
409
+ return None
410
+ frames = []
411
+ for frame in parsed["frames"]:
412
+ if frame["in_cell"]:
413
+ frame = {
414
+ **frame,
415
+ "file": cell_name,
416
+ "line": frame["line"] - cell_line_offset,
417
+ }
418
+ frames.append(frame)
419
+ cell_frames = [frame for frame in frames if frame["in_cell"]]
420
+ deepest = cell_frames[-1] if cell_frames else None
421
+ if deepest is None:
422
+ return {**parsed, "frames": frames}
423
+ if deepest["function"]:
424
+ location = "%s:%d in %s" % (
425
+ deepest["file"], deepest["line"], deepest["function"])
426
+ else:
427
+ location = "%s:%d" % (deepest["file"], deepest["line"])
428
+ return {**parsed, "frames": frames, "location": location}
429
+
430
+
431
+ def _interp_chain(python):
432
+ """Interpreter symlink chain, every level (cycle-capped).
433
+
434
+ Args:
435
+ python: Interpreter path starting the chain.
436
+
437
+ Returns:
438
+ Chain paths from the venv entry to the resolved leaf.
439
+ """
440
+ chain, seen, current = [], set(), python
441
+ for _ in range(32):
442
+ if current in seen:
443
+ break
444
+ seen.add(current)
445
+ chain.append(current)
446
+ if not os.path.islink(current):
447
+ break
448
+ link = os.readlink(current)
449
+ current = (
450
+ link if os.path.isabs(link)
451
+ else os.path.join(os.path.dirname(current), link)
452
+ )
453
+ return chain
454
+
455
+
456
+ def _confirm_tree_dead(pgid, grace_s=_KILL_CONFIRM_GRACE_S):
457
+ """True once no process-group member survives (bounded wait)."""
458
+ deadline = time.monotonic() + grace_s
459
+ while True:
460
+ if not pgid_survivors(pgid):
461
+ return True
462
+ if time.monotonic() >= deadline:
463
+ return False
464
+ time.sleep(_KILL_CONFIRM_STEP_S)