dr-exec 0.1.1__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.
- dr_exec/__init__.py +227 -0
- dr_exec/_bootstrap.py +206 -0
- dr_exec/_declaration.py +131 -0
- dr_exec/_identity.py +285 -0
- dr_exec/_model.py +251 -0
- dr_exec/_probe.py +85 -0
- dr_exec/_protocol.py +438 -0
- dr_exec/_provenance.py +104 -0
- dr_exec/_retention.py +137 -0
- dr_exec/_scheduler.py +621 -0
- dr_exec/_spawn.py +271 -0
- dr_exec/_wire.py +48 -0
- dr_exec/cancel.py +25 -0
- dr_exec/declare.py +390 -0
- dr_exec/engine.py +1605 -0
- dr_exec/errors.py +13 -0
- dr_exec/executor.py +175 -0
- dr_exec/fake.py +157 -0
- dr_exec/kinds.py +134 -0
- dr_exec/names.py +16 -0
- dr_exec/pool.py +510 -0
- dr_exec/protocols.py +75 -0
- dr_exec/py.typed +0 -0
- dr_exec/record.py +463 -0
- dr_exec/runtime.py +141 -0
- dr_exec/store.py +568 -0
- dr_exec-0.1.1.dist-info/METADATA +55 -0
- dr_exec-0.1.1.dist-info/RECORD +29 -0
- dr_exec-0.1.1.dist-info/WHEEL +4 -0
dr_exec/__init__.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""Public dr-exec v1 capability and data-contract surface."""
|
|
2
|
+
|
|
3
|
+
from dr_exec.cancel import CancelToken
|
|
4
|
+
from dr_exec.declare import (
|
|
5
|
+
Budgets,
|
|
6
|
+
ByteBudget,
|
|
7
|
+
CountBudget,
|
|
8
|
+
DurationBudget,
|
|
9
|
+
EnvGrant,
|
|
10
|
+
EnvGrantRecord,
|
|
11
|
+
EnvVar,
|
|
12
|
+
ExecutionJob,
|
|
13
|
+
ExecutionTarget,
|
|
14
|
+
ExecutorSelfBudgets,
|
|
15
|
+
FiniteByteLimit,
|
|
16
|
+
FiniteCountLimit,
|
|
17
|
+
FiniteDurationLimit,
|
|
18
|
+
FiniteOutput,
|
|
19
|
+
OutputBudget,
|
|
20
|
+
PayloadRetentionBudget,
|
|
21
|
+
StreamRetentionBudget,
|
|
22
|
+
TrustedCommandTarget,
|
|
23
|
+
UnbudgetedLimit,
|
|
24
|
+
UnbudgetedOutput,
|
|
25
|
+
UntrustedCommandTarget,
|
|
26
|
+
UntrustedPythonTarget,
|
|
27
|
+
)
|
|
28
|
+
from dr_exec.errors import DeclarationError, ExecutorFailure, RecordLoadError
|
|
29
|
+
from dr_exec.executor import ProcessExecutor
|
|
30
|
+
from dr_exec.fake import FakeExecutor
|
|
31
|
+
from dr_exec.kinds import (
|
|
32
|
+
BudgetAxis,
|
|
33
|
+
CapacitySource,
|
|
34
|
+
ContainmentProfile,
|
|
35
|
+
EnvGrantKind,
|
|
36
|
+
ExecutionPoolState,
|
|
37
|
+
ExecutionTargetKind,
|
|
38
|
+
FailureOwner,
|
|
39
|
+
LimitKind,
|
|
40
|
+
OutcomeKind,
|
|
41
|
+
OutputOverflowPolicy,
|
|
42
|
+
ProtocolFailureCode,
|
|
43
|
+
RecordReceiptKind,
|
|
44
|
+
RecordState,
|
|
45
|
+
RuntimeKind,
|
|
46
|
+
)
|
|
47
|
+
from dr_exec.names import AttemptId, ExecutionId, JobId
|
|
48
|
+
from dr_exec.pool import (
|
|
49
|
+
AutoPoolCapacity,
|
|
50
|
+
EffectivePoolCapacity,
|
|
51
|
+
ExecutionCompletion,
|
|
52
|
+
ExecutionPool,
|
|
53
|
+
ExecutionPoolConfig,
|
|
54
|
+
ExecutionSubmission,
|
|
55
|
+
FixedPoolCapacity,
|
|
56
|
+
PoolCapacity,
|
|
57
|
+
)
|
|
58
|
+
from dr_exec.protocols import Executor, RunStore, Runtime
|
|
59
|
+
from dr_exec.record import (
|
|
60
|
+
BudgetExceededOutcome,
|
|
61
|
+
BudgetExceededOutcomeRecord,
|
|
62
|
+
CancelledOutcome,
|
|
63
|
+
CancelledOutcomeRecord,
|
|
64
|
+
CompletedExecution,
|
|
65
|
+
CompleteRecordReceipt,
|
|
66
|
+
DegradedRecordReceipt,
|
|
67
|
+
ExecutionAttribution,
|
|
68
|
+
ExecutionAttributionRecord,
|
|
69
|
+
ExecutionMeasurements,
|
|
70
|
+
ExecutionOutcome,
|
|
71
|
+
ExecutionOutcomeRecord,
|
|
72
|
+
ExecutionResult,
|
|
73
|
+
ExecutionResultRecord,
|
|
74
|
+
ExecutionTargetRecord,
|
|
75
|
+
ExitedOutcome,
|
|
76
|
+
ExitedOutcomeRecord,
|
|
77
|
+
FakeRecordReceipt,
|
|
78
|
+
FinalizedRecord,
|
|
79
|
+
OutputArtifactRecord,
|
|
80
|
+
OutputArtifactRecords,
|
|
81
|
+
PayloadOutputRecords,
|
|
82
|
+
PayloadOutputs,
|
|
83
|
+
PreparedRecord,
|
|
84
|
+
ProcessRecord,
|
|
85
|
+
ProtocolFailedOutcome,
|
|
86
|
+
ProtocolFailedOutcomeRecord,
|
|
87
|
+
RealRecordReceipt,
|
|
88
|
+
RecordingFailure,
|
|
89
|
+
RecordReceipt,
|
|
90
|
+
RetainedPayloadStream,
|
|
91
|
+
RetainedPayloadStreamRecord,
|
|
92
|
+
RunDeclaration,
|
|
93
|
+
RunningRecord,
|
|
94
|
+
RunRecord,
|
|
95
|
+
RunRecordHeader,
|
|
96
|
+
SignaledOutcome,
|
|
97
|
+
SignaledOutcomeRecord,
|
|
98
|
+
SpawnAbsentOutcome,
|
|
99
|
+
SpawnAbsentOutcomeRecord,
|
|
100
|
+
SpawnFailedOutcome,
|
|
101
|
+
SpawnFailedOutcomeRecord,
|
|
102
|
+
TrustedCommandTargetRecord,
|
|
103
|
+
UntrustedCommandTargetRecord,
|
|
104
|
+
UntrustedPythonTargetRecord,
|
|
105
|
+
)
|
|
106
|
+
from dr_exec.runtime import (
|
|
107
|
+
IsolatedHostPythonRuntime,
|
|
108
|
+
PreparedPythonProcess,
|
|
109
|
+
RuntimeRecord,
|
|
110
|
+
)
|
|
111
|
+
from dr_exec.store import (
|
|
112
|
+
DirectoryRunStore,
|
|
113
|
+
FinalizableRun,
|
|
114
|
+
PreparedRun,
|
|
115
|
+
RunningRun,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
__all__ = [
|
|
119
|
+
"AttemptId",
|
|
120
|
+
"AutoPoolCapacity",
|
|
121
|
+
"BudgetAxis",
|
|
122
|
+
"BudgetExceededOutcome",
|
|
123
|
+
"BudgetExceededOutcomeRecord",
|
|
124
|
+
"Budgets",
|
|
125
|
+
"ByteBudget",
|
|
126
|
+
"CancelToken",
|
|
127
|
+
"CancelledOutcome",
|
|
128
|
+
"CancelledOutcomeRecord",
|
|
129
|
+
"CapacitySource",
|
|
130
|
+
"CompleteRecordReceipt",
|
|
131
|
+
"CompletedExecution",
|
|
132
|
+
"ContainmentProfile",
|
|
133
|
+
"CountBudget",
|
|
134
|
+
"DeclarationError",
|
|
135
|
+
"DegradedRecordReceipt",
|
|
136
|
+
"DirectoryRunStore",
|
|
137
|
+
"DurationBudget",
|
|
138
|
+
"EffectivePoolCapacity",
|
|
139
|
+
"EnvGrant",
|
|
140
|
+
"EnvGrantKind",
|
|
141
|
+
"EnvGrantRecord",
|
|
142
|
+
"EnvVar",
|
|
143
|
+
"ExecutionAttribution",
|
|
144
|
+
"ExecutionAttributionRecord",
|
|
145
|
+
"ExecutionCompletion",
|
|
146
|
+
"ExecutionId",
|
|
147
|
+
"ExecutionJob",
|
|
148
|
+
"ExecutionMeasurements",
|
|
149
|
+
"ExecutionOutcome",
|
|
150
|
+
"ExecutionOutcomeRecord",
|
|
151
|
+
"ExecutionPool",
|
|
152
|
+
"ExecutionPoolConfig",
|
|
153
|
+
"ExecutionPoolState",
|
|
154
|
+
"ExecutionResult",
|
|
155
|
+
"ExecutionResultRecord",
|
|
156
|
+
"ExecutionSubmission",
|
|
157
|
+
"ExecutionTarget",
|
|
158
|
+
"ExecutionTargetKind",
|
|
159
|
+
"ExecutionTargetRecord",
|
|
160
|
+
"Executor",
|
|
161
|
+
"ExecutorFailure",
|
|
162
|
+
"ExecutorSelfBudgets",
|
|
163
|
+
"ExitedOutcome",
|
|
164
|
+
"ExitedOutcomeRecord",
|
|
165
|
+
"FailureOwner",
|
|
166
|
+
"FakeExecutor",
|
|
167
|
+
"FakeRecordReceipt",
|
|
168
|
+
"FinalizableRun",
|
|
169
|
+
"FinalizedRecord",
|
|
170
|
+
"FiniteByteLimit",
|
|
171
|
+
"FiniteCountLimit",
|
|
172
|
+
"FiniteDurationLimit",
|
|
173
|
+
"FiniteOutput",
|
|
174
|
+
"FixedPoolCapacity",
|
|
175
|
+
"IsolatedHostPythonRuntime",
|
|
176
|
+
"JobId",
|
|
177
|
+
"LimitKind",
|
|
178
|
+
"OutcomeKind",
|
|
179
|
+
"OutputArtifactRecord",
|
|
180
|
+
"OutputArtifactRecords",
|
|
181
|
+
"OutputBudget",
|
|
182
|
+
"OutputOverflowPolicy",
|
|
183
|
+
"PayloadOutputRecords",
|
|
184
|
+
"PayloadOutputs",
|
|
185
|
+
"PayloadRetentionBudget",
|
|
186
|
+
"PoolCapacity",
|
|
187
|
+
"PreparedPythonProcess",
|
|
188
|
+
"PreparedRecord",
|
|
189
|
+
"PreparedRun",
|
|
190
|
+
"ProcessExecutor",
|
|
191
|
+
"ProcessRecord",
|
|
192
|
+
"ProtocolFailedOutcome",
|
|
193
|
+
"ProtocolFailedOutcomeRecord",
|
|
194
|
+
"ProtocolFailureCode",
|
|
195
|
+
"RealRecordReceipt",
|
|
196
|
+
"RecordLoadError",
|
|
197
|
+
"RecordReceipt",
|
|
198
|
+
"RecordReceiptKind",
|
|
199
|
+
"RecordState",
|
|
200
|
+
"RecordingFailure",
|
|
201
|
+
"RetainedPayloadStream",
|
|
202
|
+
"RetainedPayloadStreamRecord",
|
|
203
|
+
"RunDeclaration",
|
|
204
|
+
"RunRecord",
|
|
205
|
+
"RunRecordHeader",
|
|
206
|
+
"RunStore",
|
|
207
|
+
"RunningRecord",
|
|
208
|
+
"RunningRun",
|
|
209
|
+
"Runtime",
|
|
210
|
+
"RuntimeKind",
|
|
211
|
+
"RuntimeRecord",
|
|
212
|
+
"SignaledOutcome",
|
|
213
|
+
"SignaledOutcomeRecord",
|
|
214
|
+
"SpawnAbsentOutcome",
|
|
215
|
+
"SpawnAbsentOutcomeRecord",
|
|
216
|
+
"SpawnFailedOutcome",
|
|
217
|
+
"SpawnFailedOutcomeRecord",
|
|
218
|
+
"StreamRetentionBudget",
|
|
219
|
+
"TrustedCommandTarget",
|
|
220
|
+
"TrustedCommandTargetRecord",
|
|
221
|
+
"UnbudgetedLimit",
|
|
222
|
+
"UnbudgetedOutput",
|
|
223
|
+
"UntrustedCommandTarget",
|
|
224
|
+
"UntrustedCommandTargetRecord",
|
|
225
|
+
"UntrustedPythonTarget",
|
|
226
|
+
"UntrustedPythonTargetRecord",
|
|
227
|
+
]
|
dr_exec/_bootstrap.py
ADDED
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
"""Fixed isolated invocation shape and the library-owned child wrapper.
|
|
2
|
+
|
|
3
|
+
The interpreter is always invoked as ``<executable> -I -c <source>``. The
|
|
4
|
+
consumer's ``driver_source`` never reaches argv, a shell, or an import
|
|
5
|
+
path: the library-owned wrapper carries it as one embedded string literal
|
|
6
|
+
bound to a fixed name.
|
|
7
|
+
|
|
8
|
+
The wrapper runs inside an isolated host interpreter that is not required
|
|
9
|
+
to have dr-exec importable, so its body is stdlib-only and reproduces the
|
|
10
|
+
pinned canonical JSON profile directly. It opens the protected descriptor
|
|
11
|
+
before any domain code, reads the request through EOF, validates it,
|
|
12
|
+
resolves ``dr_exec_main``, and writes LF-terminated canonical frames.
|
|
13
|
+
|
|
14
|
+
Failure ownership is split at the wrapper boundary. A missing or
|
|
15
|
+
non-callable entrypoint, a source-load failure, and a callback failure are
|
|
16
|
+
payload-owned: the wrapper stops without a completion frame, so the parent
|
|
17
|
+
observes an incomplete stream with every previously accepted output
|
|
18
|
+
preserved. A protected-writer failure is executor-owned machinery failure.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
# Child-observable literals. The invocation shape, the entrypoint name,
|
|
24
|
+
# the embedded-source binding, and the protected descriptor number are
|
|
25
|
+
# pinned; changing any of them is a standing-contract revision, not an
|
|
26
|
+
# implementation detail.
|
|
27
|
+
ISOLATED_INVOCATION_ARGUMENTS = ("-I", "-c")
|
|
28
|
+
DRIVER_ENTRYPOINT_NAME = "dr_exec_main"
|
|
29
|
+
DRIVER_SOURCE_BINDING = "DR_EXEC_DRIVER_SOURCE"
|
|
30
|
+
PROTOCOL_DESCRIPTOR = 3
|
|
31
|
+
|
|
32
|
+
# The wrapper body, stdlib-only and self-contained. It reads its own
|
|
33
|
+
# module globals and never inspects argv or the environment. The pinned
|
|
34
|
+
# child-observable literals are not spelled here: they are rendered from
|
|
35
|
+
# the module constants above, which are their single source.
|
|
36
|
+
_WRAPPER_BODY = '''
|
|
37
|
+
import hashlib as _dr_exec_hashlib
|
|
38
|
+
import json as _dr_exec_json
|
|
39
|
+
import os as _dr_exec_os
|
|
40
|
+
import sys as _dr_exec_sys
|
|
41
|
+
|
|
42
|
+
_DR_EXEC_IDENTITY_FIELDS = {"schema", "schema_version", "payload"}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _dr_exec_canonical(value):
|
|
46
|
+
"""Render the pinned canonical JSON profile as exact UTF-8 bytes."""
|
|
47
|
+
return _dr_exec_json.dumps(
|
|
48
|
+
value,
|
|
49
|
+
sort_keys=True,
|
|
50
|
+
separators=(",", ":"),
|
|
51
|
+
ensure_ascii=True,
|
|
52
|
+
allow_nan=False,
|
|
53
|
+
).encode("utf-8")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _dr_exec_strict(value, path):
|
|
57
|
+
"""Accept only strict JSON values; reject anything else by path."""
|
|
58
|
+
if value is None or isinstance(value, (str, bool, int)):
|
|
59
|
+
return
|
|
60
|
+
if isinstance(value, float):
|
|
61
|
+
if value != value or value in (float("inf"), float("-inf")):
|
|
62
|
+
raise ValueError("non-finite number at " + path)
|
|
63
|
+
return
|
|
64
|
+
if isinstance(value, list):
|
|
65
|
+
for index, item in enumerate(value):
|
|
66
|
+
_dr_exec_strict(item, path + "[" + str(index) + "]")
|
|
67
|
+
return
|
|
68
|
+
if isinstance(value, dict):
|
|
69
|
+
for key, item in value.items():
|
|
70
|
+
if not isinstance(key, str):
|
|
71
|
+
raise ValueError("non-string key at " + path)
|
|
72
|
+
_dr_exec_strict(item, path + "." + key)
|
|
73
|
+
return
|
|
74
|
+
raise ValueError("unsupported value at " + path)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _dr_exec_identity(document, origin):
|
|
78
|
+
"""Validate the exact three-field Identity Document shape."""
|
|
79
|
+
if not isinstance(document, dict):
|
|
80
|
+
raise ValueError(origin + " must be an object")
|
|
81
|
+
if set(document) != _DR_EXEC_IDENTITY_FIELDS:
|
|
82
|
+
raise ValueError(origin + " must have exactly the identity fields")
|
|
83
|
+
if not isinstance(document["schema"], str):
|
|
84
|
+
raise ValueError(origin + " schema must be a string")
|
|
85
|
+
version = document["schema_version"]
|
|
86
|
+
if isinstance(version, bool) or not isinstance(version, int):
|
|
87
|
+
raise ValueError(origin + " schema_version must be an integer")
|
|
88
|
+
_dr_exec_strict(document["payload"], origin + ".payload")
|
|
89
|
+
return document
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _dr_exec_read_request():
|
|
93
|
+
"""Read stdin through EOF and validate the canonical request."""
|
|
94
|
+
data = _dr_exec_sys.stdin.buffer.read()
|
|
95
|
+
try:
|
|
96
|
+
decoded = _dr_exec_json.loads(data.decode("utf-8"))
|
|
97
|
+
except (UnicodeDecodeError, ValueError) as error:
|
|
98
|
+
raise ValueError("request is not strict JSON") from error
|
|
99
|
+
document = _dr_exec_identity(decoded, "request")
|
|
100
|
+
if _dr_exec_canonical(document) != data:
|
|
101
|
+
raise ValueError("request bytes are not canonical JSON bytes")
|
|
102
|
+
return document, data
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class _DrExecProtocolWriter:
|
|
106
|
+
"""The protected fd 3 writer, owned by the library, not the payload.
|
|
107
|
+
|
|
108
|
+
The descriptor is duplicated and the original closed at construction,
|
|
109
|
+
so the handle survives domain code that replaces language-level
|
|
110
|
+
stdout or stderr, and the payload cannot reach the protected stream
|
|
111
|
+
through the well-known descriptor number. Every frame is flushed
|
|
112
|
+
before its call returns, so a later payload crash cannot lose an
|
|
113
|
+
output the parent already accepted.
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
def __init__(self, descriptor):
|
|
117
|
+
self._stream = _dr_exec_os.fdopen(_dr_exec_os.dup(descriptor), "wb")
|
|
118
|
+
_dr_exec_os.close(descriptor)
|
|
119
|
+
self._sequence = 0
|
|
120
|
+
self._closed = False
|
|
121
|
+
|
|
122
|
+
def prelude(self, request_bytes):
|
|
123
|
+
digest = _dr_exec_hashlib.sha256(request_bytes).hexdigest()
|
|
124
|
+
self._frame({
|
|
125
|
+
"version": 1,
|
|
126
|
+
"kind": "prelude",
|
|
127
|
+
"request_id_sha256": digest,
|
|
128
|
+
})
|
|
129
|
+
|
|
130
|
+
def emit(self, document):
|
|
131
|
+
validated = _dr_exec_identity(document, "output document")
|
|
132
|
+
self._frame({
|
|
133
|
+
"version": 1,
|
|
134
|
+
"kind": "output",
|
|
135
|
+
"sequence": self._sequence,
|
|
136
|
+
"document": {
|
|
137
|
+
"schema": validated["schema"],
|
|
138
|
+
"schema_version": validated["schema_version"],
|
|
139
|
+
"payload": validated["payload"],
|
|
140
|
+
},
|
|
141
|
+
})
|
|
142
|
+
self._sequence += 1
|
|
143
|
+
|
|
144
|
+
def complete(self):
|
|
145
|
+
self._frame({
|
|
146
|
+
"version": 1,
|
|
147
|
+
"kind": "complete",
|
|
148
|
+
"output_count": self._sequence,
|
|
149
|
+
})
|
|
150
|
+
self._closed = True
|
|
151
|
+
self._stream.close()
|
|
152
|
+
|
|
153
|
+
def _frame(self, frame):
|
|
154
|
+
if self._closed:
|
|
155
|
+
raise ValueError("the protected stream is already complete")
|
|
156
|
+
self._stream.write(_dr_exec_canonical(frame) + b"\\n")
|
|
157
|
+
self._stream.flush()
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def _dr_exec_bootstrap():
|
|
161
|
+
writer = _DrExecProtocolWriter(_DR_EXEC_PROTOCOL_DESCRIPTOR)
|
|
162
|
+
request, request_bytes = _dr_exec_read_request()
|
|
163
|
+
writer.prelude(request_bytes)
|
|
164
|
+
namespace = {"__name__": "dr_exec_driver"}
|
|
165
|
+
exec(DR_EXEC_DRIVER_SOURCE, namespace)
|
|
166
|
+
entrypoint = namespace.get(_DR_EXEC_ENTRYPOINT_NAME)
|
|
167
|
+
if not callable(entrypoint):
|
|
168
|
+
raise ValueError(
|
|
169
|
+
"driver_source must define a callable " + _DR_EXEC_ENTRYPOINT_NAME
|
|
170
|
+
)
|
|
171
|
+
entrypoint(request, writer.emit)
|
|
172
|
+
writer.complete()
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
_dr_exec_bootstrap()
|
|
176
|
+
'''
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def driver_wrapper_source(driver_source: str, /) -> str:
|
|
180
|
+
"""Return the library-owned wrapper source embedding ``driver_source``.
|
|
181
|
+
|
|
182
|
+
``driver_source`` is embedded through ``repr``, so arbitrary consumer
|
|
183
|
+
text -- quotes, backslashes, and newlines included -- stays one inert
|
|
184
|
+
string literal in the wrapper rather than executable wrapper syntax.
|
|
185
|
+
The pinned child-observable literals are rendered from the module
|
|
186
|
+
constants, so the child can only ever observe their one spelling.
|
|
187
|
+
"""
|
|
188
|
+
if "\0" in driver_source:
|
|
189
|
+
raise ValueError("driver_source must not contain NUL")
|
|
190
|
+
return "\n".join(
|
|
191
|
+
(
|
|
192
|
+
f"{DRIVER_SOURCE_BINDING} = {driver_source!r}",
|
|
193
|
+
f"_DR_EXEC_PROTOCOL_DESCRIPTOR = {PROTOCOL_DESCRIPTOR}",
|
|
194
|
+
f"_DR_EXEC_ENTRYPOINT_NAME = {DRIVER_ENTRYPOINT_NAME!r}",
|
|
195
|
+
_WRAPPER_BODY,
|
|
196
|
+
)
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
__all__ = [
|
|
201
|
+
"DRIVER_ENTRYPOINT_NAME",
|
|
202
|
+
"DRIVER_SOURCE_BINDING",
|
|
203
|
+
"ISOLATED_INVOCATION_ARGUMENTS",
|
|
204
|
+
"PROTOCOL_DESCRIPTOR",
|
|
205
|
+
"driver_wrapper_source",
|
|
206
|
+
]
|
dr_exec/_declaration.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""The declaration rules every executor applies, production or fake.
|
|
2
|
+
|
|
3
|
+
These are the checks that read only the declaration itself: the bytes the
|
|
4
|
+
child would receive against the declared input budget, and whether argv[0]
|
|
5
|
+
has a defensible meaning under the declared environment grant. They depend
|
|
6
|
+
on no host, no runtime, and no process, so one implementation serves both
|
|
7
|
+
`ProcessExecutor` and `FakeExecutor` and the two cannot drift into
|
|
8
|
+
accepting different declarations.
|
|
9
|
+
|
|
10
|
+
Host support is deliberately *not* here. Refusing an unsupported platform
|
|
11
|
+
is a statement about where a containment claim holds, not about whether a
|
|
12
|
+
declaration is well-formed, and only the production path makes that claim.
|
|
13
|
+
|
|
14
|
+
The Python target's transport bytes are the canonical request document,
|
|
15
|
+
which the declaration already carries in full: the runtime chooses the
|
|
16
|
+
interpreter invocation but never the request, so the input length measured
|
|
17
|
+
here is the same length the engine measures after preparation.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import os
|
|
23
|
+
from pathlib import Path
|
|
24
|
+
|
|
25
|
+
from dr_exec._protocol import request_transport_bytes
|
|
26
|
+
from dr_exec.declare import (
|
|
27
|
+
EnvGrant,
|
|
28
|
+
ExecutionJob,
|
|
29
|
+
FiniteByteLimit,
|
|
30
|
+
TrustedCommandTarget,
|
|
31
|
+
UntrustedCommandTarget,
|
|
32
|
+
UntrustedPythonTarget,
|
|
33
|
+
)
|
|
34
|
+
from dr_exec.errors import DeclarationError
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def granted_environment(grant: EnvGrant, /) -> dict[str, str]:
|
|
38
|
+
"""Materialize the exact environment the child receives.
|
|
39
|
+
|
|
40
|
+
Values were snapshotted when the grant was constructed, so nothing
|
|
41
|
+
here consults the parent's live environment: the grant is the whole
|
|
42
|
+
inherited state.
|
|
43
|
+
"""
|
|
44
|
+
return {variable.name: variable.value for variable in grant.variables}
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def declared_input_bytes(job: ExecutionJob, /) -> bytes:
|
|
48
|
+
"""Return the exact bytes the declaration would send on child stdin."""
|
|
49
|
+
match job.target:
|
|
50
|
+
case TrustedCommandTarget() | UntrustedCommandTarget():
|
|
51
|
+
return job.target.stdin
|
|
52
|
+
case UntrustedPythonTarget():
|
|
53
|
+
return request_transport_bytes(job.target.request)
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def validate_input_budget(job: ExecutionJob, stdin_bytes: bytes, /) -> None:
|
|
57
|
+
"""Compare declared input length with the budget before any spawn.
|
|
58
|
+
|
|
59
|
+
Input bounds are the one workload axis checked before a child exists,
|
|
60
|
+
so an over-budget input never costs a spawn.
|
|
61
|
+
"""
|
|
62
|
+
budget = job.budgets.input_bytes
|
|
63
|
+
limit = budget.max_bytes if isinstance(budget, FiniteByteLimit) else None
|
|
64
|
+
if limit is not None and len(stdin_bytes) > limit:
|
|
65
|
+
raise DeclarationError(
|
|
66
|
+
f"declared input of {len(stdin_bytes)} bytes exceeds the "
|
|
67
|
+
f"{limit}-byte input budget"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def validate_command_resolvability(
|
|
72
|
+
argv: tuple[str, ...],
|
|
73
|
+
environment: dict[str, str],
|
|
74
|
+
/,
|
|
75
|
+
) -> None:
|
|
76
|
+
"""Refuse an argv[0] the granted environment gives no meaning.
|
|
77
|
+
|
|
78
|
+
Absent a granted ``PATH``, only an absolute executable resolves; a
|
|
79
|
+
relative executable with no granted ``PATH`` has no defensible meaning
|
|
80
|
+
and is a declaration error rather than a spawn attempt that would
|
|
81
|
+
consult the parent's ambient search path.
|
|
82
|
+
|
|
83
|
+
A granted ``PATH`` resolves only through absolute entries, because the
|
|
84
|
+
child changes to its scratch directory before ``exec``: a relative hit
|
|
85
|
+
would name nothing the search found, and reading it against the
|
|
86
|
+
parent's location is the ambient cwd this package never consults. An
|
|
87
|
+
empty entry is the same case spelled shorter, since it means the
|
|
88
|
+
current directory.
|
|
89
|
+
|
|
90
|
+
A name that resolves nowhere is not refused here: production leaves
|
|
91
|
+
that to the spawn, which reports absence as an outcome rather than
|
|
92
|
+
raising.
|
|
93
|
+
"""
|
|
94
|
+
name = argv[0]
|
|
95
|
+
if Path(name).is_absolute():
|
|
96
|
+
return
|
|
97
|
+
granted_path = environment.get("PATH")
|
|
98
|
+
if granted_path is None:
|
|
99
|
+
raise DeclarationError(
|
|
100
|
+
"a relative executable requires a granted PATH: " + name
|
|
101
|
+
)
|
|
102
|
+
for entry in granted_path.split(os.pathsep):
|
|
103
|
+
if not Path(entry).is_absolute():
|
|
104
|
+
raise DeclarationError(
|
|
105
|
+
"a granted PATH resolves only through absolute entries: "
|
|
106
|
+
+ entry
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def validate_declaration(job: ExecutionJob, /) -> None:
|
|
111
|
+
"""Apply every host-independent declaration rule, in engine order."""
|
|
112
|
+
validate_input_budget(job, declared_input_bytes(job))
|
|
113
|
+
match job.target:
|
|
114
|
+
case TrustedCommandTarget() | UntrustedCommandTarget():
|
|
115
|
+
validate_command_resolvability(
|
|
116
|
+
job.target.argv, granted_environment(job.env)
|
|
117
|
+
)
|
|
118
|
+
case UntrustedPythonTarget():
|
|
119
|
+
# The runtime owns this invocation and resolved its absolute
|
|
120
|
+
# executable at construction, so there is no caller-declared
|
|
121
|
+
# argv[0] to resolve.
|
|
122
|
+
return
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
__all__ = [
|
|
126
|
+
"declared_input_bytes",
|
|
127
|
+
"granted_environment",
|
|
128
|
+
"validate_command_resolvability",
|
|
129
|
+
"validate_declaration",
|
|
130
|
+
"validate_input_budget",
|
|
131
|
+
]
|