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.
- rocket_engine/__init__.py +8 -0
- rocket_engine/__main__.py +12 -0
- rocket_engine/adapters/README.md +54 -0
- rocket_engine/adapters/__init__.py +7 -0
- rocket_engine/adapters/api.py +39 -0
- rocket_engine/adapters/base.py +46 -0
- rocket_engine/adapters/cace/__init__.py +163 -0
- rocket_engine/adapters/circulax/README.md +10 -0
- rocket_engine/adapters/circulax/__init__.py +112 -0
- rocket_engine/adapters/circulax/_driver.py +190 -0
- rocket_engine/adapters/circulax/adapter.py +538 -0
- rocket_engine/adapters/circulax/results.py +291 -0
- rocket_engine/adapters/cli/README.md +34 -0
- rocket_engine/adapters/cli/__init__.py +305 -0
- rocket_engine/adapters/connectivity/__init__.py +196 -0
- rocket_engine/adapters/connectivity/results.py +214 -0
- rocket_engine/adapters/cosim/README.md +9 -0
- rocket_engine/adapters/cosim/__init__.py +1390 -0
- rocket_engine/adapters/cosim/results.py +214 -0
- rocket_engine/adapters/devsim/__init__.py +171 -0
- rocket_engine/adapters/digital_flow/README.md +9 -0
- rocket_engine/adapters/digital_flow/__init__.py +1121 -0
- rocket_engine/adapters/digital_flow/results.py +227 -0
- rocket_engine/adapters/digital_flow/state.py +462 -0
- rocket_engine/adapters/drc/README.klayout.md +7 -0
- rocket_engine/adapters/drc/README.md +7 -0
- rocket_engine/adapters/drc/__init__.py +1 -0
- rocket_engine/adapters/drc/klayout.py +952 -0
- rocket_engine/adapters/drc/magic.py +287 -0
- rocket_engine/adapters/drc/results.py +184 -0
- rocket_engine/adapters/drc/results_kl.py +188 -0
- rocket_engine/adapters/elmer/__init__.py +163 -0
- rocket_engine/adapters/envelope.py +120 -0
- rocket_engine/adapters/equivalence/__init__.py +156 -0
- rocket_engine/adapters/errors.py +181 -0
- rocket_engine/adapters/extract_magic/README.md +15 -0
- rocket_engine/adapters/extract_magic/__init__.py +425 -0
- rocket_engine/adapters/extract_magic/results.py +209 -0
- rocket_engine/adapters/formal_sby/README.md +7 -0
- rocket_engine/adapters/formal_sby/__init__.py +773 -0
- rocket_engine/adapters/formal_sby/formal_edge.py +195 -0
- rocket_engine/adapters/formal_sby/results.py +185 -0
- rocket_engine/adapters/gdsfactory/__init__.py +194 -0
- rocket_engine/adapters/gdsfactory/_driver.py +459 -0
- rocket_engine/adapters/gdsfactory/adapter.py +609 -0
- rocket_engine/adapters/gdsfactory/results.py +181 -0
- rocket_engine/adapters/hdl_sim/README.md +7 -0
- rocket_engine/adapters/hdl_sim/__init__.py +369 -0
- rocket_engine/adapters/hdl_sim/ghdl.py +155 -0
- rocket_engine/adapters/hdl_sim/results.py +178 -0
- rocket_engine/adapters/hdl_sim/verilator.py +167 -0
- rocket_engine/adapters/http/README.md +15 -0
- rocket_engine/adapters/http/__init__.py +458 -0
- rocket_engine/adapters/http/events.py +63 -0
- rocket_engine/adapters/ipc/README.md +9 -0
- rocket_engine/adapters/ipc/__init__.py +111 -0
- rocket_engine/adapters/lvs_netgen/README.md +20 -0
- rocket_engine/adapters/lvs_netgen/__init__.py +889 -0
- rocket_engine/adapters/lvs_netgen/results.py +168 -0
- rocket_engine/adapters/mcp/README.md +19 -0
- rocket_engine/adapters/mcp/__init__.py +85 -0
- rocket_engine/adapters/mcp/api-schema.json +6107 -0
- rocket_engine/adapters/mcp/pull.py +246 -0
- rocket_engine/adapters/mcp/server.py +794 -0
- rocket_engine/adapters/meep/__init__.py +167 -0
- rocket_engine/adapters/meow/README.md +7 -0
- rocket_engine/adapters/meow/__init__.py +190 -0
- rocket_engine/adapters/meow/_driver.py +227 -0
- rocket_engine/adapters/meow/_envelope.py +108 -0
- rocket_engine/adapters/meow/adapter.py +491 -0
- rocket_engine/adapters/meow/results.py +218 -0
- rocket_engine/adapters/mutation/__init__.py +192 -0
- rocket_engine/adapters/ngspice/README.md +28 -0
- rocket_engine/adapters/ngspice/__init__.py +627 -0
- rocket_engine/adapters/ngspice/results.py +215 -0
- rocket_engine/adapters/postlayout/README.md +28 -0
- rocket_engine/adapters/postlayout/__init__.py +17 -0
- rocket_engine/adapters/postlayout/adapter.py +1062 -0
- rocket_engine/adapters/postlayout/results.py +330 -0
- rocket_engine/adapters/postlayout/translate.py +71 -0
- rocket_engine/adapters/profile.py +220 -0
- rocket_engine/adapters/sax/README.md +23 -0
- rocket_engine/adapters/sax/__init__.py +516 -0
- rocket_engine/adapters/sax/_driver.py +148 -0
- rocket_engine/adapters/sax/results.py +291 -0
- rocket_engine/adapters/stdio_console.py +1151 -0
- rocket_engine/adapters/syntax/README.md +5 -0
- rocket_engine/adapters/syntax/__init__.py +366 -0
- rocket_engine/adapters/syntax/results.py +181 -0
- rocket_engine/adapters/synth_yosys/README.md +7 -0
- rocket_engine/adapters/synth_yosys/__init__.py +655 -0
- rocket_engine/adapters/synth_yosys/amaranth.py +120 -0
- rocket_engine/adapters/synth_yosys/ghdl.py +128 -0
- rocket_engine/adapters/synth_yosys/results.py +173 -0
- rocket_engine/adapters/timing_sta/README.md +9 -0
- rocket_engine/adapters/timing_sta/__init__.py +726 -0
- rocket_engine/adapters/view_sync/__init__.py +185 -0
- rocket_engine/adapters/view_sync/results.py +189 -0
- rocket_engine/adapters/vlsir/__init__.py +142 -0
- rocket_engine/adapters/xschem/__init__.py +440 -0
- rocket_engine/adapters/xschem/deck.py +266 -0
- rocket_engine/clients/__init__.py +5 -0
- rocket_engine/clients/runtime.py +255 -0
- rocket_engine/composition.py +2279 -0
- rocket_engine/execution/README.md +30 -0
- rocket_engine/execution/__init__.py +150 -0
- rocket_engine/execution/apig/__init__.py +1 -0
- rocket_engine/execution/apig/pool.py +463 -0
- rocket_engine/execution/apig/submit.py +124 -0
- rocket_engine/execution/block_runs_scope.py +479 -0
- rocket_engine/execution/coalesce.py +414 -0
- rocket_engine/execution/console/__init__.py +1 -0
- rocket_engine/execution/console/apply_session.py +318 -0
- rocket_engine/execution/console/lifecycle.py +265 -0
- rocket_engine/execution/dispatch.py +205 -0
- rocket_engine/execution/errors.py +31 -0
- rocket_engine/execution/ports.py +797 -0
- rocket_engine/execution/pyexec/__init__.py +1 -0
- rocket_engine/execution/pyexec/apply_context.py +547 -0
- rocket_engine/execution/pyexec/approvals.py +173 -0
- rocket_engine/execution/pyexec/runner.py +404 -0
- rocket_engine/execution/pyexec/support.py +464 -0
- rocket_engine/execution/python_exec.py +897 -0
- rocket_engine/execution/queue.py +85 -0
- rocket_engine/execution/replay_safe.py +601 -0
- rocket_engine/execution/routing/__init__.py +1 -0
- rocket_engine/execution/routing/calls.py +364 -0
- rocket_engine/execution/routing/namespace.py +785 -0
- rocket_engine/execution/routing/ports.py +102 -0
- rocket_engine/execution/routing/tools.py +346 -0
- rocket_engine/execution/runner.py +311 -0
- rocket_engine/execution/runtime.py +1149 -0
- rocket_engine/execution/service.py +279 -0
- rocket_engine/execution/worker.py +464 -0
- rocket_engine/interfaces/README.md +26 -0
- rocket_engine/interfaces/__init__.py +58 -0
- rocket_engine/interfaces/apig_assets.py +407 -0
- rocket_engine/interfaces/celllink.py +461 -0
- rocket_engine/interfaces/documents.py +92 -0
- rocket_engine/interfaces/envelopes.py +131 -0
- rocket_engine/interfaces/errors.py +16 -0
- rocket_engine/interfaces/layout.py +410 -0
- rocket_engine/interfaces/layout_schema.yaml +278 -0
- rocket_engine/interfaces/paths.py +223 -0
- rocket_engine/interfaces/pdks.py +550 -0
- rocket_engine/interfaces/ports.py +72 -0
- rocket_engine/interfaces/references.py +115 -0
- rocket_engine/interfaces/refs.py +85 -0
- rocket_engine/interfaces/runparams.py +304 -0
- rocket_engine/interfaces/setup.py +552 -0
- rocket_engine/interfaces/setup_catalogue.yaml +243 -0
- rocket_engine/interfaces/setup_probes.py +66 -0
- rocket_engine/interfaces/vocabulary.py +58 -0
- rocket_engine/interfaces/yaml_dump.py +31 -0
- rocket_engine/library/README.md +22 -0
- rocket_engine/library/__init__.py +13 -0
- rocket_engine/library/apig_catalog.py +371 -0
- rocket_engine/library/catalog.py +185 -0
- rocket_engine/library/errors.py +16 -0
- rocket_engine/mutate/__init__.py +776 -0
- rocket_engine/mutate/operators.py +700 -0
- rocket_engine/ops/__init__.py +1 -0
- rocket_engine/ops/deferred.py +279 -0
- rocket_engine/ops/editors.py +404 -0
- rocket_engine/ops/evidence.py +194 -0
- rocket_engine/ops/gates.py +262 -0
- rocket_engine/ops/ports.py +76 -0
- rocket_engine/ops/queries.py +417 -0
- rocket_engine/ops/runs.py +553 -0
- rocket_engine/ops/submit.py +614 -0
- rocket_engine/project/README.md +27 -0
- rocket_engine/project/__init__.py +6 -0
- rocket_engine/project/canvas/__init__.py +55 -0
- rocket_engine/project/canvas/compile.py +109 -0
- rocket_engine/project/canvas/integrate.py +48 -0
- rocket_engine/project/canvas/schema.py +241 -0
- rocket_engine/project/canvas/templates.py +46 -0
- rocket_engine/project/canvas/validate.py +603 -0
- rocket_engine/project/edit/__init__.py +5 -0
- rocket_engine/project/edit/cas.py +87 -0
- rocket_engine/project/edit/transactions.py +282 -0
- rocket_engine/project/errors.py +16 -0
- rocket_engine/project/frontend13.py +685 -0
- rocket_engine/project/geometry/__init__.py +23 -0
- rocket_engine/project/geometry/ops.py +451 -0
- rocket_engine/project/geometry/snap.py +103 -0
- rocket_engine/project/ip/__init__.py +1 -0
- rocket_engine/project/ip/blocks.py +195 -0
- rocket_engine/project/ip/entry.schema.json +78 -0
- rocket_engine/project/ip/lifecycle.py +402 -0
- rocket_engine/project/ip/verbs.py +123 -0
- rocket_engine/project/layers/__init__.py +6 -0
- rocket_engine/project/layers/bindings.py +115 -0
- rocket_engine/project/layers/catalog.py +228 -0
- rocket_engine/project/layers/presets.py +43 -0
- rocket_engine/project/ledger/__init__.py +1 -0
- rocket_engine/project/ledger/docs.py +167 -0
- rocket_engine/project/ledger/entities.py +176 -0
- rocket_engine/project/ledger/events.py +29 -0
- rocket_engine/project/ledger/runs.py +257 -0
- rocket_engine/project/ledger/shared.py +174 -0
- rocket_engine/project/ledger/snapshots.py +385 -0
- rocket_engine/project/ledger/wires.py +552 -0
- rocket_engine/project/process/__init__.py +14 -0
- rocket_engine/project/process/model.py +193 -0
- rocket_engine/project/process/recipes.py +121 -0
- rocket_engine/project/schematic/__init__.py +1 -0
- rocket_engine/project/schematic/hierarchy.py +312 -0
- rocket_engine/project/schematic/instances.py +393 -0
- rocket_engine/project/schematic/sheets.py +198 -0
- rocket_engine/project/service.py +2941 -0
- rocket_engine/project/symbols/__init__.py +1 -0
- rocket_engine/project/symbols/cells.py +324 -0
- rocket_engine/project/symbols/linkage.py +356 -0
- rocket_engine/project/symbols/symbols.py +352 -0
- rocket_engine/project/templates/__init__.py +303 -0
- rocket_engine/project/templates/errors.py +25 -0
- rocket_engine/project/testbench/__init__.py +49 -0
- rocket_engine/project/testbench/blocks.py +619 -0
- rocket_engine/project/testbench/templates.py +134 -0
- rocket_engine/stores/README.md +12 -0
- rocket_engine/stores/__init__.py +6 -0
- rocket_engine/stores/ecs.py +1508 -0
- rocket_engine/stores/layers.py +1023 -0
- rocket_engine/stores/settings.py +1188 -0
- rocket_engine/stores/wireplace.py +1324 -0
- rocket_engine/workflows/README.md +18 -0
- rocket_engine/workflows/__init__.py +53 -0
- rocket_engine/workflows/capability_plan.py +102 -0
- rocket_engine/workflows/completion.py +228 -0
- rocket_engine/workflows/connectgraph.py +734 -0
- rocket_engine/workflows/connectgraph_emit.py +336 -0
- rocket_engine/workflows/gates.py +144 -0
- rocket_engine/workflows/photonic_wire.py +76 -0
- rocket_engine/workflows/stages.py +324 -0
- rocket_engine/workflows/viewsync.py +399 -0
- rocket_engine-0.0.2b1.dist-info/METADATA +200 -0
- rocket_engine-0.0.2b1.dist-info/RECORD +241 -0
- rocket_engine-0.0.2b1.dist-info/WHEEL +4 -0
- rocket_engine-0.0.2b1.dist-info/entry_points.txt +2 -0
- 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)
|