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,311 @@
|
|
|
1
|
+
"""Bubblewrap isolation runner (I16): confine solver subprocesses.
|
|
2
|
+
|
|
3
|
+
Profiles are data: namespaces, ro binds, symlinks, tmpfs mounts, env.
|
|
4
|
+
The workdir is always bound read-write at its own host path, so both
|
|
5
|
+
absolute and relative argv work unchanged inside. Preflight proves the
|
|
6
|
+
profile on a trivial command before any real work runs; unprovable
|
|
7
|
+
profiles refuse loudly instead of degrading silently. See
|
|
8
|
+
docs/probe-bwrap-isolation.md for the verified recipe and the
|
|
9
|
+
environment limits found while probing.
|
|
10
|
+
|
|
11
|
+
Profiles also carry `env_unset` (emitted as `--unsetenv`): bwrap
|
|
12
|
+
inherits the caller's environment except where told otherwise, so a
|
|
13
|
+
host DISPLAY would leak into the sandbox and abort display-sensitive
|
|
14
|
+
tools (netgen's Tk init). Default profiles scrub it.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import os
|
|
18
|
+
import re
|
|
19
|
+
import shutil
|
|
20
|
+
import signal
|
|
21
|
+
import subprocess
|
|
22
|
+
import tempfile
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
from .errors import ExecutionError
|
|
26
|
+
|
|
27
|
+
_SYSTEM_BINDS_RO = [("/usr", "/usr"), ("/etc", "/etc"), ("/proc", "/proc")]
|
|
28
|
+
_SYSTEM_SYMLINKS = [
|
|
29
|
+
("usr/bin", "/bin"),
|
|
30
|
+
("usr/sbin", "/sbin"),
|
|
31
|
+
("usr/lib", "/lib"),
|
|
32
|
+
("usr/lib64", "/lib64"),
|
|
33
|
+
]
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def tool_roots(tool_paths):
|
|
37
|
+
"""Read-only roots covering resolved tool binaries.
|
|
38
|
+
|
|
39
|
+
A tool under `*/bin/` binds its grandparent (the suite root);
|
|
40
|
+
anywhere else binds its parent directory. Deduped, order kept.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
tool_paths: Resolved tool binary paths to cover.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
(src, dest) ro-bind pairs covering the tools.
|
|
47
|
+
"""
|
|
48
|
+
roots = []
|
|
49
|
+
for path in tool_paths:
|
|
50
|
+
parent = Path(path).parent
|
|
51
|
+
root = str(parent.parent if parent.name == "bin" else parent)
|
|
52
|
+
if root not in [str(r) for r, _ in roots]:
|
|
53
|
+
roots.append((root, root))
|
|
54
|
+
return roots
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def default_profile(tool_paths=()):
|
|
58
|
+
"""Sandbox profile from the verified probe recipe, plus tool roots.
|
|
59
|
+
|
|
60
|
+
Args:
|
|
61
|
+
tool_paths: Resolved tool binary paths to bind read-only.
|
|
62
|
+
|
|
63
|
+
Returns:
|
|
64
|
+
Profile mapping (namespaces, binds, tmpfs, env, unsets).
|
|
65
|
+
"""
|
|
66
|
+
return {
|
|
67
|
+
"namespaces": ["user"],
|
|
68
|
+
"binds_ro": list(_SYSTEM_BINDS_RO) + tool_roots(tool_paths),
|
|
69
|
+
"symlinks": list(_SYSTEM_SYMLINKS),
|
|
70
|
+
"tmpfs": ["/tmp"],
|
|
71
|
+
"env": {"PATH": "/usr/bin:/bin", "TMPDIR": "/tmp"},
|
|
72
|
+
"env_unset": ["DISPLAY"],
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
_OUTPUT_LIMIT_BYTES = 1 << 20 # per stream; deployment may lower, never raise
|
|
77
|
+
_CSI_RE = re.compile(r"\x1b\[[0-9;?]*[A-Za-z]")
|
|
78
|
+
_BAD_CHARS_RE = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]")
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def clean_stream(data):
|
|
82
|
+
"""Decode, strip terminal/control bytes, bound length with a marker.
|
|
83
|
+
|
|
84
|
+
Truncation counts exact input bytes; a split multibyte tail decodes
|
|
85
|
+
as U+FFFD, which the marker line explains by construction.
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
data: Raw stream bytes to clean.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
Decoded text, stripped, bounded, truncation marked.
|
|
92
|
+
"""
|
|
93
|
+
total = len(data)
|
|
94
|
+
text = data[:_OUTPUT_LIMIT_BYTES].decode("utf-8", "replace")
|
|
95
|
+
text = _CSI_RE.sub("", text)
|
|
96
|
+
text = _BAD_CHARS_RE.sub("", text)
|
|
97
|
+
if total > _OUTPUT_LIMIT_BYTES:
|
|
98
|
+
text += f"\n...[truncated after {_OUTPUT_LIMIT_BYTES} of {total} bytes]\n"
|
|
99
|
+
return text
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
_clean_stream = clean_stream
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def _kill_tree(proc):
|
|
106
|
+
"""SIGKILL one spawned process group, then reap the leader.
|
|
107
|
+
|
|
108
|
+
Every sandboxed spawn leads its own session (start_new_session),
|
|
109
|
+
so the group kill reaches grandchildren the leader already
|
|
110
|
+
orphaned. Missing-group errors mean everyone already exited.
|
|
111
|
+
"""
|
|
112
|
+
try:
|
|
113
|
+
os.killpg(proc.pid, signal.SIGKILL)
|
|
114
|
+
except (ProcessLookupError, PermissionError, OSError):
|
|
115
|
+
pass
|
|
116
|
+
try:
|
|
117
|
+
proc.kill()
|
|
118
|
+
except (ProcessLookupError, OSError):
|
|
119
|
+
pass
|
|
120
|
+
proc.wait()
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def pgid_survivors(pgid):
|
|
124
|
+
"""Pids still in one process group (Linux /proc scan).
|
|
125
|
+
|
|
126
|
+
Excludes this process. Used to CONFIRM a tree kill, never to
|
|
127
|
+
select new victims: kill by group first, then scan.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
pgid: Process group id to scan.
|
|
131
|
+
|
|
132
|
+
Returns:
|
|
133
|
+
Surviving pids in the group (Linux /proc scan).
|
|
134
|
+
"""
|
|
135
|
+
me = os.getpid()
|
|
136
|
+
found = []
|
|
137
|
+
procfs = Path("/proc")
|
|
138
|
+
if not procfs.is_dir():
|
|
139
|
+
return found
|
|
140
|
+
for entry in procfs.iterdir():
|
|
141
|
+
if not entry.name.isdigit() or int(entry.name) == me:
|
|
142
|
+
continue
|
|
143
|
+
try:
|
|
144
|
+
stat = (entry / "stat").read_text(encoding="utf-8")
|
|
145
|
+
except OSError:
|
|
146
|
+
continue
|
|
147
|
+
try:
|
|
148
|
+
group = int(stat.rsplit(")", 1)[1].split()[2])
|
|
149
|
+
except (IndexError, ValueError):
|
|
150
|
+
continue
|
|
151
|
+
if group == pgid:
|
|
152
|
+
found.append(int(entry.name))
|
|
153
|
+
return found
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
class BwrapRunner:
|
|
157
|
+
"""Bubblewrap-confined subprocess runs under one profile.
|
|
158
|
+
|
|
159
|
+
Args:
|
|
160
|
+
profile: Sandbox profile mapping (namespaces, binds,
|
|
161
|
+
tmpfs, env, unsets).
|
|
162
|
+
binary: bwrap binary name or path.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
def __init__(self, profile, binary="bwrap"):
|
|
166
|
+
self._profile = dict(profile)
|
|
167
|
+
self._binary = binary
|
|
168
|
+
|
|
169
|
+
def _resolve(self):
|
|
170
|
+
found = shutil.which(self._binary)
|
|
171
|
+
if found is not None:
|
|
172
|
+
return found
|
|
173
|
+
if Path(self._binary).is_file():
|
|
174
|
+
return self._binary
|
|
175
|
+
raise ExecutionError(
|
|
176
|
+
"missing_dependency", f"bwrap not found: {self._binary}."
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
def _argv(self, binary, workdir, command, extra_binds=(), extra_env=None):
|
|
180
|
+
argv = [binary]
|
|
181
|
+
for ns in self._profile.get("namespaces", ["user"]):
|
|
182
|
+
argv.append(f"--unshare-{ns}")
|
|
183
|
+
argv.append("--die-with-parent")
|
|
184
|
+
for src, dest in self._profile.get("binds_ro", []):
|
|
185
|
+
argv += ["--ro-bind", src, dest]
|
|
186
|
+
for src, dest in extra_binds:
|
|
187
|
+
argv += ["--ro-bind", src, dest]
|
|
188
|
+
for src, dest in self._profile.get("symlinks", []):
|
|
189
|
+
argv += ["--symlink", src, dest]
|
|
190
|
+
for path in self._profile.get("tmpfs", []):
|
|
191
|
+
argv += ["--tmpfs", path]
|
|
192
|
+
argv += ["--bind", str(workdir), str(workdir)]
|
|
193
|
+
for key, value in self._profile.get("env", {}).items():
|
|
194
|
+
argv += ["--setenv", key, value]
|
|
195
|
+
for key, value in dict(extra_env or {}).items():
|
|
196
|
+
argv += ["--setenv", key, value]
|
|
197
|
+
for key in self._profile.get("env_unset", []):
|
|
198
|
+
argv += ["--unsetenv", key]
|
|
199
|
+
argv += ["--chdir", str(workdir), *command]
|
|
200
|
+
return argv
|
|
201
|
+
|
|
202
|
+
def argv_for(self, workdir, command, extra_binds=(), extra_env=None):
|
|
203
|
+
"""Sandbox argv for one run; per-run env rides as extra `--setenv`.
|
|
204
|
+
|
|
205
|
+
Uses the configured binary verbatim (no PATH resolution): call
|
|
206
|
+
after a passing preflight, or let PATH lookup resolve it at
|
|
207
|
+
spawn. Unset entries are emitted last, so unset wins on a
|
|
208
|
+
set/unset conflict by construction.
|
|
209
|
+
|
|
210
|
+
Args:
|
|
211
|
+
workdir: Directory bound read-write at its host path.
|
|
212
|
+
command: Command argv to run inside the sandbox.
|
|
213
|
+
extra_binds: Per-run (src, dest) ro-bind pairs.
|
|
214
|
+
extra_env: Per-run env mapping emitted as --setenv.
|
|
215
|
+
|
|
216
|
+
Returns:
|
|
217
|
+
Sandbox argv for one run.
|
|
218
|
+
"""
|
|
219
|
+
return self._argv(
|
|
220
|
+
self._binary, workdir, command, extra_binds, extra_env
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
def preflight(self, timeout_s=30):
|
|
224
|
+
"""Prove the profile on a trivial command before real work.
|
|
225
|
+
|
|
226
|
+
Unprovable profiles refuse loudly instead of degrading
|
|
227
|
+
silently.
|
|
228
|
+
|
|
229
|
+
Args:
|
|
230
|
+
timeout_s: Probe budget in seconds.
|
|
231
|
+
|
|
232
|
+
Returns:
|
|
233
|
+
Readiness mapping with the resolved binary.
|
|
234
|
+
|
|
235
|
+
Raises:
|
|
236
|
+
ExecutionError: missing_dependency when bwrap is not
|
|
237
|
+
found, runner_unavailable when the probe fails or
|
|
238
|
+
times out.
|
|
239
|
+
"""
|
|
240
|
+
binary = self._resolve()
|
|
241
|
+
with tempfile.TemporaryDirectory(prefix="bwrap_probe_") as tmp:
|
|
242
|
+
try:
|
|
243
|
+
proc = subprocess.run(
|
|
244
|
+
self._argv(binary, tmp, ["/bin/true"]),
|
|
245
|
+
capture_output=True,
|
|
246
|
+
text=True,
|
|
247
|
+
timeout=timeout_s,
|
|
248
|
+
)
|
|
249
|
+
except subprocess.TimeoutExpired as exc:
|
|
250
|
+
raise ExecutionError(
|
|
251
|
+
"runner_unavailable", "bwrap probe timed out."
|
|
252
|
+
) from exc
|
|
253
|
+
if proc.returncode != 0:
|
|
254
|
+
lines = (proc.stderr or "").strip().splitlines()
|
|
255
|
+
detail = lines[0] if lines else f"exit {proc.returncode}"
|
|
256
|
+
raise ExecutionError(
|
|
257
|
+
"runner_unavailable", f"bwrap probe failed: {detail}."
|
|
258
|
+
)
|
|
259
|
+
return {"ready": True, "binary": binary}
|
|
260
|
+
|
|
261
|
+
def run(self, command, workdir, timeout_s=120, extra_binds=(), input_text=None):
|
|
262
|
+
"""Run one command confined, budgeted, with cleaned streams.
|
|
263
|
+
|
|
264
|
+
Timeouts kill the whole process group, then raise — never a
|
|
265
|
+
hanging engine or a partial pass.
|
|
266
|
+
|
|
267
|
+
Args:
|
|
268
|
+
command: Command argv to run inside the sandbox.
|
|
269
|
+
workdir: Directory bound read-write at its host path.
|
|
270
|
+
timeout_s: Run budget in seconds.
|
|
271
|
+
extra_binds: Per-run (src, dest) ro-bind pairs.
|
|
272
|
+
input_text: Stdin text, or None for no pipe.
|
|
273
|
+
|
|
274
|
+
Returns:
|
|
275
|
+
Completed process with cleaned stdout/stderr text.
|
|
276
|
+
|
|
277
|
+
Raises:
|
|
278
|
+
ExecutionError: missing_dependency when bwrap is not
|
|
279
|
+
found, invalid_reference for a missing workdir,
|
|
280
|
+
runner_unavailable on timeout.
|
|
281
|
+
"""
|
|
282
|
+
binary = self._resolve()
|
|
283
|
+
if not Path(workdir).is_dir():
|
|
284
|
+
raise ExecutionError(
|
|
285
|
+
"invalid_reference", f"Workdir not found: {workdir}."
|
|
286
|
+
)
|
|
287
|
+
proc = subprocess.Popen(
|
|
288
|
+
self._argv(binary, workdir, command, extra_binds),
|
|
289
|
+
stdin=subprocess.PIPE if input_text is not None else None,
|
|
290
|
+
stdout=subprocess.PIPE,
|
|
291
|
+
stderr=subprocess.PIPE,
|
|
292
|
+
cwd=workdir,
|
|
293
|
+
start_new_session=True,
|
|
294
|
+
)
|
|
295
|
+
try:
|
|
296
|
+
out_bytes, err_bytes = proc.communicate(
|
|
297
|
+
input_text.encode() if input_text is not None else None,
|
|
298
|
+
timeout=timeout_s,
|
|
299
|
+
)
|
|
300
|
+
except subprocess.TimeoutExpired as exc:
|
|
301
|
+
_kill_tree(proc)
|
|
302
|
+
raise ExecutionError(
|
|
303
|
+
"runner_unavailable",
|
|
304
|
+
f"Sandboxed run timed out after {timeout_s}s.",
|
|
305
|
+
) from exc
|
|
306
|
+
return subprocess.CompletedProcess(
|
|
307
|
+
proc.args,
|
|
308
|
+
proc.returncode,
|
|
309
|
+
_clean_stream(out_bytes),
|
|
310
|
+
_clean_stream(err_bytes),
|
|
311
|
+
)
|