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,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
|