tywrap 0.5.0 → 0.6.0
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.
- package/dist/cli.js +8 -6
- package/dist/cli.js.map +1 -1
- package/dist/config/index.d.ts.map +1 -1
- package/dist/config/index.js +236 -147
- package/dist/config/index.js.map +1 -1
- package/dist/core/annotation-parser.d.ts.map +1 -1
- package/dist/core/annotation-parser.js +93 -83
- package/dist/core/annotation-parser.js.map +1 -1
- package/dist/core/discovery.d.ts.map +1 -1
- package/dist/core/discovery.js +26 -0
- package/dist/core/discovery.js.map +1 -1
- package/dist/core/emit-call.d.ts +58 -0
- package/dist/core/emit-call.d.ts.map +1 -0
- package/dist/core/emit-call.js +103 -0
- package/dist/core/emit-call.js.map +1 -0
- package/dist/core/generator.d.ts +2 -0
- package/dist/core/generator.d.ts.map +1 -1
- package/dist/core/generator.js +54 -211
- package/dist/core/generator.js.map +1 -1
- package/dist/core/mapper.d.ts.map +1 -1
- package/dist/core/mapper.js +118 -104
- package/dist/core/mapper.js.map +1 -1
- package/dist/index.d.ts +6 -25
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -31
- package/dist/index.js.map +1 -1
- package/dist/runtime/bounded-context.d.ts +2 -2
- package/dist/runtime/bounded-context.js +2 -2
- package/dist/runtime/{safe-codec.d.ts → bridge-codec.d.ts} +11 -11
- package/dist/runtime/bridge-codec.d.ts.map +1 -0
- package/dist/runtime/{safe-codec.js → bridge-codec.js} +26 -26
- package/dist/runtime/bridge-codec.js.map +1 -0
- package/dist/runtime/{http-io.d.ts → http-transport.d.ts} +6 -6
- package/dist/runtime/http-transport.d.ts.map +1 -0
- package/dist/runtime/{http-io.js → http-transport.js} +3 -3
- package/dist/runtime/http-transport.js.map +1 -0
- package/dist/runtime/http.d.ts +3 -3
- package/dist/runtime/http.d.ts.map +1 -1
- package/dist/runtime/http.js +4 -4
- package/dist/runtime/http.js.map +1 -1
- package/dist/runtime/index.d.ts +9 -1
- package/dist/runtime/index.d.ts.map +1 -1
- package/dist/runtime/index.js +9 -1
- package/dist/runtime/index.js.map +1 -1
- package/dist/runtime/node.d.ts +6 -6
- package/dist/runtime/node.d.ts.map +1 -1
- package/dist/runtime/node.js +7 -7
- package/dist/runtime/node.js.map +1 -1
- package/dist/runtime/pooled-transport.d.ts +11 -11
- package/dist/runtime/pooled-transport.d.ts.map +1 -1
- package/dist/runtime/pooled-transport.js +10 -10
- package/dist/runtime/pooled-transport.js.map +1 -1
- package/dist/runtime/pyodide-bootstrap-core.generated.d.ts.map +1 -1
- package/dist/runtime/pyodide-bootstrap-core.generated.js +1 -1
- package/dist/runtime/pyodide-bootstrap-core.generated.js.map +1 -1
- package/dist/runtime/{pyodide-io.d.ts → pyodide-transport.d.ts} +8 -8
- package/dist/runtime/pyodide-transport.d.ts.map +1 -0
- package/dist/runtime/{pyodide-io.js → pyodide-transport.js} +5 -5
- package/dist/runtime/pyodide-transport.js.map +1 -0
- package/dist/runtime/pyodide.d.ts +3 -3
- package/dist/runtime/pyodide.d.ts.map +1 -1
- package/dist/runtime/pyodide.js +5 -5
- package/dist/runtime/pyodide.js.map +1 -1
- package/dist/runtime/rpc-client.d.ts +5 -5
- package/dist/runtime/rpc-client.d.ts.map +1 -1
- package/dist/runtime/rpc-client.js +6 -7
- package/dist/runtime/rpc-client.js.map +1 -1
- package/dist/runtime/{process-io.d.ts → subprocess-transport.d.ts} +9 -9
- package/dist/runtime/subprocess-transport.d.ts.map +1 -0
- package/dist/runtime/{process-io.js → subprocess-transport.js} +6 -6
- package/dist/runtime/subprocess-transport.js.map +1 -0
- package/dist/runtime/{worker-pool.d.ts → transport-pool.d.ts} +15 -15
- package/dist/runtime/transport-pool.d.ts.map +1 -0
- package/dist/runtime/{worker-pool.js → transport-pool.js} +6 -6
- package/dist/runtime/transport-pool.js.map +1 -0
- package/dist/runtime/transport.d.ts +14 -8
- package/dist/runtime/transport.d.ts.map +1 -1
- package/dist/runtime/transport.js +10 -4
- package/dist/runtime/transport.js.map +1 -1
- package/dist/types/index.d.ts +10 -1
- package/dist/types/index.d.ts.map +1 -1
- package/dist/tywrap.d.ts.map +1 -1
- package/dist/tywrap.js +1 -3
- package/dist/tywrap.js.map +1 -1
- package/dist/utils/cache.d.ts +2 -2
- package/dist/utils/cache.d.ts.map +1 -1
- package/dist/utils/cache.js +2 -2
- package/dist/utils/cache.js.map +1 -1
- package/dist/utils/codec.d.ts +1 -1
- package/dist/utils/codec.d.ts.map +1 -1
- package/dist/utils/codec.js +181 -182
- package/dist/utils/codec.js.map +1 -1
- package/dist/version.d.ts +12 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +12 -0
- package/dist/version.js.map +1 -0
- package/package.json +3 -5
- package/runtime/__pycache__/safe_codec.cpython-311.pyc +0 -0
- package/runtime/__pycache__/tywrap_bridge_core.cpython-311.pyc +0 -0
- package/runtime/python_bridge.py +54 -6
- package/runtime/safe_codec.py +8 -8
- package/runtime/tywrap_bridge_core.py +159 -23
- package/src/cli.ts +8 -12
- package/src/config/index.ts +271 -165
- package/src/core/annotation-parser.ts +112 -105
- package/src/core/discovery.ts +32 -0
- package/src/core/emit-call.ts +171 -0
- package/src/core/generator.ts +67 -263
- package/src/core/mapper.ts +125 -110
- package/src/index.ts +11 -68
- package/src/runtime/bounded-context.ts +2 -2
- package/src/runtime/{safe-codec.ts → bridge-codec.ts} +27 -27
- package/src/runtime/{http-io.ts → http-transport.ts} +5 -5
- package/src/runtime/http.ts +5 -5
- package/src/runtime/index.ts +23 -1
- package/src/runtime/node.ts +15 -15
- package/src/runtime/pooled-transport.ts +15 -15
- package/src/runtime/pyodide-bootstrap-core.generated.ts +1 -1
- package/src/runtime/{pyodide-io.ts → pyodide-transport.ts} +7 -7
- package/src/runtime/pyodide.ts +6 -6
- package/src/runtime/rpc-client.ts +12 -8
- package/src/runtime/{process-io.ts → subprocess-transport.ts} +8 -8
- package/src/runtime/{worker-pool.ts → transport-pool.ts} +26 -26
- package/src/runtime/transport.ts +15 -8
- package/src/types/index.ts +10 -1
- package/src/tywrap.ts +1 -3
- package/src/utils/cache.ts +2 -2
- package/src/utils/codec.ts +217 -196
- package/src/version.ts +12 -0
- package/dist/core/analyzer.d.ts +0 -64
- package/dist/core/analyzer.d.ts.map +0 -1
- package/dist/core/analyzer.js +0 -710
- package/dist/core/analyzer.js.map +0 -1
- package/dist/runtime/http-io.d.ts.map +0 -1
- package/dist/runtime/http-io.js.map +0 -1
- package/dist/runtime/optimized-node.d.ts +0 -23
- package/dist/runtime/optimized-node.d.ts.map +0 -1
- package/dist/runtime/optimized-node.js +0 -23
- package/dist/runtime/optimized-node.js.map +0 -1
- package/dist/runtime/process-io.d.ts.map +0 -1
- package/dist/runtime/process-io.js.map +0 -1
- package/dist/runtime/protocol.d.ts +0 -3
- package/dist/runtime/protocol.d.ts.map +0 -1
- package/dist/runtime/protocol.js +0 -3
- package/dist/runtime/protocol.js.map +0 -1
- package/dist/runtime/pyodide-io.d.ts.map +0 -1
- package/dist/runtime/pyodide-io.js.map +0 -1
- package/dist/runtime/safe-codec.d.ts.map +0 -1
- package/dist/runtime/safe-codec.js.map +0 -1
- package/dist/runtime/worker-pool.d.ts.map +0 -1
- package/dist/runtime/worker-pool.js.map +0 -1
- package/dist/utils/bundle-optimizer.d.ts +0 -182
- package/dist/utils/bundle-optimizer.d.ts.map +0 -1
- package/dist/utils/bundle-optimizer.js +0 -680
- package/dist/utils/bundle-optimizer.js.map +0 -1
- package/dist/utils/memory-profiler.d.ts +0 -123
- package/dist/utils/memory-profiler.d.ts.map +0 -1
- package/dist/utils/memory-profiler.js +0 -426
- package/dist/utils/memory-profiler.js.map +0 -1
- package/dist/utils/parallel-processor.d.ts +0 -146
- package/dist/utils/parallel-processor.d.ts.map +0 -1
- package/dist/utils/parallel-processor.js +0 -707
- package/dist/utils/parallel-processor.js.map +0 -1
- package/src/core/analyzer.ts +0 -824
- package/src/runtime/optimized-node.ts +0 -25
- package/src/runtime/protocol.ts +0 -2
- package/src/utils/bundle-optimizer.ts +0 -876
- package/src/utils/memory-profiler.ts +0 -582
- package/src/utils/parallel-processor.ts +0 -955
package/runtime/safe_codec.py
CHANGED
|
@@ -87,7 +87,7 @@ def _has_pydantic_model_dump(obj: Any) -> bool:
|
|
|
87
87
|
return callable(model_dump)
|
|
88
88
|
|
|
89
89
|
|
|
90
|
-
class
|
|
90
|
+
class BridgeCodec:
|
|
91
91
|
"""
|
|
92
92
|
Safe JSON codec with explicit edge case handling.
|
|
93
93
|
|
|
@@ -103,7 +103,7 @@ class SafeCodec:
|
|
|
103
103
|
max_payload_bytes: Maximum payload size in bytes (default 10MB).
|
|
104
104
|
|
|
105
105
|
Example:
|
|
106
|
-
>>> codec =
|
|
106
|
+
>>> codec = BridgeCodec()
|
|
107
107
|
>>> codec.encode({"key": "value"})
|
|
108
108
|
'{"key": "value"}'
|
|
109
109
|
>>> codec.decode('{"key": "value"}')
|
|
@@ -300,19 +300,19 @@ class SafeCodec:
|
|
|
300
300
|
|
|
301
301
|
|
|
302
302
|
# Module-level convenience instance with default settings
|
|
303
|
-
_default_codec: Optional[
|
|
303
|
+
_default_codec: Optional[BridgeCodec] = None
|
|
304
304
|
|
|
305
305
|
|
|
306
|
-
def get_default_codec() ->
|
|
306
|
+
def get_default_codec() -> BridgeCodec:
|
|
307
307
|
"""
|
|
308
|
-
Get or create the default
|
|
308
|
+
Get or create the default BridgeCodec instance.
|
|
309
309
|
|
|
310
310
|
Returns:
|
|
311
|
-
The default
|
|
311
|
+
The default BridgeCodec instance with standard settings.
|
|
312
312
|
"""
|
|
313
313
|
global _default_codec
|
|
314
314
|
if _default_codec is None:
|
|
315
|
-
_default_codec =
|
|
315
|
+
_default_codec = BridgeCodec()
|
|
316
316
|
return _default_codec
|
|
317
317
|
|
|
318
318
|
|
|
@@ -331,7 +331,7 @@ def encode(value: Any, *, allow_nan: bool = False) -> str:
|
|
|
331
331
|
CodecError: If encoding fails.
|
|
332
332
|
"""
|
|
333
333
|
if allow_nan:
|
|
334
|
-
codec =
|
|
334
|
+
codec = BridgeCodec(allow_nan=True)
|
|
335
335
|
return codec.encode(value)
|
|
336
336
|
return get_default_codec().encode(value)
|
|
337
337
|
|
|
@@ -6,10 +6,10 @@ protocol. It is imported by:
|
|
|
6
6
|
|
|
7
7
|
- runtime/python_bridge.py (the Node/Bun/Deno subprocess server and the HTTP
|
|
8
8
|
server), which owns I/O concerns: the stdin/stdout JSONL loop, env-var size
|
|
9
|
-
guards, the real OS pid, bridge='python-subprocess', and the final
|
|
9
|
+
guards, the real OS pid, bridge='python-subprocess', and the final BridgeCodec
|
|
10
10
|
encode wrapper.
|
|
11
11
|
|
|
12
|
-
- the in-WASM Pyodide server (src/runtime/pyodide-
|
|
12
|
+
- the in-WASM Pyodide server (src/runtime/pyodide-transport.ts). Pyodide cannot read
|
|
13
13
|
this file from disk, so it is shipped as a build-time-generated TypeScript
|
|
14
14
|
string constant (src/runtime/pyodide-bootstrap-core.generated.ts) produced by
|
|
15
15
|
scripts/generate-pyodide-bootstrap.mjs and exec'd into a module registered in
|
|
@@ -17,7 +17,7 @@ protocol. It is imported by:
|
|
|
17
17
|
asserts the generated constant stays byte-identical to this file.
|
|
18
18
|
|
|
19
19
|
CROSS-LANGUAGE CONTRACT (Python <-> the TypeScript decoder in src/utils/codec.ts
|
|
20
|
-
and the request encoder in src/runtime/
|
|
20
|
+
and the request encoder in src/runtime/bridge-codec.ts):
|
|
21
21
|
|
|
22
22
|
* Every value-type "marker" envelope carries {'__tywrap__': <type>,
|
|
23
23
|
'codecVersion': 1, 'encoding': ...}. The 6 markers are: ndarray, dataframe,
|
|
@@ -66,6 +66,123 @@ class InstanceHandleError(ValueError):
|
|
|
66
66
|
"""Raised when an instance handle is unknown or no longer valid."""
|
|
67
67
|
|
|
68
68
|
|
|
69
|
+
class ImportNotAllowedError(PermissionError):
|
|
70
|
+
"""Raised when a requested module import is not on the active allowlist."""
|
|
71
|
+
|
|
72
|
+
def __init__(self, module_name):
|
|
73
|
+
super().__init__(
|
|
74
|
+
f'Import of module {module_name!r} is not permitted by the tywrap bridge '
|
|
75
|
+
'allowlist; add it to TYWRAP_ALLOWED_MODULES (subprocess) or the '
|
|
76
|
+
'allowed_modules parameter to enable it'
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class AttributeNotAllowedError(PermissionError):
|
|
81
|
+
"""Raised when access to a private/dunder attribute is denied by policy."""
|
|
82
|
+
|
|
83
|
+
def __init__(self, attr_name):
|
|
84
|
+
super().__init__(
|
|
85
|
+
f'Access to attribute {attr_name!r} is not permitted by the tywrap bridge: '
|
|
86
|
+
'underscore-prefixed (private/dunder) attributes are blocked to prevent '
|
|
87
|
+
'sandbox-escape via attributes like __globals__/__subclasses__/__builtins__; '
|
|
88
|
+
'set TYWRAP_ALLOW_PRIVATE_ATTRS=1 (subprocess) or pass allow_private_attrs=True '
|
|
89
|
+
'to override'
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# =============================================================================
|
|
94
|
+
# IMPORT / ATTRIBUTE ALLOWLIST (trust boundary enforcement)
|
|
95
|
+
# =============================================================================
|
|
96
|
+
#
|
|
97
|
+
# The bridge dispatches call/instantiate/call_method by importing the requested
|
|
98
|
+
# module and getattr-ing the requested function/class/method. That is an
|
|
99
|
+
# arbitrary import+getattr+call surface, so two complementary guards live here.
|
|
100
|
+
# Both are PURE (no env reads) so the rules behave identically under the
|
|
101
|
+
# subprocess server and the in-WASM Pyodide server; the subprocess server derives
|
|
102
|
+
# the parameters from env vars (TYWRAP_ALLOWED_MODULES / TYWRAP_ALLOW_PRIVATE_ATTRS)
|
|
103
|
+
# and threads them in, exactly like force_json_markers / torch_allow_copy.
|
|
104
|
+
#
|
|
105
|
+
# 1. MODULE ALLOWLIST (opt-in, default = allow all):
|
|
106
|
+
# allowed_modules=None means "no restriction" so existing configurations keep
|
|
107
|
+
# working unchanged. When a caller supplies a set, only those modules (plus the
|
|
108
|
+
# stdlib the bridge itself needs to serialize results, see _BRIDGE_REQUIRED_MODULES)
|
|
109
|
+
# may be imported; submodules of an allowed module are permitted (e.g. allowing
|
|
110
|
+
# 'scipy' also allows 'scipy.sparse'). A non-allowlisted import fails LOUDLY with
|
|
111
|
+
# ImportNotAllowedError rather than silently importing.
|
|
112
|
+
#
|
|
113
|
+
# 2. PRIVATE-ATTRIBUTE BLOCK (default ON):
|
|
114
|
+
# getattr of any name starting with '_' (single-underscore private OR dunder) is
|
|
115
|
+
# rejected. This blocks the classic escape chain (obj.__class__.__subclasses__()
|
|
116
|
+
# /__globals__/__builtins__/__import__) without depending on the module allowlist.
|
|
117
|
+
# tywrap-generated wrappers never reference underscore-prefixed names (the IR
|
|
118
|
+
# analyzer skips them), so this does not regress generated code. Set
|
|
119
|
+
# allow_private_attrs=True to restore unrestricted getattr for trusted callers.
|
|
120
|
+
|
|
121
|
+
# Stdlib modules the bridge's own serialization/handlers may need to import even
|
|
122
|
+
# when a caller-supplied allowlist is active. Optional codec deps (numpy, pandas,
|
|
123
|
+
# scipy, torch, sklearn, pyarrow) are intentionally NOT here: if a caller restricts
|
|
124
|
+
# modules, they must opt those in explicitly. These names cover only what the
|
|
125
|
+
# bridge core itself imports.
|
|
126
|
+
_BRIDGE_REQUIRED_MODULES = frozenset(
|
|
127
|
+
{
|
|
128
|
+
'base64',
|
|
129
|
+
'datetime',
|
|
130
|
+
'decimal',
|
|
131
|
+
'importlib',
|
|
132
|
+
'json',
|
|
133
|
+
'math',
|
|
134
|
+
'sys',
|
|
135
|
+
'traceback',
|
|
136
|
+
'uuid',
|
|
137
|
+
'pathlib',
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _top_level_package(module_name):
|
|
143
|
+
"""Return the top-level package of a dotted module name ('a.b.c' -> 'a')."""
|
|
144
|
+
return module_name.split('.', 1)[0]
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _is_module_allowed(module_name, allowed_modules):
|
|
148
|
+
"""
|
|
149
|
+
Return True when module_name may be imported under the active policy.
|
|
150
|
+
|
|
151
|
+
allowed_modules=None disables enforcement (allow all). Otherwise a module is
|
|
152
|
+
allowed when it (or its top-level package) is explicitly listed, or it is one
|
|
153
|
+
of the stdlib modules the bridge itself requires.
|
|
154
|
+
"""
|
|
155
|
+
if allowed_modules is None:
|
|
156
|
+
return True
|
|
157
|
+
if module_name in allowed_modules or module_name in _BRIDGE_REQUIRED_MODULES:
|
|
158
|
+
return True
|
|
159
|
+
top = _top_level_package(module_name)
|
|
160
|
+
return top in allowed_modules or top in _BRIDGE_REQUIRED_MODULES
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def import_allowed_module(module_name, allowed_modules):
|
|
164
|
+
"""
|
|
165
|
+
Import module_name only if permitted by the allowlist, else raise loudly.
|
|
166
|
+
|
|
167
|
+
This is the single chokepoint every handler routes module imports through.
|
|
168
|
+
"""
|
|
169
|
+
if not _is_module_allowed(module_name, allowed_modules):
|
|
170
|
+
raise ImportNotAllowedError(module_name)
|
|
171
|
+
return importlib.import_module(module_name)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def get_allowed_attr(obj, attr_name, *, allow_private_attrs):
|
|
175
|
+
"""
|
|
176
|
+
getattr(obj, attr_name) with the private/dunder block applied.
|
|
177
|
+
|
|
178
|
+
Rejects any underscore-prefixed name unless allow_private_attrs is True. This
|
|
179
|
+
is the single chokepoint every handler routes attribute access through.
|
|
180
|
+
"""
|
|
181
|
+
if not allow_private_attrs and attr_name.startswith('_'):
|
|
182
|
+
raise AttributeNotAllowedError(attr_name)
|
|
183
|
+
return getattr(obj, attr_name)
|
|
184
|
+
|
|
185
|
+
|
|
69
186
|
class CodecError(Exception):
|
|
70
187
|
"""Raised when value encoding fails (e.g. NaN/Infinity not allowed)."""
|
|
71
188
|
|
|
@@ -85,10 +202,10 @@ def _deserialize_bytes_envelope(value):
|
|
|
85
202
|
Decode base64-encoded bytes envelopes from JS into Python bytes.
|
|
86
203
|
|
|
87
204
|
Supported shapes:
|
|
88
|
-
- { "__tywrap_bytes__": true, "b64": "..." } (JS
|
|
205
|
+
- { "__tywrap_bytes__": true, "b64": "..." } (JS BridgeCodec.encodeRequest)
|
|
89
206
|
- { "__type__": "bytes", "encoding": "base64", "data": "..." } (legacy/compat)
|
|
90
207
|
|
|
91
|
-
Why: TS
|
|
208
|
+
Why: TS BridgeCodec encodes Uint8Array/ArrayBuffer as base64 objects, but
|
|
92
209
|
Python handlers expect real bytes/bytearray to preserve behavior (e.g., len()).
|
|
93
210
|
"""
|
|
94
211
|
if not isinstance(value, dict):
|
|
@@ -530,7 +647,7 @@ def serialize(obj, *, force_json_markers, torch_allow_copy=False):
|
|
|
530
647
|
"""
|
|
531
648
|
Top-level result serializer. Dispatch order is significant: numpy ndarray ->
|
|
532
649
|
dataframe -> series -> scipy.sparse -> torch -> sklearn -> Pydantic -> stdlib
|
|
533
|
-
-> passthrough. The remaining
|
|
650
|
+
-> passthrough. The remaining BridgeCodec value behaviors (numpy/pandas scalars,
|
|
534
651
|
bytes, sets, complex rejection, NaN/Infinity) are applied later during JSON
|
|
535
652
|
encoding by default_encoder.
|
|
536
653
|
"""
|
|
@@ -558,12 +675,12 @@ def serialize(obj, *, force_json_markers, torch_allow_copy=False):
|
|
|
558
675
|
|
|
559
676
|
|
|
560
677
|
# =============================================================================
|
|
561
|
-
# JSON ENCODE:
|
|
678
|
+
# JSON ENCODE: BridgeCodec-equivalent value handling (NaN reject, scalars, bytes)
|
|
562
679
|
# =============================================================================
|
|
563
680
|
#
|
|
564
|
-
# This mirrors
|
|
681
|
+
# This mirrors BridgeCodec._default_encoder (runtime/safe_codec.py) for the VALUE
|
|
565
682
|
# behaviors that are part of the wire contract. The subprocess server still uses
|
|
566
|
-
# the real
|
|
683
|
+
# the real BridgeCodec for its final encode (it also enforces size limits); this
|
|
567
684
|
# core encoder exists so the Pyodide server gets identical value handling without
|
|
568
685
|
# depending on safe_codec.py. The conformance suite asserts these behaviors match.
|
|
569
686
|
|
|
@@ -594,7 +711,7 @@ def _is_pandas_scalar(obj):
|
|
|
594
711
|
|
|
595
712
|
def make_default_encoder(*, allow_nan):
|
|
596
713
|
"""
|
|
597
|
-
Build a json.dumps default= encoder matching
|
|
714
|
+
Build a json.dumps default= encoder matching BridgeCodec's value handling.
|
|
598
715
|
|
|
599
716
|
Raises CodecError for NaN/Infinity extracted from numpy scalars (json.dumps
|
|
600
717
|
itself rejects top-level/nested NaN/Infinity floats when allow_nan=False).
|
|
@@ -663,11 +780,11 @@ def make_default_encoder(*, allow_nan):
|
|
|
663
780
|
|
|
664
781
|
def encode_value(value, *, allow_nan):
|
|
665
782
|
"""
|
|
666
|
-
JSON-encode a fully-serialized response value, applying the
|
|
783
|
+
JSON-encode a fully-serialized response value, applying the BridgeCodec-equivalent
|
|
667
784
|
default encoder and rejecting NaN/Infinity when allow_nan is False.
|
|
668
785
|
|
|
669
786
|
Raises CodecError (wrapping the json.dumps ValueError) on NaN/Infinity, matching
|
|
670
|
-
|
|
787
|
+
BridgeCodec's "Cannot serialize NaN..." wording so error parity holds.
|
|
671
788
|
"""
|
|
672
789
|
try:
|
|
673
790
|
return json.dumps(value, default=make_default_encoder(allow_nan=allow_nan), allow_nan=allow_nan)
|
|
@@ -729,31 +846,31 @@ def coerce_dict(value, key):
|
|
|
729
846
|
return value
|
|
730
847
|
|
|
731
848
|
|
|
732
|
-
def handle_call(params, *, force_json_markers, torch_allow_copy):
|
|
849
|
+
def handle_call(params, *, force_json_markers, torch_allow_copy, allowed_modules, allow_private_attrs):
|
|
733
850
|
module_name = require_str(params, 'module')
|
|
734
851
|
function_name = require_str(params, 'functionName')
|
|
735
852
|
args = deserialize(coerce_list(params.get('args'), 'args'))
|
|
736
853
|
kwargs = deserialize(coerce_dict(params.get('kwargs'), 'kwargs'))
|
|
737
|
-
mod =
|
|
738
|
-
func =
|
|
854
|
+
mod = import_allowed_module(module_name, allowed_modules)
|
|
855
|
+
func = get_allowed_attr(mod, function_name, allow_private_attrs=allow_private_attrs)
|
|
739
856
|
res = func(*args, **kwargs)
|
|
740
857
|
return serialize(res, force_json_markers=force_json_markers, torch_allow_copy=torch_allow_copy)
|
|
741
858
|
|
|
742
859
|
|
|
743
|
-
def handle_instantiate(params, instances):
|
|
860
|
+
def handle_instantiate(params, instances, *, allowed_modules, allow_private_attrs):
|
|
744
861
|
module_name = require_str(params, 'module')
|
|
745
862
|
class_name = require_str(params, 'className')
|
|
746
863
|
args = deserialize(coerce_list(params.get('args'), 'args'))
|
|
747
864
|
kwargs = deserialize(coerce_dict(params.get('kwargs'), 'kwargs'))
|
|
748
|
-
mod =
|
|
749
|
-
cls =
|
|
865
|
+
mod = import_allowed_module(module_name, allowed_modules)
|
|
866
|
+
cls = get_allowed_attr(mod, class_name, allow_private_attrs=allow_private_attrs)
|
|
750
867
|
obj = cls(*args, **kwargs)
|
|
751
868
|
handle_id = str(id(obj))
|
|
752
869
|
instances[handle_id] = obj
|
|
753
870
|
return handle_id
|
|
754
871
|
|
|
755
872
|
|
|
756
|
-
def handle_call_method(params, instances, *, force_json_markers, torch_allow_copy):
|
|
873
|
+
def handle_call_method(params, instances, *, force_json_markers, torch_allow_copy, allow_private_attrs):
|
|
757
874
|
handle_id = require_str(params, 'handle')
|
|
758
875
|
method_name = require_str(params, 'methodName')
|
|
759
876
|
args = deserialize(coerce_list(params.get('args'), 'args'))
|
|
@@ -761,7 +878,7 @@ def handle_call_method(params, instances, *, force_json_markers, torch_allow_cop
|
|
|
761
878
|
if handle_id not in instances:
|
|
762
879
|
raise InstanceHandleError(f'Unknown instance handle: {handle_id}')
|
|
763
880
|
obj = instances[handle_id]
|
|
764
|
-
func =
|
|
881
|
+
func = get_allowed_attr(obj, method_name, allow_private_attrs=allow_private_attrs)
|
|
765
882
|
res = func(*args, **kwargs)
|
|
766
883
|
return serialize(res, force_json_markers=force_json_markers, torch_allow_copy=torch_allow_copy)
|
|
767
884
|
|
|
@@ -815,6 +932,8 @@ def dispatch_request(
|
|
|
815
932
|
python_version=None,
|
|
816
933
|
torch_allow_copy=False,
|
|
817
934
|
arrow_available_override=None,
|
|
935
|
+
allowed_modules=None,
|
|
936
|
+
allow_private_attrs=False,
|
|
818
937
|
):
|
|
819
938
|
"""
|
|
820
939
|
Validate and route a request, returning the fully-serialized response dict
|
|
@@ -824,6 +943,13 @@ def dispatch_request(
|
|
|
824
943
|
|
|
825
944
|
allow_nan is accepted for signature symmetry; NaN rejection happens during
|
|
826
945
|
the final encode_value() call, which the caller performs.
|
|
946
|
+
|
|
947
|
+
allowed_modules: None (default) disables the import allowlist so existing
|
|
948
|
+
behavior is preserved. Supplying a set restricts call/instantiate imports to
|
|
949
|
+
those modules (plus the stdlib the bridge itself needs) and raises
|
|
950
|
+
ImportNotAllowedError otherwise. allow_private_attrs=False (default) blocks
|
|
951
|
+
getattr of underscore-prefixed names; True restores unrestricted access. See
|
|
952
|
+
the IMPORT / ATTRIBUTE ALLOWLIST section above for the full trust model.
|
|
827
953
|
"""
|
|
828
954
|
mid = require_protocol(msg)
|
|
829
955
|
method = msg.get('method')
|
|
@@ -832,13 +958,23 @@ def dispatch_request(
|
|
|
832
958
|
params = coerce_dict(msg.get('params'), 'params')
|
|
833
959
|
if method == 'call':
|
|
834
960
|
result = handle_call(
|
|
835
|
-
params,
|
|
961
|
+
params,
|
|
962
|
+
force_json_markers=force_json_markers,
|
|
963
|
+
torch_allow_copy=torch_allow_copy,
|
|
964
|
+
allowed_modules=allowed_modules,
|
|
965
|
+
allow_private_attrs=allow_private_attrs,
|
|
836
966
|
)
|
|
837
967
|
elif method == 'instantiate':
|
|
838
|
-
result = handle_instantiate(
|
|
968
|
+
result = handle_instantiate(
|
|
969
|
+
params, instances, allowed_modules=allowed_modules, allow_private_attrs=allow_private_attrs
|
|
970
|
+
)
|
|
839
971
|
elif method == 'call_method':
|
|
840
972
|
result = handle_call_method(
|
|
841
|
-
params,
|
|
973
|
+
params,
|
|
974
|
+
instances,
|
|
975
|
+
force_json_markers=force_json_markers,
|
|
976
|
+
torch_allow_copy=torch_allow_copy,
|
|
977
|
+
allow_private_attrs=allow_private_attrs,
|
|
842
978
|
)
|
|
843
979
|
elif method == 'dispose_instance':
|
|
844
980
|
result = handle_dispose_instance(params, instances)
|
package/src/cli.ts
CHANGED
|
@@ -46,23 +46,22 @@ function parseModules(modulesList?: string): string[] {
|
|
|
46
46
|
.filter(Boolean);
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
-
function buildModulesConfig(
|
|
50
|
-
|
|
51
|
-
runtime
|
|
52
|
-
|
|
53
|
-
return Object.fromEntries(modules.map(name => [name, {
|
|
49
|
+
function buildModulesConfig(modules: string[]): TywrapOptions['pythonModules'] {
|
|
50
|
+
// The per-module `runtime` field is dead input (never read during generation);
|
|
51
|
+
// the active runtime is resolved from the top-level `runtime` config. Seed only
|
|
52
|
+
// the fields that actually affect generation.
|
|
53
|
+
return Object.fromEntries(modules.map(name => [name, { typeHints: 'strict' }]));
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
function renderConfigTemplate(options: {
|
|
57
57
|
format: 'ts' | 'json';
|
|
58
58
|
modules: string[];
|
|
59
|
-
runtime: RuntimeStrategy;
|
|
60
59
|
outputDir: string;
|
|
61
60
|
}): string {
|
|
62
61
|
const modules = options.modules.length > 0 ? options.modules : ['math'];
|
|
63
62
|
if (options.format === 'json') {
|
|
64
63
|
const config = {
|
|
65
|
-
pythonModules: buildModulesConfig(modules
|
|
64
|
+
pythonModules: buildModulesConfig(modules),
|
|
66
65
|
output: { dir: options.outputDir, format: 'esm', declaration: false, sourceMap: false },
|
|
67
66
|
runtime: { node: { pythonPath: 'python3' } },
|
|
68
67
|
types: { presets: ['stdlib'] },
|
|
@@ -71,9 +70,7 @@ function renderConfigTemplate(options: {
|
|
|
71
70
|
}
|
|
72
71
|
|
|
73
72
|
const moduleLines = modules
|
|
74
|
-
.map(
|
|
75
|
-
name => ` ${JSON.stringify(name)}: { runtime: '${options.runtime}', typeHints: 'strict' },`
|
|
76
|
-
)
|
|
73
|
+
.map(name => ` ${JSON.stringify(name)}: { typeHints: 'strict' },`)
|
|
77
74
|
.join('\n');
|
|
78
75
|
return `import { defineConfig } from 'tywrap';
|
|
79
76
|
|
|
@@ -230,7 +227,7 @@ async function main(): Promise<void> {
|
|
|
230
227
|
|
|
231
228
|
const overrides: NonNullable<ResolveConfigOptions['overrides']> = {};
|
|
232
229
|
if (modules.length > 0) {
|
|
233
|
-
overrides.pythonModules = buildModulesConfig(modules
|
|
230
|
+
overrides.pythonModules = buildModulesConfig(modules);
|
|
234
231
|
}
|
|
235
232
|
|
|
236
233
|
if (
|
|
@@ -456,7 +453,6 @@ async function main(): Promise<void> {
|
|
|
456
453
|
const content = renderConfigTemplate({
|
|
457
454
|
format: argv.format,
|
|
458
455
|
modules,
|
|
459
|
-
runtime: argv.runtime,
|
|
460
456
|
outputDir: argv.outputDir,
|
|
461
457
|
});
|
|
462
458
|
|