amkernel 0.1.0__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.
- amkernel/__init__.py +7 -0
- amkernel/__main__.py +5 -0
- amkernel/artifact_store.py +1363 -0
- amkernel/cli.py +172 -0
- amkernel/distribution.py +597 -0
- amkernel/output.py +444 -0
- amkernel/protocol.py +1464 -0
- amkernel/py.typed +0 -0
- amkernel/runner.py +1222 -0
- amkernel/runtime.py +581 -0
- amkernel/server.py +774 -0
- amkernel/skill/SKILL.md +124 -0
- amkernel/skill/references/amkernel-appium.md +46 -0
- amkernel/skill/references/official-skills.json +232 -0
- amkernel/skill/references/official-sources.json +21 -0
- amkernel/skill/scripts/verify_official_skills.py +252 -0
- amkernel/structured_output.py +328 -0
- amkernel/supervisor.py +1453 -0
- amkernel/workspace.py +305 -0
- amkernel-0.1.0.dist-info/METADATA +105 -0
- amkernel-0.1.0.dist-info/RECORD +23 -0
- amkernel-0.1.0.dist-info/WHEEL +4 -0
- amkernel-0.1.0.dist-info/entry_points.txt +3 -0
amkernel/runner.py
ADDED
|
@@ -0,0 +1,1222 @@
|
|
|
1
|
+
"""Persistent Python Runner process for the private AMKernel protocol."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
import __future__
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import ast
|
|
8
|
+
import builtins
|
|
9
|
+
import hashlib
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
import platform
|
|
13
|
+
import signal
|
|
14
|
+
import sys
|
|
15
|
+
import traceback
|
|
16
|
+
import unicodedata
|
|
17
|
+
import uuid
|
|
18
|
+
from collections.abc import Sized
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from importlib import metadata as importlib_metadata
|
|
21
|
+
from types import CodeType, FrameType, TracebackType
|
|
22
|
+
from typing import BinaryIO, cast
|
|
23
|
+
|
|
24
|
+
from amkernel.protocol import (
|
|
25
|
+
DIAGNOSTIC_FD_ARGUMENT,
|
|
26
|
+
MAX_ARTIFACT_BYTES,
|
|
27
|
+
MAX_ARTIFACT_CHUNK_BYTES,
|
|
28
|
+
MAX_ARTIFACT_TRANSACTIONS,
|
|
29
|
+
MAX_ERROR_BYTES,
|
|
30
|
+
MAX_ERROR_TYPE_BYTES,
|
|
31
|
+
MAX_IMAGE_TRANSACTIONS,
|
|
32
|
+
MAX_JSON_DISPLAY_BYTES,
|
|
33
|
+
MAX_JSON_DISPLAY_ITEM_BYTES,
|
|
34
|
+
MAX_JSON_DISPLAYS,
|
|
35
|
+
MAX_PACKAGE_VERSION_BYTES,
|
|
36
|
+
MAX_PROTOCOL_ERROR_MESSAGE_BYTES,
|
|
37
|
+
MAX_RESULT_BYTES,
|
|
38
|
+
MAX_STRUCTURED_RESULT_BYTES,
|
|
39
|
+
REQUEST_FD_ARGUMENT,
|
|
40
|
+
RESPONSE_FD_ARGUMENT,
|
|
41
|
+
ArtifactBeginResponse,
|
|
42
|
+
ArtifactChunkResponse,
|
|
43
|
+
ArtifactEndResponse,
|
|
44
|
+
ArtifactKind,
|
|
45
|
+
DisplayJsonResponse,
|
|
46
|
+
DoneOutcome,
|
|
47
|
+
DoneResponse,
|
|
48
|
+
EnvironmentKind,
|
|
49
|
+
ErrorInfo,
|
|
50
|
+
ExecuteRequest,
|
|
51
|
+
InitializeRequest,
|
|
52
|
+
JsonValue,
|
|
53
|
+
ProtocolErrorCode,
|
|
54
|
+
ProtocolErrorResponse,
|
|
55
|
+
ProtocolViolation,
|
|
56
|
+
ReadyResponse,
|
|
57
|
+
Response,
|
|
58
|
+
RuntimeDiagnostics,
|
|
59
|
+
RuntimePackage,
|
|
60
|
+
ShutdownRequest,
|
|
61
|
+
StartedResponse,
|
|
62
|
+
encode_response,
|
|
63
|
+
json_size,
|
|
64
|
+
read_request,
|
|
65
|
+
snapshot_json,
|
|
66
|
+
validate_logical_name,
|
|
67
|
+
validate_mime_type,
|
|
68
|
+
write_frame,
|
|
69
|
+
)
|
|
70
|
+
from amkernel.structured_output import (
|
|
71
|
+
ArtifactError,
|
|
72
|
+
DisplayError,
|
|
73
|
+
StructuredOutputError,
|
|
74
|
+
bytes_like,
|
|
75
|
+
strict_json,
|
|
76
|
+
validate_image,
|
|
77
|
+
validate_safe_artifact,
|
|
78
|
+
)
|
|
79
|
+
from amkernel.workspace import Workspace, WorkspaceError
|
|
80
|
+
|
|
81
|
+
_REPR_BYTES = 4_096
|
|
82
|
+
_REPR_OBJECT_BYTES = 8_192
|
|
83
|
+
_TRACEBACK_TEXT_BYTES = 7_000
|
|
84
|
+
_ERROR_MESSAGE_BYTES = 2_048
|
|
85
|
+
_MAX_EXCEPTION_CHAIN = 32
|
|
86
|
+
_MAX_TRACEBACK_FRAMES = 100
|
|
87
|
+
_FUTURE_FLAGS = sum(
|
|
88
|
+
getattr(__future__, feature).compiler_flag for feature in __future__.all_feature_names
|
|
89
|
+
)
|
|
90
|
+
_GETCWD = os.getcwd
|
|
91
|
+
_REALPATH = os.path.realpath
|
|
92
|
+
_ABSPATH = os.path.abspath
|
|
93
|
+
_EXISTS = os.path.exists
|
|
94
|
+
_JOIN = os.path.join
|
|
95
|
+
_SAMEFILE = os.path.samefile
|
|
96
|
+
_INITIAL_EXECUTABLE = _ABSPATH(sys.executable)
|
|
97
|
+
_INITIAL_PYTHON_VERSION = platform.python_version()
|
|
98
|
+
_INITIAL_IMPLEMENTATION = platform.python_implementation()
|
|
99
|
+
_INITIAL_PLATFORM = platform.platform()
|
|
100
|
+
_INITIAL_PREFIX = sys.prefix
|
|
101
|
+
_INITIAL_BASE_PREFIX = sys.base_prefix
|
|
102
|
+
_PTHREAD_SIGMASK = signal.pthread_sigmask
|
|
103
|
+
_INSTALL_SIGNAL_HANDLER = signal.signal
|
|
104
|
+
_PENDING_SIGNALS = signal.sigpending
|
|
105
|
+
_WAIT_FOR_SIGNAL = signal.sigwait
|
|
106
|
+
_SIGINT = signal.SIGINT
|
|
107
|
+
_SIG_BLOCK = signal.SIG_BLOCK
|
|
108
|
+
_SIG_SETMASK = signal.SIG_SETMASK
|
|
109
|
+
_TRACEBACK_EXCEPTION = traceback.TracebackException
|
|
110
|
+
_DISTRIBUTION_VERSION = importlib_metadata.version
|
|
111
|
+
_PACKAGE_NOT_FOUND_ERROR = importlib_metadata.PackageNotFoundError
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass(slots=True)
|
|
115
|
+
class _SignalState:
|
|
116
|
+
executing: bool = False
|
|
117
|
+
|
|
118
|
+
def handle_sigint(self, _signum: int, _frame: FrameType | None) -> None:
|
|
119
|
+
if self.executing:
|
|
120
|
+
self.executing = False
|
|
121
|
+
raise KeyboardInterrupt
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
@dataclass(frozen=True, slots=True)
|
|
125
|
+
class _CellResult:
|
|
126
|
+
outcome: DoneOutcome
|
|
127
|
+
has_result: bool
|
|
128
|
+
result: JsonValue
|
|
129
|
+
error: ErrorInfo | None
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
_STRUCTURED_RESULT_HEADROOM = 2_097_152
|
|
133
|
+
_MAX_JSON_WIRE_BYTES = MAX_STRUCTURED_RESULT_BYTES - MAX_RESULT_BYTES - _STRUCTURED_RESULT_HEADROOM
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
@dataclass(frozen=True, slots=True)
|
|
137
|
+
class _Bootstrap:
|
|
138
|
+
request: BinaryIO
|
|
139
|
+
response: BinaryIO
|
|
140
|
+
diagnostic: BinaryIO
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
class _RuntimeDiagnosticsFailure(RuntimeError):
|
|
144
|
+
pass
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _build_argument_parser() -> argparse.ArgumentParser:
|
|
148
|
+
parser = argparse.ArgumentParser(prog="python -m amkernel.runner", allow_abbrev=False)
|
|
149
|
+
parser.add_argument(REQUEST_FD_ARGUMENT, type=int, required=True)
|
|
150
|
+
parser.add_argument(RESPONSE_FD_ARGUMENT, type=int, required=True)
|
|
151
|
+
parser.add_argument(DIAGNOSTIC_FD_ARGUMENT, type=int, required=True)
|
|
152
|
+
return parser
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _bootstrap(argv: list[str] | None) -> _Bootstrap:
|
|
156
|
+
raw_arguments = list(sys.argv[1:] if argv is None else argv)
|
|
157
|
+
for option in (REQUEST_FD_ARGUMENT, RESPONSE_FD_ARGUMENT, DIAGNOSTIC_FD_ARGUMENT):
|
|
158
|
+
occurrences = sum(
|
|
159
|
+
argument == option or argument.startswith(f"{option}=") for argument in raw_arguments
|
|
160
|
+
)
|
|
161
|
+
if occurrences != 1:
|
|
162
|
+
raise ValueError(f"{option} must appear exactly once")
|
|
163
|
+
arguments = _build_argument_parser().parse_args(raw_arguments)
|
|
164
|
+
descriptors = (
|
|
165
|
+
cast(int, arguments.request_fd),
|
|
166
|
+
cast(int, arguments.response_fd),
|
|
167
|
+
cast(int, arguments.diagnostic_fd),
|
|
168
|
+
)
|
|
169
|
+
if any(descriptor < 3 for descriptor in descriptors):
|
|
170
|
+
raise ValueError("private protocol descriptors must be at least 3")
|
|
171
|
+
if len(set(descriptors)) != len(descriptors):
|
|
172
|
+
raise ValueError("private protocol descriptors must be distinct")
|
|
173
|
+
|
|
174
|
+
for descriptor in descriptors:
|
|
175
|
+
os.fstat(descriptor)
|
|
176
|
+
os.set_inheritable(descriptor, False)
|
|
177
|
+
|
|
178
|
+
def close_protocol_descriptors_in_child() -> None:
|
|
179
|
+
for descriptor in descriptors:
|
|
180
|
+
try:
|
|
181
|
+
os.close(descriptor)
|
|
182
|
+
except OSError:
|
|
183
|
+
pass
|
|
184
|
+
|
|
185
|
+
os.register_at_fork(after_in_child=close_protocol_descriptors_in_child)
|
|
186
|
+
return _Bootstrap(
|
|
187
|
+
request=cast(BinaryIO, os.fdopen(descriptors[0], "rb", buffering=0, closefd=False)),
|
|
188
|
+
response=cast(BinaryIO, os.fdopen(descriptors[1], "wb", buffering=0, closefd=False)),
|
|
189
|
+
diagnostic=cast(BinaryIO, os.fdopen(descriptors[2], "wb", buffering=0, closefd=False)),
|
|
190
|
+
)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _write_diagnostic(stream: BinaryIO, message: str) -> None:
|
|
194
|
+
bounded, _ = _truncate_utf8(message, MAX_PROTOCOL_ERROR_MESSAGE_BYTES)
|
|
195
|
+
encoded = f"{bounded}\n".encode("utf-8", errors="replace")
|
|
196
|
+
try:
|
|
197
|
+
stream.write(encoded)
|
|
198
|
+
stream.flush()
|
|
199
|
+
except (BrokenPipeError, OSError):
|
|
200
|
+
pass
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def _write_response(stream: BinaryIO, response: Response) -> None:
|
|
204
|
+
write_frame(stream, encode_response(response))
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _send_protocol_error(
|
|
208
|
+
response_stream: BinaryIO,
|
|
209
|
+
correlation_id: str,
|
|
210
|
+
code: ProtocolErrorCode,
|
|
211
|
+
message: str,
|
|
212
|
+
) -> None:
|
|
213
|
+
bounded_message, _ = _truncate_utf8(message, MAX_PROTOCOL_ERROR_MESSAGE_BYTES)
|
|
214
|
+
_write_response(
|
|
215
|
+
response_stream,
|
|
216
|
+
ProtocolErrorResponse(correlation_id, code, bounded_message or code),
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def _classify_environment(launch_working_directory: str) -> EnvironmentKind:
|
|
221
|
+
if _INITIAL_PREFIX == _INITIAL_BASE_PREFIX:
|
|
222
|
+
return "system"
|
|
223
|
+
project_environment = _JOIN(launch_working_directory, ".venv")
|
|
224
|
+
try:
|
|
225
|
+
if _EXISTS(project_environment) and _SAMEFILE(_INITIAL_PREFIX, project_environment):
|
|
226
|
+
return "project_venv"
|
|
227
|
+
except OSError:
|
|
228
|
+
pass
|
|
229
|
+
return "active_venv"
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _current_working_directory() -> str:
|
|
233
|
+
try:
|
|
234
|
+
return _REALPATH(_GETCWD())
|
|
235
|
+
except OSError as error:
|
|
236
|
+
raise _RuntimeDiagnosticsFailure(
|
|
237
|
+
f"cannot resolve current working directory: {error}"
|
|
238
|
+
) from error
|
|
239
|
+
|
|
240
|
+
|
|
241
|
+
def _initial_runtime_diagnostics(launch_working_directory: str) -> RuntimeDiagnostics:
|
|
242
|
+
return RuntimeDiagnostics(
|
|
243
|
+
executable=_INITIAL_EXECUTABLE,
|
|
244
|
+
python_version=_INITIAL_PYTHON_VERSION,
|
|
245
|
+
working_directory=_current_working_directory(),
|
|
246
|
+
environment_kind=_classify_environment(launch_working_directory),
|
|
247
|
+
implementation=_INITIAL_IMPLEMENTATION,
|
|
248
|
+
platform=_INITIAL_PLATFORM,
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
|
|
252
|
+
def _current_runtime_diagnostics(
|
|
253
|
+
identity: RuntimeDiagnostics,
|
|
254
|
+
package_names: tuple[str, ...],
|
|
255
|
+
diagnostic: BinaryIO,
|
|
256
|
+
) -> RuntimeDiagnostics:
|
|
257
|
+
return RuntimeDiagnostics(
|
|
258
|
+
executable=identity.executable,
|
|
259
|
+
python_version=identity.python_version,
|
|
260
|
+
working_directory=_current_working_directory(),
|
|
261
|
+
environment_kind=identity.environment_kind,
|
|
262
|
+
implementation=identity.implementation,
|
|
263
|
+
platform=identity.platform,
|
|
264
|
+
packages=_inspect_packages(package_names, diagnostic),
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def _inspect_packages(
|
|
269
|
+
package_names: tuple[str, ...], diagnostic: BinaryIO
|
|
270
|
+
) -> tuple[RuntimePackage, ...]:
|
|
271
|
+
packages: list[RuntimePackage] = []
|
|
272
|
+
for name in package_names:
|
|
273
|
+
try:
|
|
274
|
+
version = _DISTRIBUTION_VERSION(name)
|
|
275
|
+
except _PACKAGE_NOT_FOUND_ERROR:
|
|
276
|
+
packages.append(RuntimePackage(name=name, error="not installed"))
|
|
277
|
+
continue
|
|
278
|
+
except Exception as error:
|
|
279
|
+
_write_diagnostic(
|
|
280
|
+
diagnostic,
|
|
281
|
+
f"package metadata unavailable for {name}: {type(error).__name__}: {error}",
|
|
282
|
+
)
|
|
283
|
+
packages.append(RuntimePackage(name=name, error="metadata unavailable"))
|
|
284
|
+
continue
|
|
285
|
+
if not _valid_package_version(version):
|
|
286
|
+
packages.append(RuntimePackage(name=name, error="invalid metadata"))
|
|
287
|
+
continue
|
|
288
|
+
packages.append(RuntimePackage(name=name, version=version))
|
|
289
|
+
return tuple(packages)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def _valid_package_version(value: object) -> bool:
|
|
293
|
+
if type(value) is not str or not value:
|
|
294
|
+
return False
|
|
295
|
+
try:
|
|
296
|
+
size = len(value.encode("utf-8"))
|
|
297
|
+
except UnicodeEncodeError:
|
|
298
|
+
return False
|
|
299
|
+
return size <= MAX_PACKAGE_VERSION_BYTES and not any(
|
|
300
|
+
unicodedata.category(character) in {"Cc", "Cf"} for character in value
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def _validate_initialization(request: InitializeRequest, diagnostics: RuntimeDiagnostics) -> None:
|
|
305
|
+
if diagnostics.working_directory != request.launch_working_directory:
|
|
306
|
+
raise ProtocolViolation(
|
|
307
|
+
"Runner working directory does not match initialize request",
|
|
308
|
+
correlation_id=request.correlation_id,
|
|
309
|
+
)
|
|
310
|
+
if diagnostics.environment_kind != request.environment_kind:
|
|
311
|
+
raise ProtocolViolation(
|
|
312
|
+
"Runner environment kind does not match initialize request",
|
|
313
|
+
correlation_id=request.correlation_id,
|
|
314
|
+
)
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
class _RunnerOutputFailure(BaseException):
|
|
318
|
+
"""An internal output-frame failure that user code must not catch as data validation."""
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
class _StructuredEmitter:
|
|
322
|
+
"""Own execution-scoped helper bounds and ordered private-frame emission."""
|
|
323
|
+
|
|
324
|
+
def __init__(self, response: BinaryIO) -> None:
|
|
325
|
+
self._response = response
|
|
326
|
+
self._correlation_id: str | None = None
|
|
327
|
+
self._execution_count = 0
|
|
328
|
+
self._sequence = 1
|
|
329
|
+
self._json_count = 0
|
|
330
|
+
self._json_bytes = 0
|
|
331
|
+
self._artifact_count = 0
|
|
332
|
+
self._image_count = 0
|
|
333
|
+
self._artifact_bytes = 0
|
|
334
|
+
|
|
335
|
+
def begin(self, correlation_id: str, execution_count: int) -> None:
|
|
336
|
+
self._correlation_id = correlation_id
|
|
337
|
+
self._execution_count = execution_count
|
|
338
|
+
self._sequence = 1
|
|
339
|
+
self._json_count = 0
|
|
340
|
+
self._json_bytes = 0
|
|
341
|
+
self._artifact_count = 0
|
|
342
|
+
self._image_count = 0
|
|
343
|
+
self._artifact_bytes = 0
|
|
344
|
+
|
|
345
|
+
def end(self) -> None:
|
|
346
|
+
self._correlation_id = None
|
|
347
|
+
|
|
348
|
+
def display(self, value: object, *, mime_type: str | None = None) -> None:
|
|
349
|
+
"""Emit one strict JSON value or one fully validated raster image."""
|
|
350
|
+
try:
|
|
351
|
+
if mime_type is None:
|
|
352
|
+
self._display_json(value)
|
|
353
|
+
return
|
|
354
|
+
declared = validate_mime_type(mime_type)
|
|
355
|
+
data = bytes_like(value)
|
|
356
|
+
validate_image(data, declared)
|
|
357
|
+
self._emit_artifact("image", "image", declared, data)
|
|
358
|
+
except (StructuredOutputError, ValueError, ProtocolViolation) as error:
|
|
359
|
+
raise DisplayError(str(error)) from error
|
|
360
|
+
|
|
361
|
+
def artifact(self, name: str, content: object, *, mime_type: str | None = None) -> None:
|
|
362
|
+
"""Emit one bounded text, bytes, or strict-JSON explicit artifact."""
|
|
363
|
+
try:
|
|
364
|
+
logical_name = validate_logical_name(name)
|
|
365
|
+
inferred_mime = mime_type is None
|
|
366
|
+
if isinstance(content, str):
|
|
367
|
+
data = content.encode("utf-8", errors="strict")
|
|
368
|
+
declared = mime_type or "text/plain; charset=utf-8"
|
|
369
|
+
elif isinstance(content, (bytes, bytearray, memoryview)):
|
|
370
|
+
data = bytes_like(content)
|
|
371
|
+
declared = mime_type or "application/octet-stream"
|
|
372
|
+
else:
|
|
373
|
+
value = strict_json(content, MAX_ARTIFACT_BYTES)
|
|
374
|
+
data = json.dumps(
|
|
375
|
+
value,
|
|
376
|
+
ensure_ascii=False,
|
|
377
|
+
allow_nan=False,
|
|
378
|
+
separators=(",", ":"),
|
|
379
|
+
).encode("utf-8")
|
|
380
|
+
declared = mime_type or "application/json"
|
|
381
|
+
declared = validate_mime_type(declared)
|
|
382
|
+
if inferred_mime:
|
|
383
|
+
validate_safe_artifact(data, declared)
|
|
384
|
+
self._emit_artifact("artifact", logical_name, declared, data)
|
|
385
|
+
except (StructuredOutputError, UnicodeError, ValueError, ProtocolViolation) as error:
|
|
386
|
+
raise ArtifactError(str(error)) from error
|
|
387
|
+
|
|
388
|
+
def _display_json(self, value: object) -> None:
|
|
389
|
+
snapshot = strict_json(value, MAX_JSON_DISPLAY_ITEM_BYTES)
|
|
390
|
+
size = json_size(snapshot)
|
|
391
|
+
if self._json_count >= MAX_JSON_DISPLAYS:
|
|
392
|
+
raise StructuredOutputError("JSON display count exceeds maximum")
|
|
393
|
+
if self._json_bytes + size > min(MAX_JSON_DISPLAY_BYTES, _MAX_JSON_WIRE_BYTES):
|
|
394
|
+
raise StructuredOutputError("JSON display bytes exceed terminal result capacity")
|
|
395
|
+
correlation_id = self._require_active()
|
|
396
|
+
previous_mask = _PTHREAD_SIGMASK(signal.SIG_BLOCK, {_SIGINT})
|
|
397
|
+
try:
|
|
398
|
+
self._send(
|
|
399
|
+
DisplayJsonResponse(
|
|
400
|
+
correlation_id,
|
|
401
|
+
self._execution_count,
|
|
402
|
+
self._sequence,
|
|
403
|
+
snapshot,
|
|
404
|
+
)
|
|
405
|
+
)
|
|
406
|
+
self._json_count += 1
|
|
407
|
+
self._json_bytes += size
|
|
408
|
+
self._sequence += 1
|
|
409
|
+
finally:
|
|
410
|
+
_PTHREAD_SIGMASK(signal.SIG_SETMASK, previous_mask)
|
|
411
|
+
|
|
412
|
+
def _emit_artifact(self, kind: str, name: str, declared_mime_type: str, data: bytes) -> None:
|
|
413
|
+
if self._artifact_count >= MAX_ARTIFACT_TRANSACTIONS:
|
|
414
|
+
raise StructuredOutputError("artifact item count exceeds maximum")
|
|
415
|
+
if kind == "image" and self._image_count >= MAX_IMAGE_TRANSACTIONS:
|
|
416
|
+
raise StructuredOutputError("image item count exceeds maximum")
|
|
417
|
+
if len(data) > MAX_ARTIFACT_BYTES or self._artifact_bytes + len(data) > MAX_ARTIFACT_BYTES:
|
|
418
|
+
raise StructuredOutputError("artifact bytes exceed maximum")
|
|
419
|
+
correlation_id = self._require_active()
|
|
420
|
+
transaction_id = uuid.uuid4().hex
|
|
421
|
+
sequence = self._sequence
|
|
422
|
+
previous_mask = _PTHREAD_SIGMASK(signal.SIG_BLOCK, {_SIGINT})
|
|
423
|
+
try:
|
|
424
|
+
self._send(
|
|
425
|
+
ArtifactBeginResponse(
|
|
426
|
+
correlation_id,
|
|
427
|
+
self._execution_count,
|
|
428
|
+
sequence,
|
|
429
|
+
transaction_id,
|
|
430
|
+
cast(ArtifactKind, kind),
|
|
431
|
+
name,
|
|
432
|
+
declared_mime_type,
|
|
433
|
+
len(data),
|
|
434
|
+
)
|
|
435
|
+
)
|
|
436
|
+
digest = hashlib.sha256()
|
|
437
|
+
chunk_count = 0
|
|
438
|
+
for offset in range(0, len(data), MAX_ARTIFACT_CHUNK_BYTES):
|
|
439
|
+
chunk = data[offset : offset + MAX_ARTIFACT_CHUNK_BYTES]
|
|
440
|
+
digest.update(chunk)
|
|
441
|
+
self._send(
|
|
442
|
+
ArtifactChunkResponse(
|
|
443
|
+
correlation_id,
|
|
444
|
+
self._execution_count,
|
|
445
|
+
sequence,
|
|
446
|
+
transaction_id,
|
|
447
|
+
chunk_count,
|
|
448
|
+
chunk,
|
|
449
|
+
)
|
|
450
|
+
)
|
|
451
|
+
chunk_count += 1
|
|
452
|
+
self._send(
|
|
453
|
+
ArtifactEndResponse(
|
|
454
|
+
correlation_id,
|
|
455
|
+
self._execution_count,
|
|
456
|
+
sequence,
|
|
457
|
+
transaction_id,
|
|
458
|
+
chunk_count,
|
|
459
|
+
digest.hexdigest(),
|
|
460
|
+
)
|
|
461
|
+
)
|
|
462
|
+
self._artifact_count += 1
|
|
463
|
+
self._image_count += kind == "image"
|
|
464
|
+
self._artifact_bytes += len(data)
|
|
465
|
+
self._sequence += 1
|
|
466
|
+
finally:
|
|
467
|
+
_PTHREAD_SIGMASK(signal.SIG_SETMASK, previous_mask)
|
|
468
|
+
|
|
469
|
+
def _require_active(self) -> str:
|
|
470
|
+
if self._correlation_id is None:
|
|
471
|
+
raise StructuredOutputError("structured output helper is not executing")
|
|
472
|
+
return self._correlation_id
|
|
473
|
+
|
|
474
|
+
def _send(self, response: Response) -> None:
|
|
475
|
+
try:
|
|
476
|
+
_write_response(self._response, response)
|
|
477
|
+
except BaseException as error:
|
|
478
|
+
raise _RunnerOutputFailure from error
|
|
479
|
+
|
|
480
|
+
|
|
481
|
+
def _namespace(emitter: _StructuredEmitter, workspace: Workspace) -> dict[str, object]:
|
|
482
|
+
read_helper, write_helper, env_helper = workspace.helpers()
|
|
483
|
+
return {
|
|
484
|
+
"__name__": "__main__",
|
|
485
|
+
"__package__": None,
|
|
486
|
+
"__builtins__": builtins,
|
|
487
|
+
"display": emitter.display,
|
|
488
|
+
"artifact": emitter.artifact,
|
|
489
|
+
"read": read_helper,
|
|
490
|
+
"write": write_helper,
|
|
491
|
+
"env": env_helper,
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _compile_and_execute(code: str, execution_count: int, namespace: dict[str, object]) -> object:
|
|
496
|
+
filename = f"<amkernel-cell-{execution_count}>"
|
|
497
|
+
module = ast.parse(code, filename=filename, mode="exec")
|
|
498
|
+
final_expression: ast.expr | None = None
|
|
499
|
+
statements = module.body
|
|
500
|
+
if statements and isinstance(statements[-1], ast.Expr):
|
|
501
|
+
final_expression = statements[-1].value
|
|
502
|
+
statements = statements[:-1]
|
|
503
|
+
|
|
504
|
+
future_flags = 0
|
|
505
|
+
prefix_code: CodeType | None = None
|
|
506
|
+
if statements:
|
|
507
|
+
prefix = ast.Module(body=statements, type_ignores=module.type_ignores)
|
|
508
|
+
ast.fix_missing_locations(prefix)
|
|
509
|
+
prefix_code = compile(prefix, filename, "exec", dont_inherit=True)
|
|
510
|
+
future_flags = prefix_code.co_flags & _FUTURE_FLAGS
|
|
511
|
+
|
|
512
|
+
expression_code: CodeType | None = None
|
|
513
|
+
if final_expression is not None:
|
|
514
|
+
expression = ast.Expression(body=final_expression)
|
|
515
|
+
ast.fix_missing_locations(expression)
|
|
516
|
+
expression_code = compile(
|
|
517
|
+
expression,
|
|
518
|
+
filename,
|
|
519
|
+
"eval",
|
|
520
|
+
flags=future_flags,
|
|
521
|
+
dont_inherit=True,
|
|
522
|
+
)
|
|
523
|
+
|
|
524
|
+
if prefix_code is not None:
|
|
525
|
+
exec(prefix_code, namespace, namespace)
|
|
526
|
+
if expression_code is None:
|
|
527
|
+
return _NO_RESULT
|
|
528
|
+
return eval(expression_code, namespace, namespace)
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
class _NoResult:
|
|
532
|
+
pass
|
|
533
|
+
|
|
534
|
+
|
|
535
|
+
_NO_RESULT = _NoResult()
|
|
536
|
+
|
|
537
|
+
|
|
538
|
+
def _execute_cell(code: str, execution_count: int, namespace: dict[str, object]) -> _CellResult:
|
|
539
|
+
try:
|
|
540
|
+
value = _compile_and_execute(code, execution_count, namespace)
|
|
541
|
+
if value is _NO_RESULT:
|
|
542
|
+
return _CellResult("completed", False, None, None)
|
|
543
|
+
return _CellResult("completed", True, _normalize_result(value), None)
|
|
544
|
+
except _RunnerOutputFailure:
|
|
545
|
+
raise
|
|
546
|
+
except KeyboardInterrupt as error:
|
|
547
|
+
return _CellResult("interrupted", False, None, _error_info(error))
|
|
548
|
+
except BaseException as error:
|
|
549
|
+
return _CellResult("failed", False, None, _error_info(error))
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def _normalize_result(value: object) -> JsonValue:
|
|
553
|
+
candidate = cast(JsonValue, value)
|
|
554
|
+
try:
|
|
555
|
+
encoded_size = json_size(candidate, MAX_RESULT_BYTES)
|
|
556
|
+
except ProtocolViolation:
|
|
557
|
+
return _bounded_repr(value)
|
|
558
|
+
if encoded_size > MAX_RESULT_BYTES:
|
|
559
|
+
return _bounded_repr(value, replaced=True, oversized_json=True)
|
|
560
|
+
try:
|
|
561
|
+
return snapshot_json(candidate, MAX_RESULT_BYTES)
|
|
562
|
+
except ProtocolViolation:
|
|
563
|
+
return _bounded_repr(value, replaced=True)
|
|
564
|
+
|
|
565
|
+
|
|
566
|
+
def _bounded_repr(
|
|
567
|
+
value: object,
|
|
568
|
+
*,
|
|
569
|
+
replaced: bool = False,
|
|
570
|
+
oversized_json: bool = False,
|
|
571
|
+
) -> JsonValue:
|
|
572
|
+
type_name, type_replaced = _type_name(value)
|
|
573
|
+
representation_failed = False
|
|
574
|
+
representation_replaced = replaced
|
|
575
|
+
if oversized_json:
|
|
576
|
+
representation = _oversized_json_summary(value, type_name)
|
|
577
|
+
representation_replaced = True
|
|
578
|
+
elif type(value) in {bytes, bytearray, list, tuple, dict, set, frozenset}:
|
|
579
|
+
representation = _container_summary(value, type_name)
|
|
580
|
+
representation_replaced = True
|
|
581
|
+
elif isinstance(value, (str, bytes, bytearray, list, tuple, dict, set, frozenset)):
|
|
582
|
+
representation = f"<{type_name}; builtin subclass representation omitted>"
|
|
583
|
+
representation_replaced = True
|
|
584
|
+
else:
|
|
585
|
+
try:
|
|
586
|
+
representation = repr(value)
|
|
587
|
+
except BaseException as error:
|
|
588
|
+
representation_failed = True
|
|
589
|
+
error_type, _ = _type_name(error)
|
|
590
|
+
representation = f"<repr failed: {error_type}>"
|
|
591
|
+
|
|
592
|
+
representation, value_truncated = _truncate_utf8(representation, _REPR_BYTES)
|
|
593
|
+
truncated = type_replaced or value_truncated or representation_failed or representation_replaced
|
|
594
|
+
|
|
595
|
+
result: dict[str, JsonValue] = {
|
|
596
|
+
"kind": "repr",
|
|
597
|
+
"typeName": type_name,
|
|
598
|
+
"value": representation,
|
|
599
|
+
"truncated": truncated,
|
|
600
|
+
}
|
|
601
|
+
while json_size(result, _REPR_OBJECT_BYTES) > _REPR_OBJECT_BYTES:
|
|
602
|
+
truncated = True
|
|
603
|
+
if representation:
|
|
604
|
+
representation = representation[: max(0, len(representation) // 2)]
|
|
605
|
+
elif type_name:
|
|
606
|
+
type_name = type_name[: max(0, len(type_name) // 2)]
|
|
607
|
+
else: # pragma: no cover - fixed field overhead is below the limit
|
|
608
|
+
raise ProtocolViolation("cannot bound representation object")
|
|
609
|
+
result = {
|
|
610
|
+
"kind": "repr",
|
|
611
|
+
"typeName": type_name,
|
|
612
|
+
"value": representation,
|
|
613
|
+
"truncated": truncated,
|
|
614
|
+
}
|
|
615
|
+
return result
|
|
616
|
+
|
|
617
|
+
|
|
618
|
+
def _oversized_json_summary(value: object, type_name: str) -> str:
|
|
619
|
+
if type(value) is str:
|
|
620
|
+
prefix, _ = _truncate_utf8(value, _REPR_BYTES // 2)
|
|
621
|
+
return f"{prefix!r} <{len(value)} characters; JSON result exceeds limit>"
|
|
622
|
+
return _container_summary(value, type_name, suffix="JSON result exceeds limit")
|
|
623
|
+
|
|
624
|
+
|
|
625
|
+
def _container_summary(
|
|
626
|
+
value: object,
|
|
627
|
+
type_name: str,
|
|
628
|
+
*,
|
|
629
|
+
suffix: str = "bounded non-JSON representation",
|
|
630
|
+
) -> str:
|
|
631
|
+
if type(value) not in {bytes, bytearray, list, tuple, dict, set, frozenset}:
|
|
632
|
+
return f"<{type_name}; {suffix}>"
|
|
633
|
+
length = len(cast(Sized, value))
|
|
634
|
+
return f"<{type_name} len={length}; {suffix}>"
|
|
635
|
+
|
|
636
|
+
|
|
637
|
+
def _type_name(value: object) -> tuple[str, bool]:
|
|
638
|
+
value_type = type(value)
|
|
639
|
+
try:
|
|
640
|
+
module = type.__getattribute__(value_type, "__module__")
|
|
641
|
+
qualified_name = type.__getattribute__(value_type, "__qualname__")
|
|
642
|
+
except BaseException:
|
|
643
|
+
return "<unknown>", True
|
|
644
|
+
if type(module) is not str or type(qualified_name) is not str or not qualified_name:
|
|
645
|
+
return "<unknown>", True
|
|
646
|
+
name = f"{module}.{qualified_name}" if module and module != "builtins" else qualified_name
|
|
647
|
+
return _truncate_utf8(name, MAX_ERROR_TYPE_BYTES)
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def _error_info(error: BaseException) -> ErrorInfo:
|
|
651
|
+
error_type, _ = _type_name(error)
|
|
652
|
+
message = _bounded_exception_message(error, error_type)
|
|
653
|
+
traceback_lines = _bounded_traceback_lines(error, error_type, message)
|
|
654
|
+
result = ErrorInfo(error_type, message, traceback_lines)
|
|
655
|
+
wire: JsonValue = {
|
|
656
|
+
"type": result.type,
|
|
657
|
+
"message": result.message,
|
|
658
|
+
"traceback": list(result.traceback),
|
|
659
|
+
}
|
|
660
|
+
if json_size(wire, MAX_ERROR_BYTES) > MAX_ERROR_BYTES:
|
|
661
|
+
bounded_message, _ = _truncate_utf8(result.message, 1_024)
|
|
662
|
+
result = ErrorInfo(
|
|
663
|
+
result.type,
|
|
664
|
+
bounded_message,
|
|
665
|
+
("[traceback truncated]",),
|
|
666
|
+
)
|
|
667
|
+
return result
|
|
668
|
+
|
|
669
|
+
|
|
670
|
+
class _SafeExceptionValue(Exception):
|
|
671
|
+
"""Proxy whose string form and SyntaxError fields are already bounded."""
|
|
672
|
+
|
|
673
|
+
msg: str
|
|
674
|
+
filename: str | None
|
|
675
|
+
lineno: int | None
|
|
676
|
+
end_lineno: int | None
|
|
677
|
+
offset: int | None
|
|
678
|
+
end_offset: int | None
|
|
679
|
+
text: str | None
|
|
680
|
+
print_file_and_line: bool | None
|
|
681
|
+
|
|
682
|
+
|
|
683
|
+
class _ExceptionChainTruncated(Exception):
|
|
684
|
+
pass
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
@dataclass(slots=True)
|
|
688
|
+
class _TraceBudget:
|
|
689
|
+
exceptions: int = _MAX_EXCEPTION_CHAIN
|
|
690
|
+
frames: int = _MAX_TRACEBACK_FRAMES
|
|
691
|
+
truncated: bool = False
|
|
692
|
+
|
|
693
|
+
|
|
694
|
+
def _bounded_exception_message(error: BaseException, error_type: str) -> str:
|
|
695
|
+
error_class = type(error)
|
|
696
|
+
try:
|
|
697
|
+
custom_string = type.__getattribute__(error_class, "__str__") is not BaseException.__str__
|
|
698
|
+
class_module = type.__getattribute__(error_class, "__module__")
|
|
699
|
+
except BaseException:
|
|
700
|
+
return f"<message unavailable for {error_type}>"
|
|
701
|
+
if type(class_module) is not str:
|
|
702
|
+
return f"<message unavailable for {error_type}>"
|
|
703
|
+
if custom_string and class_module != "builtins":
|
|
704
|
+
return f"<custom __str__ omitted for {error_type}>"
|
|
705
|
+
|
|
706
|
+
if isinstance(error, SyntaxError):
|
|
707
|
+
try:
|
|
708
|
+
syntax_message = BaseException.__getattribute__(error, "msg")
|
|
709
|
+
except BaseException:
|
|
710
|
+
syntax_message = None
|
|
711
|
+
if type(syntax_message) is str:
|
|
712
|
+
bounded, _ = _truncate_utf8(syntax_message, _ERROR_MESSAGE_BYTES)
|
|
713
|
+
return bounded
|
|
714
|
+
|
|
715
|
+
try:
|
|
716
|
+
arguments = BaseException.__getattribute__(error, "args")
|
|
717
|
+
except BaseException:
|
|
718
|
+
return f"<message unavailable for {error_type}>"
|
|
719
|
+
if type(arguments) is not tuple or not arguments:
|
|
720
|
+
return ""
|
|
721
|
+
if len(arguments) == 1:
|
|
722
|
+
return _bounded_error_argument(
|
|
723
|
+
arguments[0],
|
|
724
|
+
quote_string=type(error) is KeyError,
|
|
725
|
+
)
|
|
726
|
+
|
|
727
|
+
parts: list[str] = []
|
|
728
|
+
remaining = _ERROR_MESSAGE_BYTES - 2
|
|
729
|
+
for argument in arguments:
|
|
730
|
+
part = _bounded_error_argument(argument, quote_string=True)
|
|
731
|
+
separator = 2 if parts else 0
|
|
732
|
+
bounded, truncated = _truncate_utf8(part, max(0, remaining - separator))
|
|
733
|
+
if parts:
|
|
734
|
+
parts.append(", ")
|
|
735
|
+
remaining -= 2
|
|
736
|
+
parts.append(bounded)
|
|
737
|
+
remaining -= len(bounded.encode())
|
|
738
|
+
if truncated or remaining <= 0:
|
|
739
|
+
break
|
|
740
|
+
message, _ = _truncate_utf8("(" + "".join(parts) + ")", _ERROR_MESSAGE_BYTES)
|
|
741
|
+
return message
|
|
742
|
+
|
|
743
|
+
|
|
744
|
+
def _bounded_error_argument(value: object, *, quote_string: bool) -> str:
|
|
745
|
+
if type(value) is str:
|
|
746
|
+
bounded, _ = _truncate_utf8(value, _ERROR_MESSAGE_BYTES - 2)
|
|
747
|
+
return repr(bounded) if quote_string else bounded
|
|
748
|
+
if type(value) is int:
|
|
749
|
+
integer = value
|
|
750
|
+
estimated_digits = (abs(integer).bit_length() * 30_103) // 100_000 + 1
|
|
751
|
+
estimated_digits += integer < 0
|
|
752
|
+
if estimated_digits > _ERROR_MESSAGE_BYTES:
|
|
753
|
+
return f"<int approximately {estimated_digits} digits>"
|
|
754
|
+
try:
|
|
755
|
+
return str(integer)
|
|
756
|
+
except ValueError:
|
|
757
|
+
return "<int exceeds conversion limit>"
|
|
758
|
+
if type(value) in {float, bool, type(None)}:
|
|
759
|
+
return repr(value)
|
|
760
|
+
if type(value) in {bytes, bytearray}:
|
|
761
|
+
return f"<{type(value).__name__} len={len(cast(Sized, value))}>"
|
|
762
|
+
value_type, _ = _type_name(value)
|
|
763
|
+
return f"<{value_type}>"
|
|
764
|
+
|
|
765
|
+
|
|
766
|
+
def _safe_traceback_exception(
|
|
767
|
+
error: BaseException,
|
|
768
|
+
message: str,
|
|
769
|
+
seen: set[int] | None = None,
|
|
770
|
+
budget: _TraceBudget | None = None,
|
|
771
|
+
) -> traceback.TracebackException | None:
|
|
772
|
+
if seen is None:
|
|
773
|
+
seen = set()
|
|
774
|
+
if budget is None:
|
|
775
|
+
budget = _TraceBudget()
|
|
776
|
+
identity = id(error)
|
|
777
|
+
if identity in seen:
|
|
778
|
+
return None
|
|
779
|
+
if budget.exceptions <= 0:
|
|
780
|
+
budget.truncated = True
|
|
781
|
+
return _TRACEBACK_EXCEPTION(
|
|
782
|
+
_ExceptionChainTruncated,
|
|
783
|
+
_SafeExceptionValue("exception chain truncated"),
|
|
784
|
+
None,
|
|
785
|
+
limit=0,
|
|
786
|
+
lookup_lines=False,
|
|
787
|
+
capture_locals=False,
|
|
788
|
+
compact=True,
|
|
789
|
+
)
|
|
790
|
+
seen.add(identity)
|
|
791
|
+
budget.exceptions -= 1
|
|
792
|
+
|
|
793
|
+
error_class = type(error)
|
|
794
|
+
safe_type_name, _ = _type_name(error)
|
|
795
|
+
safe_error_class = _safe_exception_type(error_class, safe_type_name)
|
|
796
|
+
proxy = _SafeExceptionValue(message)
|
|
797
|
+
if issubclass(error_class, SyntaxError):
|
|
798
|
+
_copy_syntax_error_fields(error, proxy, message)
|
|
799
|
+
try:
|
|
800
|
+
original_traceback = cast(
|
|
801
|
+
TracebackType | None,
|
|
802
|
+
BaseException.__getattribute__(error, "__traceback__"),
|
|
803
|
+
)
|
|
804
|
+
except BaseException:
|
|
805
|
+
original_traceback = None
|
|
806
|
+
frame_limit = _consume_traceback_budget(original_traceback, budget)
|
|
807
|
+
rendered = _TRACEBACK_EXCEPTION(
|
|
808
|
+
safe_error_class,
|
|
809
|
+
proxy,
|
|
810
|
+
original_traceback,
|
|
811
|
+
limit=frame_limit,
|
|
812
|
+
lookup_lines=False,
|
|
813
|
+
capture_locals=False,
|
|
814
|
+
compact=True,
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
try:
|
|
818
|
+
cause = BaseException.__getattribute__(error, "__cause__")
|
|
819
|
+
context = BaseException.__getattribute__(error, "__context__")
|
|
820
|
+
suppress_context = bool(BaseException.__getattribute__(error, "__suppress_context__"))
|
|
821
|
+
except BaseException:
|
|
822
|
+
cause = None
|
|
823
|
+
context = None
|
|
824
|
+
suppress_context = False
|
|
825
|
+
if isinstance(cause, BaseException):
|
|
826
|
+
cause_type, _ = _type_name(cause)
|
|
827
|
+
rendered.__cause__ = _safe_traceback_exception(
|
|
828
|
+
cause,
|
|
829
|
+
_bounded_exception_message(cause, cause_type),
|
|
830
|
+
seen,
|
|
831
|
+
budget,
|
|
832
|
+
)
|
|
833
|
+
if isinstance(context, BaseException) and not suppress_context:
|
|
834
|
+
context_type, _ = _type_name(context)
|
|
835
|
+
rendered.__context__ = _safe_traceback_exception(
|
|
836
|
+
context,
|
|
837
|
+
_bounded_exception_message(context, context_type),
|
|
838
|
+
seen,
|
|
839
|
+
budget,
|
|
840
|
+
)
|
|
841
|
+
rendered.__suppress_context__ = suppress_context
|
|
842
|
+
return rendered
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
def _safe_exception_type(
|
|
846
|
+
error_class: type[BaseException],
|
|
847
|
+
safe_type_name: str,
|
|
848
|
+
) -> type[BaseException]:
|
|
849
|
+
base: type[BaseException] = SyntaxError if issubclass(error_class, SyntaxError) else Exception
|
|
850
|
+
try:
|
|
851
|
+
return type(safe_type_name, (base,), {"__module__": "builtins"})
|
|
852
|
+
except BaseException:
|
|
853
|
+
return RuntimeError
|
|
854
|
+
|
|
855
|
+
|
|
856
|
+
def _consume_traceback_budget(
|
|
857
|
+
current: TracebackType | None,
|
|
858
|
+
budget: _TraceBudget,
|
|
859
|
+
) -> int:
|
|
860
|
+
count = 0
|
|
861
|
+
traceback_cursor = current
|
|
862
|
+
while traceback_cursor is not None and count < budget.frames:
|
|
863
|
+
try:
|
|
864
|
+
traceback_cursor = traceback_cursor.tb_next
|
|
865
|
+
except BaseException:
|
|
866
|
+
traceback_cursor = None
|
|
867
|
+
count += 1
|
|
868
|
+
if traceback_cursor is not None:
|
|
869
|
+
budget.truncated = True
|
|
870
|
+
budget.frames -= count
|
|
871
|
+
return count
|
|
872
|
+
|
|
873
|
+
|
|
874
|
+
def _copy_syntax_error_fields(
|
|
875
|
+
error: BaseException,
|
|
876
|
+
proxy: _SafeExceptionValue,
|
|
877
|
+
message: str,
|
|
878
|
+
) -> None:
|
|
879
|
+
proxy.msg = message
|
|
880
|
+
for field in ("filename", "text"):
|
|
881
|
+
try:
|
|
882
|
+
value = BaseException.__getattribute__(error, field)
|
|
883
|
+
except BaseException:
|
|
884
|
+
value = None
|
|
885
|
+
if type(value) is str:
|
|
886
|
+
value, _ = _truncate_utf8(value, 4_096)
|
|
887
|
+
else:
|
|
888
|
+
value = None
|
|
889
|
+
setattr(proxy, field, value)
|
|
890
|
+
for field in ("lineno", "end_lineno", "offset", "end_offset"):
|
|
891
|
+
try:
|
|
892
|
+
value = BaseException.__getattribute__(error, field)
|
|
893
|
+
except BaseException:
|
|
894
|
+
value = None
|
|
895
|
+
if type(value) is not int:
|
|
896
|
+
value = None
|
|
897
|
+
setattr(proxy, field, value)
|
|
898
|
+
try:
|
|
899
|
+
print_file_and_line = BaseException.__getattribute__(error, "print_file_and_line")
|
|
900
|
+
except BaseException:
|
|
901
|
+
print_file_and_line = None
|
|
902
|
+
if type(print_file_and_line) is not bool:
|
|
903
|
+
print_file_and_line = None
|
|
904
|
+
proxy.print_file_and_line = print_file_and_line
|
|
905
|
+
|
|
906
|
+
|
|
907
|
+
def _bounded_traceback_lines(
|
|
908
|
+
error: BaseException,
|
|
909
|
+
error_type: str,
|
|
910
|
+
message: str,
|
|
911
|
+
) -> tuple[str, ...]:
|
|
912
|
+
budget = _TraceBudget()
|
|
913
|
+
try:
|
|
914
|
+
traceback_exception = _safe_traceback_exception(error, message, budget=budget)
|
|
915
|
+
if traceback_exception is None:
|
|
916
|
+
return (f"{error_type}: {message}",)
|
|
917
|
+
rendered = traceback_exception.format(chain=True)
|
|
918
|
+
except BaseException:
|
|
919
|
+
return (f"{error_type}: {message}",)
|
|
920
|
+
|
|
921
|
+
lines: list[str] = []
|
|
922
|
+
remaining = _TRACEBACK_TEXT_BYTES
|
|
923
|
+
was_truncated = False
|
|
924
|
+
try:
|
|
925
|
+
for chunk in rendered:
|
|
926
|
+
bounded, chunk_truncated = _truncate_utf8(chunk, remaining)
|
|
927
|
+
lines.extend(bounded.splitlines())
|
|
928
|
+
remaining -= len(bounded.encode())
|
|
929
|
+
if chunk_truncated or remaining <= 0:
|
|
930
|
+
was_truncated = True
|
|
931
|
+
break
|
|
932
|
+
except BaseException:
|
|
933
|
+
return (f"{error_type}: {message}",)
|
|
934
|
+
|
|
935
|
+
if was_truncated:
|
|
936
|
+
lines.append("[traceback truncated]")
|
|
937
|
+
if not lines:
|
|
938
|
+
lines.append(error_type)
|
|
939
|
+
if budget.truncated and "[traceback truncated]" not in lines:
|
|
940
|
+
lines.append("[traceback truncated]")
|
|
941
|
+
return tuple(lines)
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
def _truncate_utf8(value: str, maximum_bytes: int) -> tuple[str, bool]:
|
|
945
|
+
if maximum_bytes < 0:
|
|
946
|
+
raise ValueError("maximum_bytes must be non-negative")
|
|
947
|
+
characters: list[str] = []
|
|
948
|
+
sizes: list[int] = []
|
|
949
|
+
used = 0
|
|
950
|
+
normalized = False
|
|
951
|
+
for character in value:
|
|
952
|
+
try:
|
|
953
|
+
encoded = character.encode("utf-8")
|
|
954
|
+
normalized_character = character
|
|
955
|
+
except UnicodeEncodeError:
|
|
956
|
+
encoded = "�".encode()
|
|
957
|
+
normalized_character = "�"
|
|
958
|
+
normalized = True
|
|
959
|
+
if used + len(encoded) > maximum_bytes:
|
|
960
|
+
ellipsis = "…" if maximum_bytes >= len("…".encode()) else ""
|
|
961
|
+
content_limit = maximum_bytes - len(ellipsis.encode())
|
|
962
|
+
while sizes and used > content_limit:
|
|
963
|
+
used -= sizes.pop()
|
|
964
|
+
characters.pop()
|
|
965
|
+
return "".join(characters) + ellipsis, True
|
|
966
|
+
characters.append(normalized_character)
|
|
967
|
+
sizes.append(len(encoded))
|
|
968
|
+
used += len(encoded)
|
|
969
|
+
if normalized:
|
|
970
|
+
return "".join(characters), True
|
|
971
|
+
return value, False
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
def _flush_user_streams(diagnostic: BinaryIO) -> None:
|
|
975
|
+
seen: set[int] = set()
|
|
976
|
+
for stream in (sys.stdout, sys.stderr, sys.__stdout__, sys.__stderr__):
|
|
977
|
+
if stream is None or id(stream) in seen:
|
|
978
|
+
continue
|
|
979
|
+
seen.add(id(stream))
|
|
980
|
+
try:
|
|
981
|
+
stream.flush()
|
|
982
|
+
except BaseException as error:
|
|
983
|
+
_write_diagnostic(diagnostic, f"user stream flush failed: {type(error).__name__}")
|
|
984
|
+
|
|
985
|
+
|
|
986
|
+
def _block_sigint_for_finalization(
|
|
987
|
+
signal_state: _SignalState,
|
|
988
|
+
cell_result: _CellResult,
|
|
989
|
+
) -> _CellResult:
|
|
990
|
+
try:
|
|
991
|
+
_PTHREAD_SIGMASK(_SIG_BLOCK, {_SIGINT})
|
|
992
|
+
except KeyboardInterrupt as error:
|
|
993
|
+
signal_state.executing = False
|
|
994
|
+
cell_result = _CellResult("interrupted", False, None, _error_info(error))
|
|
995
|
+
_PTHREAD_SIGMASK(_SIG_BLOCK, {_SIGINT})
|
|
996
|
+
return cell_result
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
def _unblock_sigint_for_execution(baseline_mask: set[int]) -> None:
|
|
1000
|
+
# Darwin can leave an interrupt sent after Started pending across unmask.
|
|
1001
|
+
# Consume a signal already known to be pending, then map it synchronously.
|
|
1002
|
+
if _SIGINT in _PENDING_SIGNALS():
|
|
1003
|
+
_WAIT_FOR_SIGNAL({_SIGINT})
|
|
1004
|
+
_PTHREAD_SIGMASK(_SIG_SETMASK, baseline_mask)
|
|
1005
|
+
raise KeyboardInterrupt
|
|
1006
|
+
_PTHREAD_SIGMASK(_SIG_SETMASK, baseline_mask)
|
|
1007
|
+
|
|
1008
|
+
|
|
1009
|
+
def _execute_request(
|
|
1010
|
+
request: ExecuteRequest,
|
|
1011
|
+
*,
|
|
1012
|
+
response: BinaryIO,
|
|
1013
|
+
diagnostic: BinaryIO,
|
|
1014
|
+
signal_state: _SignalState,
|
|
1015
|
+
baseline_mask: set[int],
|
|
1016
|
+
namespace: dict[str, object],
|
|
1017
|
+
emitter: _StructuredEmitter,
|
|
1018
|
+
execution_count: int,
|
|
1019
|
+
runtime_identity: RuntimeDiagnostics,
|
|
1020
|
+
) -> tuple[int, bool]:
|
|
1021
|
+
next_count = execution_count + 1
|
|
1022
|
+
_PTHREAD_SIGMASK(_SIG_BLOCK, {_SIGINT})
|
|
1023
|
+
_INSTALL_SIGNAL_HANDLER(_SIGINT, signal_state.handle_sigint)
|
|
1024
|
+
signal_state.executing = True
|
|
1025
|
+
_write_response(response, StartedResponse(request.correlation_id, next_count))
|
|
1026
|
+
execution_count = next_count
|
|
1027
|
+
emitter.begin(request.correlation_id, execution_count)
|
|
1028
|
+
|
|
1029
|
+
try:
|
|
1030
|
+
try:
|
|
1031
|
+
_unblock_sigint_for_execution(baseline_mask)
|
|
1032
|
+
cell_result = _execute_cell(request.code, execution_count, namespace)
|
|
1033
|
+
cell_result = _block_sigint_for_finalization(signal_state, cell_result)
|
|
1034
|
+
except KeyboardInterrupt as error:
|
|
1035
|
+
cell_result = _CellResult("interrupted", False, None, _error_info(error))
|
|
1036
|
+
cell_result = _block_sigint_for_finalization(signal_state, cell_result)
|
|
1037
|
+
finally:
|
|
1038
|
+
emitter.end()
|
|
1039
|
+
signal_state.executing = False
|
|
1040
|
+
_INSTALL_SIGNAL_HANDLER(_SIGINT, signal_state.handle_sigint)
|
|
1041
|
+
try:
|
|
1042
|
+
runtime = _current_runtime_diagnostics(runtime_identity, request.packages, diagnostic)
|
|
1043
|
+
except _RuntimeDiagnosticsFailure as error:
|
|
1044
|
+
_flush_user_streams(diagnostic)
|
|
1045
|
+
_send_protocol_error(
|
|
1046
|
+
response,
|
|
1047
|
+
request.correlation_id,
|
|
1048
|
+
"runtime_diagnostics_error",
|
|
1049
|
+
str(error),
|
|
1050
|
+
)
|
|
1051
|
+
_PTHREAD_SIGMASK(_SIG_SETMASK, baseline_mask)
|
|
1052
|
+
return execution_count, False
|
|
1053
|
+
|
|
1054
|
+
_flush_user_streams(diagnostic)
|
|
1055
|
+
_write_response(
|
|
1056
|
+
response,
|
|
1057
|
+
DoneResponse(
|
|
1058
|
+
correlation_id=request.correlation_id,
|
|
1059
|
+
outcome=cell_result.outcome,
|
|
1060
|
+
execution_count=execution_count,
|
|
1061
|
+
runtime=runtime,
|
|
1062
|
+
has_result=cell_result.has_result,
|
|
1063
|
+
result=cell_result.result,
|
|
1064
|
+
error=cell_result.error,
|
|
1065
|
+
),
|
|
1066
|
+
)
|
|
1067
|
+
_PTHREAD_SIGMASK(_SIG_SETMASK, baseline_mask)
|
|
1068
|
+
return execution_count, True
|
|
1069
|
+
|
|
1070
|
+
|
|
1071
|
+
def run_runner(bootstrap: _Bootstrap) -> int:
|
|
1072
|
+
"""Run one initialized persistent namespace until shutdown or fatal protocol error."""
|
|
1073
|
+
signal_state = _SignalState()
|
|
1074
|
+
current_mask = _PTHREAD_SIGMASK(_SIG_BLOCK, set())
|
|
1075
|
+
baseline_mask = set(current_mask) - {_SIGINT}
|
|
1076
|
+
_INSTALL_SIGNAL_HANDLER(_SIGINT, signal_state.handle_sigint)
|
|
1077
|
+
_PTHREAD_SIGMASK(_SIG_SETMASK, baseline_mask)
|
|
1078
|
+
|
|
1079
|
+
try:
|
|
1080
|
+
first_request = read_request(bootstrap.request)
|
|
1081
|
+
except ProtocolViolation as error:
|
|
1082
|
+
_write_diagnostic(bootstrap.diagnostic, f"initialize frame rejected: {error}")
|
|
1083
|
+
if error.correlation_id is not None:
|
|
1084
|
+
_send_protocol_error(
|
|
1085
|
+
bootstrap.response,
|
|
1086
|
+
error.correlation_id,
|
|
1087
|
+
error.code,
|
|
1088
|
+
str(error),
|
|
1089
|
+
)
|
|
1090
|
+
return 2
|
|
1091
|
+
|
|
1092
|
+
if first_request is None:
|
|
1093
|
+
_write_diagnostic(bootstrap.diagnostic, "request EOF before initialize")
|
|
1094
|
+
return 2
|
|
1095
|
+
if not isinstance(first_request, InitializeRequest):
|
|
1096
|
+
_send_protocol_error(
|
|
1097
|
+
bootstrap.response,
|
|
1098
|
+
first_request.correlation_id,
|
|
1099
|
+
"invalid_sequence",
|
|
1100
|
+
"first request must be initialize",
|
|
1101
|
+
)
|
|
1102
|
+
return 2
|
|
1103
|
+
|
|
1104
|
+
launch_working_directory = first_request.launch_working_directory
|
|
1105
|
+
try:
|
|
1106
|
+
runtime_identity = _initial_runtime_diagnostics(launch_working_directory)
|
|
1107
|
+
except _RuntimeDiagnosticsFailure as error:
|
|
1108
|
+
_send_protocol_error(
|
|
1109
|
+
bootstrap.response,
|
|
1110
|
+
first_request.correlation_id,
|
|
1111
|
+
"runtime_diagnostics_error",
|
|
1112
|
+
str(error),
|
|
1113
|
+
)
|
|
1114
|
+
return 2
|
|
1115
|
+
try:
|
|
1116
|
+
_validate_initialization(first_request, runtime_identity)
|
|
1117
|
+
except ProtocolViolation as error:
|
|
1118
|
+
_send_protocol_error(
|
|
1119
|
+
bootstrap.response,
|
|
1120
|
+
first_request.correlation_id,
|
|
1121
|
+
"invalid_request",
|
|
1122
|
+
str(error),
|
|
1123
|
+
)
|
|
1124
|
+
return 2
|
|
1125
|
+
|
|
1126
|
+
try:
|
|
1127
|
+
workspace = Workspace(launch_working_directory)
|
|
1128
|
+
except WorkspaceError as error:
|
|
1129
|
+
_send_protocol_error(
|
|
1130
|
+
bootstrap.response,
|
|
1131
|
+
first_request.correlation_id,
|
|
1132
|
+
"internal_protocol_error",
|
|
1133
|
+
str(error),
|
|
1134
|
+
)
|
|
1135
|
+
return 2
|
|
1136
|
+
|
|
1137
|
+
_write_response(
|
|
1138
|
+
bootstrap.response,
|
|
1139
|
+
ReadyResponse(first_request.correlation_id, runtime_identity),
|
|
1140
|
+
)
|
|
1141
|
+
|
|
1142
|
+
emitter = _StructuredEmitter(bootstrap.response)
|
|
1143
|
+
namespace = _namespace(emitter, workspace)
|
|
1144
|
+
execution_count = 0
|
|
1145
|
+
while True:
|
|
1146
|
+
try:
|
|
1147
|
+
request = read_request(bootstrap.request)
|
|
1148
|
+
except ProtocolViolation as error:
|
|
1149
|
+
_write_diagnostic(bootstrap.diagnostic, f"request frame rejected: {error}")
|
|
1150
|
+
if error.correlation_id is not None:
|
|
1151
|
+
_send_protocol_error(
|
|
1152
|
+
bootstrap.response,
|
|
1153
|
+
error.correlation_id,
|
|
1154
|
+
error.code,
|
|
1155
|
+
str(error),
|
|
1156
|
+
)
|
|
1157
|
+
return 2
|
|
1158
|
+
|
|
1159
|
+
if request is None:
|
|
1160
|
+
_write_diagnostic(bootstrap.diagnostic, "unexpected request EOF")
|
|
1161
|
+
return 2
|
|
1162
|
+
if isinstance(request, ShutdownRequest):
|
|
1163
|
+
return 0
|
|
1164
|
+
if not isinstance(request, ExecuteRequest):
|
|
1165
|
+
_send_protocol_error(
|
|
1166
|
+
bootstrap.response,
|
|
1167
|
+
request.correlation_id,
|
|
1168
|
+
"invalid_sequence",
|
|
1169
|
+
"Runner accepts only execute or shutdown after initialize",
|
|
1170
|
+
)
|
|
1171
|
+
return 2
|
|
1172
|
+
|
|
1173
|
+
try:
|
|
1174
|
+
execution_count, alive = _execute_request(
|
|
1175
|
+
request,
|
|
1176
|
+
response=bootstrap.response,
|
|
1177
|
+
diagnostic=bootstrap.diagnostic,
|
|
1178
|
+
signal_state=signal_state,
|
|
1179
|
+
baseline_mask=baseline_mask,
|
|
1180
|
+
namespace=namespace,
|
|
1181
|
+
execution_count=execution_count,
|
|
1182
|
+
emitter=emitter,
|
|
1183
|
+
runtime_identity=runtime_identity,
|
|
1184
|
+
)
|
|
1185
|
+
except BaseException as error:
|
|
1186
|
+
error_info = _error_info(error)
|
|
1187
|
+
message = f"{error_info.type}: {error_info.message}"
|
|
1188
|
+
try:
|
|
1189
|
+
_send_protocol_error(
|
|
1190
|
+
bootstrap.response,
|
|
1191
|
+
request.correlation_id,
|
|
1192
|
+
"internal_protocol_error",
|
|
1193
|
+
message,
|
|
1194
|
+
)
|
|
1195
|
+
except BaseException as response_error:
|
|
1196
|
+
_write_diagnostic(
|
|
1197
|
+
bootstrap.diagnostic,
|
|
1198
|
+
f"internal protocol response failed: {type(response_error).__name__}",
|
|
1199
|
+
)
|
|
1200
|
+
return 2
|
|
1201
|
+
if not alive:
|
|
1202
|
+
return 2
|
|
1203
|
+
|
|
1204
|
+
|
|
1205
|
+
def main(argv: list[str] | None = None) -> int:
|
|
1206
|
+
"""Parse bootstrap descriptors and run the private Runner process."""
|
|
1207
|
+
try:
|
|
1208
|
+
bootstrap = _bootstrap(argv)
|
|
1209
|
+
except (OSError, ValueError) as error:
|
|
1210
|
+
print(f"Runner bootstrap failed: {error}", file=sys.__stderr__, flush=True)
|
|
1211
|
+
return 2
|
|
1212
|
+
try:
|
|
1213
|
+
return run_runner(bootstrap)
|
|
1214
|
+
except BaseException as error:
|
|
1215
|
+
error_info = _error_info(error)
|
|
1216
|
+
rendered = "\n".join(error_info.traceback)
|
|
1217
|
+
_write_diagnostic(bootstrap.diagnostic, rendered)
|
|
1218
|
+
return 2
|
|
1219
|
+
|
|
1220
|
+
|
|
1221
|
+
if __name__ == "__main__":
|
|
1222
|
+
raise SystemExit(main())
|