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,173 @@
1
+ """Approval state machine for python_execute (mixin)."""
2
+
3
+ import uuid
4
+
5
+ from ..errors import ExecutionError
6
+
7
+
8
+ class _ApprovalMixin:
9
+ """UI-resolved approvals: pending/approved/denied/expired."""
10
+
11
+ def _binding(self, approval_id):
12
+ try:
13
+ return self._approvals[approval_id]
14
+ except KeyError:
15
+ raise ExecutionError(
16
+ "approval_invalid", "Unknown or expired approval."
17
+ ) from None
18
+
19
+ def approval_status(self, approval_id):
20
+ """Read one approval's state (approvals, never job state).
21
+
22
+ Args:
23
+ approval_id: Approval bound by request_execution.
24
+
25
+ Returns:
26
+ Approval state mapping with approval_id, approval_state,
27
+ execution_id, and the run_id only once admitted.
28
+
29
+ Raises:
30
+ ExecutionError: approval_invalid for an unknown approval.
31
+ """
32
+ record = self._binding(approval_id)
33
+ self._sweep_expired()
34
+ out = {
35
+ "approval_id": approval_id,
36
+ "approval_state": record["state"],
37
+ "execution_id": record["execution_id"],
38
+ }
39
+ if record["run_id"] is not None:
40
+ out["run_id"] = record["run_id"]
41
+ if record.get("reason"):
42
+ out["reason"] = record["reason"]
43
+ return out
44
+
45
+ def resolve_approval(self, approval_id, decision, via="ui", actor=None):
46
+ """Resolve an approval through the human UI route.
47
+
48
+ Agent calls request approval; they never grant it: any `via`
49
+ other than the human UI route raises approval_forbidden and
50
+ the approval stays pending. Approving admits exactly one
51
+ queued run; denying lapses the approval with no run.
52
+
53
+ Args:
54
+ approval_id: Approval bound by request_execution.
55
+ decision: `approved` to admit one run, `denied` to
56
+ lapse the approval with no run.
57
+ via: Resolution route; only `ui` (the trusted human
58
+ route) resolves.
59
+ actor: Human actor the decision is attributed to.
60
+
61
+ Returns:
62
+ Resolution with approval_id, approval_state,
63
+ execution_id, and the run_id once admitted.
64
+
65
+ Raises:
66
+ ExecutionError: approval_invalid for an unknown approval
67
+ or any binding drift, approval_forbidden for a
68
+ non-UI route, approval_denied when a lapsed
69
+ approval is approved, approval_expired when a
70
+ lapsed approval is approved, invalid_command for
71
+ any other decision text.
72
+ """
73
+ record = self._binding(approval_id)
74
+ if decision not in ("approved", "denied"):
75
+ raise ExecutionError(
76
+ "invalid_command",
77
+ f"decision must be approved or denied, got {decision!r}.",
78
+ )
79
+ if via != "ui":
80
+ raise ExecutionError(
81
+ "approval_forbidden",
82
+ "Approvals resolve through the human UI route only; "
83
+ "agent calls cannot self-approve.",
84
+ )
85
+ state = record["state"]
86
+ if state == "denied":
87
+ raise ExecutionError(
88
+ "approval_denied", "Approval was denied; no run exists."
89
+ )
90
+ if state == "expired":
91
+ raise ExecutionError(
92
+ "approval_expired", "Approval expired; no run exists."
93
+ )
94
+ if state == "approved":
95
+ return {
96
+ "approval_id": approval_id,
97
+ "approval_state": "approved",
98
+ "execution_id": record["execution_id"],
99
+ "run_id": record["run_id"],
100
+ }
101
+ if decision == "denied":
102
+ record["state"] = "denied"
103
+ record["reason"] = "Denied on the human UI route."
104
+ record["resolved_by"] = actor or self._actor
105
+ self._persist_approval(record)
106
+ self._reap_terminal()
107
+ return {
108
+ "approval_id": approval_id,
109
+ "approval_state": "denied",
110
+ "execution_id": record["execution_id"],
111
+ }
112
+ self._sweep_expired()
113
+ if record["state"] == "expired":
114
+ raise ExecutionError(
115
+ "approval_expired", "Approval expired; no run exists."
116
+ )
117
+ if record["generation"] != self._generation:
118
+ record["state"] = "expired"
119
+ record["reason"] = "Approval generation retired; request again."
120
+ self._persist_approval(record)
121
+ raise ExecutionError(
122
+ "approval_expired", "Approval expired; no run exists."
123
+ )
124
+ if record["history_cursor"] != self._cursor:
125
+ raise ExecutionError(
126
+ "approval_invalid",
127
+ "History moved since the approval was bound.",
128
+ )
129
+ if self._cell_digest(record["execution_id"]) != record["code_sha256"]:
130
+ raise ExecutionError(
131
+ "approval_invalid",
132
+ "Cell bytes changed under their approval.",
133
+ )
134
+ if self._revision_provider is not None:
135
+ current = self._revision_provider()
136
+ if current != record["expected_revision"]:
137
+ raise ExecutionError(
138
+ "approval_invalid",
139
+ f"Project moved since approval: {record['expected_revision']} "
140
+ f"is stale at {current}.",
141
+ )
142
+ run_id = record["run_id"] or uuid.uuid4().hex
143
+ self._execution.dispatch(
144
+ "python_execute",
145
+ {"project": self._project_id,
146
+ "revision": record["expected_revision"]},
147
+ "",
148
+ run_id=run_id,
149
+ )
150
+ record["run_id"] = run_id
151
+ record["state"] = "approved"
152
+ record["resolved_by"] = actor or self._actor
153
+ self._runs[run_id] = record["execution_id"]
154
+ self._persist_approval(record)
155
+ return {
156
+ "approval_id": approval_id,
157
+ "approval_state": "approved",
158
+ "execution_id": record["execution_id"],
159
+ "run_id": run_id,
160
+ }
161
+
162
+ def _persist_approval(self, record):
163
+ (self._root / "approvals" / ("%s.yaml" % record["approval_id"])).write_text(
164
+ self._dump({**record, "project_id": self._project_id}),
165
+ encoding="utf-8",
166
+ )
167
+
168
+ def _lapse_reason(self, record):
169
+ if record["generation"] != self._generation:
170
+ return "Approval generation retired before dispatch."
171
+ if self._clock() > record["expires_at"]:
172
+ return "Approval expired before dispatch."
173
+ return None
@@ -0,0 +1,404 @@
1
+ """Worker lifecycle: confined execution of one cell."""
2
+
3
+ import hashlib
4
+ import os
5
+ import shutil
6
+ import signal
7
+ import subprocess
8
+ import sys
9
+ import tempfile
10
+ import time
11
+ import uuid
12
+ from pathlib import Path
13
+
14
+ from ..errors import ExecutionError
15
+ from ..runner import BwrapRunner, clean_stream
16
+ from ..worker import CredentialGate, worker_profile
17
+ from .support import (
18
+ CELL_ENTRY,
19
+ DEFAULT_TIMEOUT_S,
20
+ _POLL_SLICE_S,
21
+ _check_timeout,
22
+ _confirm_tree_dead,
23
+ _display_exception,
24
+ _interp_chain,
25
+ _validate_artifacts,
26
+ parse_exception,
27
+ )
28
+
29
+
30
+ class PythonExecRunner:
31
+ """Fresh prepared workers on the console's sandbox substrate.
32
+
33
+ Same confinement machinery as console cells (`BwrapRunner` +
34
+ `worker_profile` + `CredentialGate`): one worker per run, a
35
+ scratch-only write area, execution-scoped IPC credentials, and
36
+ revocation plus confirmed tree-kill before every terminal
37
+ record. The interpreter is the prepared profile's Python (with
38
+ gdsfactory/numpy where the profile installs them), bound
39
+ read-only into the sandbox — never the host interpreter, and
40
+ with no host fallback when isolation is unprovable.
41
+
42
+ Args:
43
+ python: Prepared interpreter path; the running engine's
44
+ interpreter when omitted.
45
+ extra_tool_paths: Extra resolved paths ro-bound with the
46
+ profile (tool binaries on PATH inside the worker).
47
+ profile: Full sandbox profile override; worker_profile()
48
+ over the interpreter when omitted.
49
+ binary: bwrap binary name or path.
50
+ artifacts_root: Harvest destination root; temp dir default.
51
+ default_timeout_s: Worker budget when the call omits one.
52
+ require_net: Refuse admission unless net isolation probes;
53
+ degrade loudly to user-only otherwise.
54
+ """
55
+
56
+ def __init__(self, python=None, extra_tool_paths=(), profile=None,
57
+ binary="bwrap", artifacts_root=None,
58
+ default_timeout_s=DEFAULT_TIMEOUT_S, require_net=False):
59
+ self._python = python or sys.executable
60
+ if profile is None:
61
+ # venv interpreters are often symlink chains into a
62
+ # manager-owned install (uv): the kernel traverses every
63
+ # dirent inside the sandbox, so cover each level's root
64
+ # or exec fails with ENOENT.
65
+ tool_paths = _interp_chain(self._python)
66
+ tool_paths.extend(extra_tool_paths)
67
+ base = worker_profile(tool_paths=tool_paths)
68
+ env = dict(base.get("env", {}))
69
+ env["MPLBACKEND"] = "Agg"
70
+ env["MPLCONFIGDIR"] = "/tmp"
71
+ base["env"] = env
72
+ self._base_profile = base
73
+ else:
74
+ self._base_profile = dict(profile)
75
+ self._binary = binary
76
+ self._gate = CredentialGate()
77
+ self._default_timeout_s = default_timeout_s
78
+ self._artifacts_root = Path(
79
+ artifacts_root
80
+ if artifacts_root is not None
81
+ else Path(tempfile.gettempdir()) / "rocketeda_pyexec_artifacts"
82
+ )
83
+ self._admission, self._runner = self._preflight(require_net)
84
+
85
+ def _preflight(self, require_net):
86
+ try:
87
+ runner = BwrapRunner(self._base_profile, binary=self._binary)
88
+ runner.preflight()
89
+ except ExecutionError as exc:
90
+ return (
91
+ {"admitted": False, "reason": str(exc),
92
+ "network_isolated": False},
93
+ None,
94
+ )
95
+ network_isolated = False
96
+ try:
97
+ netted = dict(self._base_profile)
98
+ namespaces = list(netted.get("namespaces", ["user"]))
99
+ if "net" not in namespaces:
100
+ namespaces.append("net")
101
+ netted["namespaces"] = namespaces
102
+ BwrapRunner(netted, binary=self._binary).preflight()
103
+ network_isolated = True
104
+ runner = BwrapRunner(netted, binary=self._binary)
105
+ except ExecutionError:
106
+ if require_net:
107
+ return (
108
+ {
109
+ "admitted": False,
110
+ "reason": "network isolation unprovable: "
111
+ "net namespace refused by the kernel.",
112
+ "network_isolated": False,
113
+ },
114
+ None,
115
+ )
116
+ try:
117
+ self._prove_python(runner)
118
+ except ExecutionError as exc:
119
+ return (
120
+ {"admitted": False, "reason": str(exc),
121
+ "network_isolated": False},
122
+ None,
123
+ )
124
+ return (
125
+ {"admitted": True, "reason": None,
126
+ "network_isolated": network_isolated},
127
+ runner,
128
+ )
129
+
130
+ def _prove_python(self, runner):
131
+ with tempfile.TemporaryDirectory(prefix="pyexec_python_probe_") as tmp:
132
+ try:
133
+ proc = runner.run(
134
+ [self._python, "-c", "pass"], tmp, timeout_s=30
135
+ )
136
+ except ExecutionError as exc:
137
+ raise ExecutionError(
138
+ "isolation_unavailable",
139
+ f"sandbox python unavailable: {exc}.",
140
+ ) from exc
141
+ if proc.returncode != 0:
142
+ raise ExecutionError(
143
+ "isolation_unavailable",
144
+ f"sandbox python unavailable: exit {proc.returncode}.",
145
+ )
146
+
147
+ @property
148
+ def admission(self):
149
+ """Isolation admission record, stamped on every result.
150
+
151
+ Returns:
152
+ Admission mapping copy (admitted, reason,
153
+ network_isolated).
154
+ """
155
+ return dict(self._admission)
156
+
157
+ @property
158
+ def gate(self):
159
+ """Execution-scoped credential gate for worker IPC.
160
+
161
+ Returns:
162
+ Credential gate.
163
+ """
164
+ return self._gate
165
+
166
+ @property
167
+ def python(self):
168
+ """Prepared interpreter path this runner executes.
169
+
170
+ Returns:
171
+ Interpreter path string.
172
+ """
173
+ return self._python
174
+
175
+ def run(self, source, run_id=None, timeout_s=None, artifacts=(),
176
+ extra_env=None, on_live=None, is_cancelled=None,
177
+ cell_name=CELL_ENTRY, cell_line_offset=0):
178
+ """Run one source text in a fresh worker; return its record.
179
+
180
+ Pre-spawn checks precede any execution, cheapest first:
181
+ artifact paths, then timeout shape, then isolation
182
+ admission. Without admission the call raises
183
+ isolation_unavailable and nothing runs anywhere — no host
184
+ fallback, ever. The wait polls in short slices so a
185
+ cancelled job kills its worker tree promptly instead of
186
+ sleeping out the budget.
187
+
188
+ Args:
189
+ source: Full worker source to execute.
190
+ run_id: Execution id; generated when omitted.
191
+ timeout_s: Worker budget; runner default when omitted.
192
+ artifacts: Scratch-relative paths to harvest.
193
+ extra_env: Per-run env additions inside the worker.
194
+ on_live: Optional callback while the worker is live.
195
+ is_cancelled: Optional zero-arg callable; a true
196
+ reading kills the worker and reports cancelled.
197
+ cell_name: Display filename in_cell frames report.
198
+ cell_line_offset: Worker lines preceding the display
199
+ cell; in_cell frames shift back by this much so
200
+ the location points at submitted code, never at
201
+ the deleted scratch or the trusted preamble.
202
+
203
+ Returns:
204
+ Terminal worker record (completed/failed/timeout/
205
+ crashed/cancelled) with bounded stdio, the parsed
206
+ exception location, artifacts, and kill
207
+ confirmations.
208
+
209
+ Raises:
210
+ ExecutionError: invalid_reference for escaping artifact
211
+ paths, invalid_command for a malformed timeout,
212
+ isolation_unavailable without admission,
213
+ runner_unavailable when the worker cannot spawn.
214
+ """
215
+ names = _validate_artifacts(artifacts)
216
+ budget = self._default_timeout_s if timeout_s is None else _check_timeout(timeout_s)
217
+ if not self._admission["admitted"]:
218
+ raise ExecutionError(
219
+ "isolation_unavailable", self._admission["reason"]
220
+ )
221
+ scratch = tempfile.mkdtemp(prefix="rocketeda_pyexec_")
222
+ run_id = run_id or uuid.uuid4().hex
223
+ token = self._gate.issue(run_id)
224
+ Path(scratch, CELL_ENTRY).write_text(source, encoding="utf-8")
225
+ env = {
226
+ "ROCKETEDA_RUN_ID": run_id,
227
+ "ROCKETEDA_PYEXEC_TOKEN": token,
228
+ "HOME": scratch,
229
+ }
230
+ env.update(extra_env or {})
231
+ argv = self._runner.argv_for(
232
+ scratch, [self._python, CELL_ENTRY], extra_env=env,
233
+ )
234
+ try:
235
+ try:
236
+ proc = subprocess.Popen(
237
+ argv,
238
+ stdin=subprocess.DEVNULL,
239
+ stdout=subprocess.PIPE,
240
+ stderr=subprocess.PIPE,
241
+ cwd=scratch,
242
+ start_new_session=True,
243
+ )
244
+ except OSError as exc:
245
+ raise ExecutionError(
246
+ "runner_unavailable", f"Worker spawn failed: {exc}."
247
+ ) from exc
248
+ try:
249
+ if on_live is not None:
250
+ on_live(run_id, token)
251
+ out_bytes, err_bytes, outcome = self._wait(
252
+ proc, budget, is_cancelled
253
+ )
254
+ if outcome == "timeout":
255
+ return self._finish(
256
+ run_id, scratch, names, out_bytes, err_bytes,
257
+ state="timeout", ok=False,
258
+ reason=f"timeout after {budget}s; partials retained.",
259
+ exit_code=None, partial=True, pgid=proc.pid,
260
+ cell_name=cell_name,
261
+ cell_line_offset=cell_line_offset,
262
+ )
263
+ if outcome == "cancelled":
264
+ return self._finish(
265
+ run_id, scratch, names, out_bytes, err_bytes,
266
+ state="cancelled", ok=False,
267
+ reason="cancelled via the job surface; "
268
+ "worker tree killed.",
269
+ exit_code=None, partial=True, pgid=proc.pid,
270
+ cell_name=cell_name,
271
+ cell_line_offset=cell_line_offset,
272
+ )
273
+ returncode = proc.returncode
274
+ if returncode == 0:
275
+ return self._finish(
276
+ run_id, scratch, names, out_bytes, err_bytes,
277
+ state="completed", ok=True, reason=None,
278
+ exit_code=0, partial=False, pgid=proc.pid,
279
+ cell_name=cell_name,
280
+ cell_line_offset=cell_line_offset,
281
+ )
282
+ # bwrap reports child signal death as 128+signo (the
283
+ # shell convention), so exits above 128 read as crashes.
284
+ # A script deliberately exiting 129+ misreads the same
285
+ # way; both land ok False, so the ambiguity never
286
+ # fake-passes.
287
+ if returncode < 0 or returncode > 128:
288
+ signo = -returncode if returncode < 0 else returncode - 128
289
+ return self._finish(
290
+ run_id, scratch, names, out_bytes, err_bytes,
291
+ state="crashed", ok=False,
292
+ reason=f"death by signal {signo}.",
293
+ exit_code=returncode, partial=True, pgid=proc.pid,
294
+ cell_name=cell_name,
295
+ cell_line_offset=cell_line_offset,
296
+ )
297
+ return self._finish(
298
+ run_id, scratch, names, out_bytes, err_bytes,
299
+ state="failed", ok=False,
300
+ reason=f"exit {returncode}.",
301
+ exit_code=returncode, partial=False, pgid=proc.pid,
302
+ cell_name=cell_name,
303
+ cell_line_offset=cell_line_offset,
304
+ )
305
+ finally:
306
+ if proc.poll() is None:
307
+ self._terminate(proc)
308
+ proc.communicate()
309
+ finally:
310
+ self._gate.revoke(run_id)
311
+ shutil.rmtree(scratch, ignore_errors=True)
312
+
313
+ @staticmethod
314
+ def _wait(proc, budget, is_cancelled):
315
+ start = time.monotonic()
316
+ while True:
317
+ if is_cancelled is not None and is_cancelled():
318
+ PythonExecRunner._terminate(proc)
319
+ out_bytes, err_bytes = proc.communicate()
320
+ return out_bytes, err_bytes, "cancelled"
321
+ remaining = budget - (time.monotonic() - start)
322
+ if remaining <= 0:
323
+ PythonExecRunner._terminate(proc)
324
+ out_bytes, err_bytes = proc.communicate()
325
+ return out_bytes, err_bytes, "timeout"
326
+ try:
327
+ out_bytes, err_bytes = proc.communicate(
328
+ timeout=min(_POLL_SLICE_S, remaining)
329
+ )
330
+ except subprocess.TimeoutExpired:
331
+ continue
332
+ return out_bytes, err_bytes, "exited"
333
+
334
+ @staticmethod
335
+ def _terminate(proc):
336
+ try:
337
+ os.killpg(proc.pid, signal.SIGKILL)
338
+ except (ProcessLookupError, PermissionError, OSError):
339
+ pass
340
+ try:
341
+ proc.kill()
342
+ except (ProcessLookupError, OSError):
343
+ pass
344
+ proc.wait()
345
+
346
+ def _finish(self, run_id, scratch, names, out_bytes, err_bytes,
347
+ state, ok, reason, exit_code, partial, pgid,
348
+ cell_name=CELL_ENTRY, cell_line_offset=0):
349
+ # Revocation + tree-kill confirmation strictly precede the
350
+ # terminal record (and therefore the next run).
351
+ self._gate.revoke(run_id)
352
+ try:
353
+ os.killpg(pgid, signal.SIGKILL)
354
+ except (ProcessLookupError, PermissionError, OSError):
355
+ pass
356
+ tree_killed = _confirm_tree_dead(pgid)
357
+ harvested, rejected = self._harvest(run_id, scratch, names)
358
+ stdout = clean_stream(out_bytes)
359
+ stderr = clean_stream(err_bytes)
360
+ exception = _display_exception(
361
+ parse_exception(stderr), cell_name, cell_line_offset)
362
+ return {
363
+ "run_id": run_id,
364
+ "state": state,
365
+ "ok": ok,
366
+ "reason": reason,
367
+ "exit_code": exit_code,
368
+ "stdout": stdout,
369
+ "stderr": stderr,
370
+ "partial": partial,
371
+ "exception": exception,
372
+ "artifacts": harvested,
373
+ "rejected_artifacts": rejected,
374
+ "network_isolated": self._admission["network_isolated"],
375
+ "credential_revoked": True,
376
+ "tree_killed": tree_killed,
377
+ "spawned": True,
378
+ "scratch": scratch,
379
+ }
380
+
381
+ def _harvest(self, run_id, scratch, names):
382
+ base = Path(scratch).resolve()
383
+ harvested, rejected = [], []
384
+ for name in names:
385
+ target = (base / name).resolve()
386
+ if not target.is_relative_to(base) or not target.is_file():
387
+ rejected.append(name)
388
+ continue
389
+ digest = hashlib.sha256()
390
+ with open(target, "rb") as handle:
391
+ for chunk in iter(lambda: handle.read(65536), b""):
392
+ digest.update(chunk)
393
+ dest = self._artifacts_root / run_id / name
394
+ dest.parent.mkdir(parents=True, exist_ok=True)
395
+ shutil.copyfile(target, dest)
396
+ harvested.append(
397
+ {
398
+ "name": name,
399
+ "path": str(dest),
400
+ "bytes": dest.stat().st_size,
401
+ "sha256": digest.hexdigest(),
402
+ }
403
+ )
404
+ return harvested, rejected