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 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
+ ]
@@ -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
+ ]