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/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())