@code-yeongyu/senpi-codemode 2026.10.27-1 → 2026.10.30-1
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/CHANGELOG.md +46 -0
- package/package.json +4 -4
- package/src/bridge/http-server.ts +18 -5
- package/src/bridge/permission-check.ts +76 -0
- package/src/bridge/protocol.ts +7 -0
- package/src/kernels/jl/kernel.ts +3 -0
- package/src/kernels/js/context-manager.ts +26 -8
- package/src/kernels/js/inline-worker-entry.js +1 -23
- package/src/kernels/js/inline-worker.ts +7 -2
- package/src/kernels/js/interpose-bun.js +220 -0
- package/src/kernels/js/interpose-capabilities.js +29 -0
- package/src/kernels/js/interpose-code.js +8 -0
- package/src/kernels/js/interpose-fs.js +124 -0
- package/src/kernels/js/interpose-modules.js +107 -0
- package/src/kernels/js/interpose-paths.js +80 -0
- package/src/kernels/js/interpose-process.js +97 -0
- package/src/kernels/js/interpose-resources.js +59 -0
- package/src/kernels/js/interpose-sqlite.js +104 -0
- package/src/kernels/js/interpose.js +86 -0
- package/src/kernels/js/kernel-contract.ts +31 -0
- package/src/kernels/js/kernel-tools-pump.js +4 -2
- package/src/kernels/js/local-module-loader.ts +9 -4
- package/src/kernels/js/permission-authority.d.mts +10 -0
- package/src/kernels/js/permission-authority.mjs +11 -0
- package/src/kernels/js/permission-core.md +114 -0
- package/src/kernels/js/permission-host.ts +388 -0
- package/src/kernels/js/permission-runtime.js +205 -0
- package/src/kernels/js/permission-sessions.ts +46 -0
- package/src/kernels/js/permission-slots.d.mts +51 -0
- package/src/kernels/js/permission-slots.mjs +133 -0
- package/src/kernels/js/permission-worker-entry.js +1 -0
- package/src/kernels/js/permission-worker-frames.ts +55 -0
- package/src/kernels/js/process-tree.js +9 -1
- package/src/kernels/js/rewrite-imports.ts +44 -0
- package/src/kernels/js/windows-tree-kill.js +10 -2
- package/src/kernels/js/worker-core.d.ts +2 -0
- package/src/kernels/js/worker-core.js +5 -2
- package/src/kernels/js/worker-cwd.js +14 -8
- package/src/kernels/js/worker-entry.js +1 -23
- package/src/kernels/js/worker-file-helpers.js +58 -0
- package/src/kernels/js/worker-host.ts +24 -6
- package/src/kernels/js/worker-package-resolve.js +10 -2
- package/src/kernels/js/worker-runtime.js +5 -65
- package/src/kernels/js/worker-slot.ts +5 -1
- package/src/kernels/js/worker-startup.ts +7 -5
- package/src/kernels/js/worker-webview.js +17 -16
- package/src/kernels/py/interpose.py +765 -0
- package/src/kernels/py/kernel-contract.ts +4 -0
- package/src/kernels/py/kernel-tools-host.ts +5 -1
- package/src/kernels/py/kernel.ts +45 -5
- package/src/kernels/py/path_identity.py +162 -0
- package/src/kernels/py/path_identity_posix.py +74 -0
- package/src/kernels/py/path_identity_windows.py +93 -0
- package/src/kernels/py/permission-core.md +103 -0
- package/src/kernels/py/permission-host.ts +198 -0
- package/src/kernels/py/permission_bridge.py +145 -0
- package/src/kernels/py/prelude.py +40 -5
- package/src/kernels/py/transport.ts +15 -8
- package/src/kernels/rb/kernel.ts +3 -0
- package/src/kernels/shared/invocation-authority.ts +59 -0
- package/src/kernels/shared/kernel-tools-pump.ts +22 -1
- package/src/kernels/shared/path-identity.d.mts +42 -0
- package/src/kernels/shared/path-identity.mjs +149 -0
- package/src/kernels/shared/subprocess-contract.ts +2 -0
- package/src/kernels/shared/subprocess-kernel.ts +28 -0
|
@@ -0,0 +1,765 @@
|
|
|
1
|
+
"""Install Python API interposition before the first user statement.
|
|
2
|
+
|
|
3
|
+
Originals and grants live in this initializer's closure, never in an importable
|
|
4
|
+
registry. This enforces ordinary APIs, not adversarial Python introspection.
|
|
5
|
+
"""
|
|
6
|
+
import builtins
|
|
7
|
+
import _imp
|
|
8
|
+
from collections.abc import MutableMapping, MutableSequence
|
|
9
|
+
import contextvars
|
|
10
|
+
import contextlib
|
|
11
|
+
import glob
|
|
12
|
+
import importlib
|
|
13
|
+
import importlib.machinery
|
|
14
|
+
import importlib.resources
|
|
15
|
+
import importlib.util
|
|
16
|
+
import io
|
|
17
|
+
import _io
|
|
18
|
+
import os
|
|
19
|
+
import pathlib
|
|
20
|
+
import pkgutil
|
|
21
|
+
import shlex
|
|
22
|
+
import shutil
|
|
23
|
+
import sqlite3
|
|
24
|
+
import subprocess
|
|
25
|
+
import sys
|
|
26
|
+
import types
|
|
27
|
+
import urllib.parse
|
|
28
|
+
import weakref
|
|
29
|
+
import zipfile
|
|
30
|
+
from typing import Callable
|
|
31
|
+
|
|
32
|
+
__builtins__ = vars(builtins).copy()
|
|
33
|
+
|
|
34
|
+
pty = importlib.import_module("pty") if os.name == "posix" else None
|
|
35
|
+
|
|
36
|
+
_identity_path = os.path.join(os.path.dirname(__file__), "path_identity.py")
|
|
37
|
+
_identity_namespace = {"__builtins__": __builtins__, "__file__": _identity_path}
|
|
38
|
+
with open(_identity_path, "rb") as _identity_source:
|
|
39
|
+
exec(compile(_identity_source.read(), _identity_path, "exec"), _identity_namespace)
|
|
40
|
+
create_identity = _identity_namespace["create_identity"]
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def install(check, enforcing, refuse, runtime):
|
|
44
|
+
identity = create_identity(runtime)
|
|
45
|
+
# Private dependency references are captured before cell code can rebind modules.
|
|
46
|
+
fsdecode, fspath, PathLike, getcwd = os.fsdecode, os.fspath, os.PathLike, os.getcwd
|
|
47
|
+
join_path, dirname, basename, isabs = os.path.join, os.path.dirname, os.path.basename, os.path.isabs
|
|
48
|
+
encode_utf8 = str.encode
|
|
49
|
+
O_WRONLY, O_RDWR, O_CREAT, O_TRUNC = os.O_WRONLY, os.O_RDWR, os.O_CREAT, os.O_TRUNC
|
|
50
|
+
platform_name, environment_get = os.name, os.environ.get
|
|
51
|
+
quote = shlex.quote
|
|
52
|
+
def join_command(args):
|
|
53
|
+
return " ".join(quote(arg) for arg in args)
|
|
54
|
+
getframe = sys._getframe
|
|
55
|
+
resolve_name = importlib.util.resolve_name
|
|
56
|
+
reference, UnsupportedOperation = weakref.ref, io.UnsupportedOperation
|
|
57
|
+
urlsplit, parse_qs, unquote = urllib.parse.urlsplit, urllib.parse.parse_qs, urllib.parse.unquote
|
|
58
|
+
DatabaseError = sqlite3.DatabaseError
|
|
59
|
+
SQLITE_ATTACH, SQLITE_DENY, SQLITE_OK = sqlite3.SQLITE_ATTACH, sqlite3.SQLITE_DENY, sqlite3.SQLITE_OK
|
|
60
|
+
native = importlib.import_module(os.name)
|
|
61
|
+
os_originals = dict(vars(os))
|
|
62
|
+
module_getattribute = types.ModuleType.__getattribute__
|
|
63
|
+
mapping_proxy = types.MappingProxyType
|
|
64
|
+
# Python helpers also resolve globals in their defining module. Freeze those
|
|
65
|
+
# dictionaries, including os.path's fspath dependency, not just their names.
|
|
66
|
+
function_type = types.FunctionType
|
|
67
|
+
path_os = types.SimpleNamespace(fspath=fspath)
|
|
68
|
+
def capture(function, **dependencies):
|
|
69
|
+
function = getattr(function, "__wrapped__", function)
|
|
70
|
+
captured = function_type(function.__code__, {**function.__globals__, "__builtins__": __builtins__, **dependencies},
|
|
71
|
+
function.__name__, function.__defaults__, function.__closure__)
|
|
72
|
+
captured.__kwdefaults__ = function.__kwdefaults__
|
|
73
|
+
return captured
|
|
74
|
+
fsdecode = capture(fsdecode, fspath=fspath)
|
|
75
|
+
join_path = capture(join_path, os=path_os)
|
|
76
|
+
dirname = capture(dirname, os=path_os)
|
|
77
|
+
basename = capture(basename, os=path_os)
|
|
78
|
+
isabs = capture(isabs, os=path_os)
|
|
79
|
+
quote = capture(quote)
|
|
80
|
+
resolve_name = capture(resolve_name)
|
|
81
|
+
urlsplit, parse_qs, unquote = capture(urlsplit), capture(parse_qs), capture(unquote)
|
|
82
|
+
grants = {}
|
|
83
|
+
modules = sys.modules
|
|
84
|
+
# The interpreter's own registries, never replaced: CPython's C-level import
|
|
85
|
+
# (PyImport_Import) reads the interpreter's module dict rather than the
|
|
86
|
+
# sys.modules attribute, so any substitute silently diverges from it and
|
|
87
|
+
# modules first imported afterwards become invisible to C importers.
|
|
88
|
+
# Captured here, before anything can reach a decision point, so every
|
|
89
|
+
# enforced decision can re-verify them.
|
|
90
|
+
meta_path, path_hooks = sys.meta_path, sys.path_hooks
|
|
91
|
+
registry_entries = tuple(modules.items())
|
|
92
|
+
meta_entries, path_entries = tuple(meta_path), tuple(path_hooks)
|
|
93
|
+
internal = contextvars.ContextVar("senpi_operation_internal", default=False)
|
|
94
|
+
engine_modules = frozenset(id(module) for module in sys.modules.values())
|
|
95
|
+
import_codes = frozenset(
|
|
96
|
+
function.__code__
|
|
97
|
+
for module in (sys.modules["importlib._bootstrap"], sys.modules["importlib._bootstrap_external"])
|
|
98
|
+
for function in vars(module).values() if isinstance(function, types.FunctionType)
|
|
99
|
+
)
|
|
100
|
+
original_import = builtins.__import__
|
|
101
|
+
original_import_module = importlib.import_module
|
|
102
|
+
original_close = os.close
|
|
103
|
+
original_open = builtins.open
|
|
104
|
+
|
|
105
|
+
def shielding():
|
|
106
|
+
return False if internal.get() else enforcing()
|
|
107
|
+
|
|
108
|
+
def intact(registry, entries):
|
|
109
|
+
return len(registry) == len(entries) and all(item is entry for item, entry in zip(registry, entries))
|
|
110
|
+
|
|
111
|
+
def active():
|
|
112
|
+
# Each enforced decision re-verifies the registries: they must still be
|
|
113
|
+
# the interpreter's own objects holding their captured entries. Cells
|
|
114
|
+
# reach them only through the views below, which refuse; this also
|
|
115
|
+
# refuses the decision when a reference obtained past those views (a
|
|
116
|
+
# base descriptor such as types.ModuleType.__getattribute__) was used to
|
|
117
|
+
# tamper with one, so no checked operation proceeds on a changed registry.
|
|
118
|
+
if not shielding():
|
|
119
|
+
return False
|
|
120
|
+
if (module_getattribute(sys, "modules") is not modules
|
|
121
|
+
or module_getattribute(sys, "meta_path") is not meta_path
|
|
122
|
+
or module_getattribute(sys, "path_hooks") is not path_hooks
|
|
123
|
+
or not all(modules.get(name) is module for name, module in registry_entries)
|
|
124
|
+
or not intact(meta_path, meta_entries) or not intact(path_hooks, path_entries)):
|
|
125
|
+
refuse("import registry mutation")
|
|
126
|
+
return True
|
|
127
|
+
|
|
128
|
+
def path_input(requested):
|
|
129
|
+
# The written spelling travels byte-for-byte and the executed spelling is the string the OS
|
|
130
|
+
# receives: the cwd-joined form of a relative path, never lexically collapsed.
|
|
131
|
+
# The captured native canonicalizer supplies the same versioned identity as JS.
|
|
132
|
+
try:
|
|
133
|
+
encode_utf8(requested, "utf-8")
|
|
134
|
+
except UnicodeEncodeError:
|
|
135
|
+
refuse("operation cannot be checked")
|
|
136
|
+
executed = requested if isabs(requested) else join_path(getcwd(), requested)
|
|
137
|
+
return {"path": requested, "executedPath": executed, **identity(executed)}
|
|
138
|
+
|
|
139
|
+
def path_call(tool, path, *, dir_fd=None):
|
|
140
|
+
if isinstance(path, int):
|
|
141
|
+
if tool == "ls":
|
|
142
|
+
resource(path, "read")
|
|
143
|
+
check("ls", path_input(grants[path][0]))
|
|
144
|
+
else:
|
|
145
|
+
resource(path, tool)
|
|
146
|
+
return
|
|
147
|
+
requested = fsdecode(fspath(path))
|
|
148
|
+
if dir_fd is not None:
|
|
149
|
+
# Relative dir-fd resolution cannot be attributed without a recorded directory.
|
|
150
|
+
resource(dir_fd, "read")
|
|
151
|
+
requested = join_path(grants[dir_fd][0], requested)
|
|
152
|
+
check(tool, path_input(requested))
|
|
153
|
+
|
|
154
|
+
def resource(fd, mode):
|
|
155
|
+
if not active():
|
|
156
|
+
return
|
|
157
|
+
grant = grants.get(fd)
|
|
158
|
+
handle = grant[2]() if grant is not None and len(grant) > 2 else None
|
|
159
|
+
live = grant is not None and (len(grant) == 2 or (handle is not None and not handle.closed))
|
|
160
|
+
if grant is None or not live or mode not in grant[1]:
|
|
161
|
+
refuse(f"ungranted descriptor {fd} for {mode}")
|
|
162
|
+
|
|
163
|
+
def modes(mode):
|
|
164
|
+
if isinstance(mode, int):
|
|
165
|
+
access = mode & (O_WRONLY | O_RDWR)
|
|
166
|
+
return ("read", "write") if access == O_RDWR else (("write",) if access == O_WRONLY else ("read",))
|
|
167
|
+
return ("read", "write") if "+" in mode else (("read",) if mode.startswith("r") else ("write",))
|
|
168
|
+
|
|
169
|
+
def opening(path, mode, kwargs):
|
|
170
|
+
allowed = modes(mode)
|
|
171
|
+
if active():
|
|
172
|
+
required = allowed
|
|
173
|
+
if isinstance(mode, int) and mode & (O_CREAT | O_TRUNC) and "write" not in required:
|
|
174
|
+
required = (*required, "write")
|
|
175
|
+
for operation in required:
|
|
176
|
+
path_call(operation, path, dir_fd=kwargs.get("dir_fd"))
|
|
177
|
+
return allowed
|
|
178
|
+
|
|
179
|
+
class File:
|
|
180
|
+
"""Retains the file interface while checking retained handles after revocation."""
|
|
181
|
+
def __init__(self, value, allowed):
|
|
182
|
+
self._value = value
|
|
183
|
+
self._allowed = allowed
|
|
184
|
+
try:
|
|
185
|
+
self._fd = value.fileno()
|
|
186
|
+
except UnsupportedOperation:
|
|
187
|
+
self._fd = None
|
|
188
|
+
def _grant(self, mode):
|
|
189
|
+
if active():
|
|
190
|
+
if mode not in self._allowed:
|
|
191
|
+
refuse(f"ungranted stream mode {mode}")
|
|
192
|
+
if self._fd is not None:
|
|
193
|
+
resource(self._fd, mode)
|
|
194
|
+
def __getattr__(self, name):
|
|
195
|
+
value = getattr(self._value, name)
|
|
196
|
+
if name in ("read", "read1", "readall", "readinto", "readinto1", "readline", "readlines", "peek",
|
|
197
|
+
"write", "writelines", "truncate"):
|
|
198
|
+
def operation(*args, **kwargs):
|
|
199
|
+
mode = "read" if name.startswith("read") or name == "peek" else "write"
|
|
200
|
+
self._grant(mode)
|
|
201
|
+
return value(*args, **kwargs)
|
|
202
|
+
return operation
|
|
203
|
+
return value
|
|
204
|
+
def __enter__(self):
|
|
205
|
+
self._value.__enter__()
|
|
206
|
+
return self
|
|
207
|
+
def __exit__(self, *args):
|
|
208
|
+
was_open = not self._value.closed
|
|
209
|
+
try:
|
|
210
|
+
return self._value.__exit__(*args)
|
|
211
|
+
finally:
|
|
212
|
+
if was_open:
|
|
213
|
+
grants.pop(self._fd, None)
|
|
214
|
+
def __iter__(self):
|
|
215
|
+
return self
|
|
216
|
+
def __next__(self):
|
|
217
|
+
self._grant("read")
|
|
218
|
+
return next(self._value)
|
|
219
|
+
def close(self):
|
|
220
|
+
if not self._value.closed:
|
|
221
|
+
grants.pop(self._fd, None)
|
|
222
|
+
return self._value.close()
|
|
223
|
+
|
|
224
|
+
def wrap_open(original):
|
|
225
|
+
def opened(file, mode="r", *args, **kwargs):
|
|
226
|
+
if not active() and not internal.get():
|
|
227
|
+
return original(file, mode, *args, **kwargs)
|
|
228
|
+
allowed = opening(file, mode, kwargs)
|
|
229
|
+
value = original(file, mode, *args, **kwargs)
|
|
230
|
+
fd = value.fileno()
|
|
231
|
+
grants[fd] = (fsdecode(fspath(file)) if not isinstance(file, int) else grants.get(file, ("", ()))[0],
|
|
232
|
+
allowed, reference(value))
|
|
233
|
+
# Zero-deny sessions receive the exact original object.
|
|
234
|
+
if not active():
|
|
235
|
+
return value
|
|
236
|
+
wrapped = File(value, allowed)
|
|
237
|
+
return wrapped
|
|
238
|
+
return opened
|
|
239
|
+
|
|
240
|
+
builtins.open = wrap_open(original_open)
|
|
241
|
+
io.open = wrap_open(io.open)
|
|
242
|
+
# Remain subclassable without putting the unguarded constructor in our MRO.
|
|
243
|
+
original_fileio = io.FileIO
|
|
244
|
+
class FileIO(File, io.RawIOBase):
|
|
245
|
+
def __init__(self, file, mode="r", closefd=True, opener=None):
|
|
246
|
+
allowed = opening(file, mode, {"opener": opener})
|
|
247
|
+
value = original_fileio(file, mode, closefd, opener)
|
|
248
|
+
File.__init__(self, value, allowed)
|
|
249
|
+
grants[value.fileno()] = (fsdecode(fspath(file)) if not isinstance(file, int) else "", allowed,
|
|
250
|
+
reference(value))
|
|
251
|
+
@property
|
|
252
|
+
def closed(self):
|
|
253
|
+
return self._value.closed
|
|
254
|
+
for name in ("read", "readall", "readinto", "readline", "readlines", "write", "writelines", "truncate",
|
|
255
|
+
"seek", "tell", "fileno", "flush", "isatty", "readable", "writable", "seekable"):
|
|
256
|
+
def fileio_method(name):
|
|
257
|
+
def wrapped(self, *args, **kwargs):
|
|
258
|
+
return File.__getattr__(self, name)(*args, **kwargs)
|
|
259
|
+
return wrapped
|
|
260
|
+
setattr(FileIO, name, fileio_method(name))
|
|
261
|
+
io.FileIO = _io.FileIO = FileIO
|
|
262
|
+
_io.open = builtins.open
|
|
263
|
+
|
|
264
|
+
original_os_open = os.open
|
|
265
|
+
def os_open(path, flags, mode=0o777, *, dir_fd=None):
|
|
266
|
+
if not active() and not internal.get():
|
|
267
|
+
return original_os_open(path, flags, mode, dir_fd=dir_fd)
|
|
268
|
+
allowed = opening(path, flags, {"dir_fd": dir_fd})
|
|
269
|
+
fd = original_os_open(path, flags, mode, dir_fd=dir_fd)
|
|
270
|
+
grants[fd] = (fsdecode(fspath(path)), allowed)
|
|
271
|
+
return fd
|
|
272
|
+
os.open = os_open
|
|
273
|
+
def close(fd):
|
|
274
|
+
grants.pop(fd, None)
|
|
275
|
+
return original_close(fd)
|
|
276
|
+
os.close = close
|
|
277
|
+
for name, grant in (("read", "read"), ("write", "write"), ("readv", "read"), ("writev", "write"),
|
|
278
|
+
("pread", "read"), ("pwrite", "write"), ("ftruncate", "write")):
|
|
279
|
+
if not hasattr(os, name):
|
|
280
|
+
continue
|
|
281
|
+
def fd_method(original, grant):
|
|
282
|
+
def wrapped(fd, *args, **kwargs):
|
|
283
|
+
resource(fd, grant)
|
|
284
|
+
return original(fd, *args, **kwargs)
|
|
285
|
+
return wrapped
|
|
286
|
+
setattr(os, name, fd_method(getattr(os, name), grant))
|
|
287
|
+
original_fdopen = os.fdopen
|
|
288
|
+
def fdopen(fd, *args, **kwargs):
|
|
289
|
+
mode = args[0] if args else kwargs.get("mode", "r")
|
|
290
|
+
for grant in modes(mode):
|
|
291
|
+
resource(fd, grant)
|
|
292
|
+
return original_fdopen(fd, *args, **kwargs)
|
|
293
|
+
os.fdopen = fdopen
|
|
294
|
+
|
|
295
|
+
def wrap_paths(owner, name, operations):
|
|
296
|
+
original = getattr(owner, name)
|
|
297
|
+
def wrapped(*args, **kwargs):
|
|
298
|
+
if active():
|
|
299
|
+
for index, key, tool in operations:
|
|
300
|
+
path = args[index] if len(args) > index else kwargs[key]
|
|
301
|
+
path_call(tool, path, dir_fd=kwargs.get("src_dir_fd" if index == 0 and len(operations) > 1
|
|
302
|
+
else "dst_dir_fd" if index == 1 else "dir_fd"))
|
|
303
|
+
return original(*args, **kwargs)
|
|
304
|
+
setattr(owner, name, wrapped)
|
|
305
|
+
|
|
306
|
+
for name in ("remove", "unlink", "rmdir", "mkdir", "makedirs", "chmod", "truncate",
|
|
307
|
+
"mkfifo", "mknod", "utime", "chown", "lchown", "lchmod", "chflags", "lchflags",
|
|
308
|
+
"setxattr", "removexattr"):
|
|
309
|
+
if hasattr(os, name):
|
|
310
|
+
wrap_paths(os, name, [(0, "path", "write")])
|
|
311
|
+
for name in ("rename", "replace", "link"):
|
|
312
|
+
wrap_paths(os, name, [(0, "src", "write"), (1, "dst", "write")])
|
|
313
|
+
wrap_paths(os, "symlink", [(1, "dst", "write")])
|
|
314
|
+
for name in ("listdir", "scandir", "walk"):
|
|
315
|
+
original = getattr(os, name)
|
|
316
|
+
def listing(original):
|
|
317
|
+
def wrapped(path=".", *args, **kwargs):
|
|
318
|
+
if active():
|
|
319
|
+
path_call("ls", path)
|
|
320
|
+
return original(path, *args, **kwargs)
|
|
321
|
+
return wrapped
|
|
322
|
+
setattr(os, name, listing(original))
|
|
323
|
+
for name in ("copyfile", "copy", "copy2", "copymode", "copystat", "copytree"):
|
|
324
|
+
wrap_paths(shutil, name, [(0, "src", "read"), (1, "dst", "write")])
|
|
325
|
+
wrap_paths(shutil, "move", [(0, "src", "write"), (1, "dst", "write")])
|
|
326
|
+
wrap_paths(shutil, "rmtree", [(0, "path", "write")])
|
|
327
|
+
for name in ("glob", "iglob"):
|
|
328
|
+
original = getattr(glob, name)
|
|
329
|
+
def globbing(original):
|
|
330
|
+
def wrapped(pathname, *args, **kwargs):
|
|
331
|
+
if active():
|
|
332
|
+
prefix = pathname
|
|
333
|
+
for wildcard in ("*", "?", "["):
|
|
334
|
+
prefix = prefix.split(wildcard, 1)[0]
|
|
335
|
+
root = dirname(prefix) or "."
|
|
336
|
+
if kwargs.get("root_dir"):
|
|
337
|
+
root = join_path(fspath(kwargs["root_dir"]), root)
|
|
338
|
+
path_call("ls", root, dir_fd=kwargs.get("dir_fd"))
|
|
339
|
+
return original(pathname, *args, **kwargs)
|
|
340
|
+
return wrapped
|
|
341
|
+
setattr(glob, name, globbing(original))
|
|
342
|
+
for name in ("iterdir", "glob", "rglob"):
|
|
343
|
+
wrap_paths(pathlib.Path, name, [(0, "self", "ls")])
|
|
344
|
+
wrap_paths(pathlib.Path, "touch", [(0, "self", "write")])
|
|
345
|
+
# pathlib delegates its remaining APIs to the patched io/os functions.
|
|
346
|
+
|
|
347
|
+
def shell_command(args, shell=False, executable=None):
|
|
348
|
+
if shell:
|
|
349
|
+
selected_shell = executable or (environment_get("COMSPEC", "cmd.exe") if platform_name == "nt" else "/bin/sh")
|
|
350
|
+
if basename(fsdecode(selected_shell)) not in ("sh", "bash", "zsh", "dash"):
|
|
351
|
+
refuse("unsupported shell")
|
|
352
|
+
# POSIX Popen passes sequence[0] as the -c program, the rest as shell arguments.
|
|
353
|
+
return fsdecode(args if isinstance(args, (str, bytes)) else args[0])
|
|
354
|
+
argv = [args] if isinstance(args, (str, bytes, PathLike)) else list(args)
|
|
355
|
+
if executable is not None:
|
|
356
|
+
argv[0] = executable
|
|
357
|
+
return join_command([fsdecode(fspath(arg)) for arg in argv])
|
|
358
|
+
|
|
359
|
+
def perform(original, args, kwargs):
|
|
360
|
+
# Trusted implementation internals (fork/exec in os.spawn, pty, Popen) have already
|
|
361
|
+
# been authorized. No user callback may run inside this region.
|
|
362
|
+
scope = internal.set(True)
|
|
363
|
+
try:
|
|
364
|
+
return original(*args, **kwargs)
|
|
365
|
+
finally:
|
|
366
|
+
internal.reset(scope)
|
|
367
|
+
|
|
368
|
+
def user_callback(callback, pipe_reader=False):
|
|
369
|
+
def wrapped(*args, **kwargs):
|
|
370
|
+
scope = internal.set(False)
|
|
371
|
+
fd = args[0] if pipe_reader else None
|
|
372
|
+
prior = grants.get(fd)
|
|
373
|
+
if pipe_reader:
|
|
374
|
+
grants[fd] = ("<pty>", ("read",))
|
|
375
|
+
try:
|
|
376
|
+
return callback(*args, **kwargs)
|
|
377
|
+
finally:
|
|
378
|
+
if pipe_reader:
|
|
379
|
+
if prior is None:
|
|
380
|
+
grants.pop(fd, None)
|
|
381
|
+
else:
|
|
382
|
+
grants[fd] = prior
|
|
383
|
+
internal.reset(scope)
|
|
384
|
+
return wrapped
|
|
385
|
+
|
|
386
|
+
original_popen_init = subprocess.Popen.__init__
|
|
387
|
+
popen_signature = __import__("inspect").signature(original_popen_init)
|
|
388
|
+
def popen_init(self, *args, **kwargs):
|
|
389
|
+
if not active():
|
|
390
|
+
return original_popen_init(self, *args, **kwargs)
|
|
391
|
+
binding = popen_signature.bind(self, *args, **kwargs)
|
|
392
|
+
bound = binding.arguments
|
|
393
|
+
if bound.get("preexec_fn") is not None:
|
|
394
|
+
bound["preexec_fn"] = user_callback(bound["preexec_fn"])
|
|
395
|
+
check("bash", {"command": shell_command(bound["args"], bound.get("shell", False), bound.get("executable"))},
|
|
396
|
+
fsdecode(bound["cwd"]) if bound.get("cwd") is not None else None)
|
|
397
|
+
return perform(original_popen_init, binding.args, binding.kwargs)
|
|
398
|
+
subprocess.Popen.__init__ = popen_init
|
|
399
|
+
for name in ("system", "popen"):
|
|
400
|
+
original = getattr(os, name)
|
|
401
|
+
def shell(original):
|
|
402
|
+
def wrapped(command, *args, **kwargs):
|
|
403
|
+
if not active():
|
|
404
|
+
return original(command, *args, **kwargs)
|
|
405
|
+
if platform_name == "nt":
|
|
406
|
+
refuse("unsupported shell")
|
|
407
|
+
check("bash", {"command": fsdecode(command)})
|
|
408
|
+
return perform(original, (command, *args), kwargs)
|
|
409
|
+
return wrapped
|
|
410
|
+
setattr(os, name, shell(original))
|
|
411
|
+
for name in dir(os):
|
|
412
|
+
if not (name.startswith("exec") or name.startswith("spawn") or name.startswith("posix_spawn")):
|
|
413
|
+
continue
|
|
414
|
+
original = getattr(os, name)
|
|
415
|
+
if not callable(original):
|
|
416
|
+
continue
|
|
417
|
+
def spawning(original, name):
|
|
418
|
+
def wrapped(*args, **kwargs):
|
|
419
|
+
if not active():
|
|
420
|
+
return original(*args, **kwargs)
|
|
421
|
+
offset = 1 if name.startswith("spawn") else 0
|
|
422
|
+
executable = args[offset]
|
|
423
|
+
rest = args[offset + 1:]
|
|
424
|
+
argv = rest[0] if name.startswith("posix_spawn") or name[offset + 4:offset + 5] == "v" else None
|
|
425
|
+
if name.startswith("exec"):
|
|
426
|
+
argv = rest[0] if name[4] == "v" else rest[:-1] if name.endswith("e") else rest
|
|
427
|
+
elif name.startswith("spawn"):
|
|
428
|
+
argv = rest[0] if name[5] == "v" else rest[:-1] if name.endswith("e") else rest
|
|
429
|
+
check("bash", {"command": shell_command(argv, executable=executable)})
|
|
430
|
+
return perform(original, args, kwargs)
|
|
431
|
+
return wrapped
|
|
432
|
+
setattr(os, name, spawning(original, name))
|
|
433
|
+
if pty is not None:
|
|
434
|
+
original_pty_spawn = pty.spawn
|
|
435
|
+
def pty_spawn(argv, *args, **kwargs):
|
|
436
|
+
if not active():
|
|
437
|
+
return original_pty_spawn(argv, *args, **kwargs)
|
|
438
|
+
args = tuple(user_callback(callback, True) for callback in args)
|
|
439
|
+
kwargs = {name: user_callback(callback, True) for name, callback in kwargs.items()}
|
|
440
|
+
check("bash", {"command": shell_command(argv)})
|
|
441
|
+
return perform(original_pty_spawn, (argv, *args), kwargs)
|
|
442
|
+
setattr(pty, "spawn", pty_spawn)
|
|
443
|
+
|
|
444
|
+
def forbidden(original, capability):
|
|
445
|
+
def wrapped(*args, **kwargs):
|
|
446
|
+
if active():
|
|
447
|
+
refuse(capability)
|
|
448
|
+
return original(*args, **kwargs)
|
|
449
|
+
return wrapped
|
|
450
|
+
for name in ("fork", "forkpty"):
|
|
451
|
+
if hasattr(os, name):
|
|
452
|
+
setattr(os, name, forbidden(getattr(os, name), f"os.{name}"))
|
|
453
|
+
importlib.reload = forbidden(importlib.reload, "importlib.reload")
|
|
454
|
+
importlib.machinery.ExtensionFileLoader.create_module = forbidden(
|
|
455
|
+
importlib.machinery.ExtensionFileLoader.create_module, "extension-module loading")
|
|
456
|
+
importlib.machinery.ExtensionFileLoader.exec_module = forbidden(
|
|
457
|
+
importlib.machinery.ExtensionFileLoader.exec_module, "extension-module loading")
|
|
458
|
+
for name in ("create_dynamic", "exec_dynamic", "create_builtin", "exec_builtin"):
|
|
459
|
+
setattr(_imp, name, forbidden(getattr(_imp, name), "extension-module loading"))
|
|
460
|
+
for name in ("create_module", "exec_module"):
|
|
461
|
+
setattr(importlib.machinery.BuiltinImporter, name, staticmethod(forbidden(
|
|
462
|
+
getattr(importlib.machinery.BuiltinImporter, name), "extension-module loading")))
|
|
463
|
+
# os re-exports native builtins; rebinding only os leaves public posix/nt aliases.
|
|
464
|
+
for name, original in os_originals.items():
|
|
465
|
+
wrapped = getattr(os, name)
|
|
466
|
+
if wrapped is not original and getattr(native, name, None) is original:
|
|
467
|
+
setattr(native, name, wrapped)
|
|
468
|
+
|
|
469
|
+
for owner in (io, _io):
|
|
470
|
+
original = owner.open_code
|
|
471
|
+
def open_code(original):
|
|
472
|
+
def wrapped(path):
|
|
473
|
+
if active():
|
|
474
|
+
path_call("read", path)
|
|
475
|
+
value = original(path)
|
|
476
|
+
grants[value.fileno()] = (fsdecode(fspath(path)), ("read",), reference(value))
|
|
477
|
+
return File(value, ("read",)) if active() else value
|
|
478
|
+
return wrapped
|
|
479
|
+
setattr(owner, "open_code", open_code(original))
|
|
480
|
+
for loader in (importlib.machinery.SourceFileLoader, importlib.machinery.SourcelessFileLoader):
|
|
481
|
+
wrap_paths(loader, "get_data", [(1, "path", "read")])
|
|
482
|
+
original = loader.get_source
|
|
483
|
+
def get_source(original):
|
|
484
|
+
def wrapped(self, fullname):
|
|
485
|
+
if active():
|
|
486
|
+
path_call("read", self.path)
|
|
487
|
+
return original(self, fullname)
|
|
488
|
+
return wrapped
|
|
489
|
+
setattr(loader, "get_source", get_source(original))
|
|
490
|
+
source_loader = importlib.machinery.SourceFileLoader
|
|
491
|
+
original_set_data = source_loader.set_data
|
|
492
|
+
cache_code = getattr(source_loader, "_cache_bytecode").__code__
|
|
493
|
+
get_code = source_loader.get_code.__code__
|
|
494
|
+
def set_data(self, path, data, *args, **kwargs):
|
|
495
|
+
if getframe(1).f_code is cache_code and getframe(2).f_code is get_code:
|
|
496
|
+
# The engine's optional bytecode cache is not a cell-requested write.
|
|
497
|
+
# A direct call to set_data still crosses the ordinary write gate.
|
|
498
|
+
return perform(original_set_data, (self, path, data, *args), kwargs)
|
|
499
|
+
if active():
|
|
500
|
+
path_call("write", path)
|
|
501
|
+
return original_set_data(self, path, data, *args, **kwargs)
|
|
502
|
+
source_loader.set_data = set_data
|
|
503
|
+
|
|
504
|
+
forbidden_modules = frozenset(("ctypes", "_ctypes", "cffi", "_cffi_backend", "_posixsubprocess", "multiprocessing"))
|
|
505
|
+
def inspect_import(name):
|
|
506
|
+
module = modules.get(name)
|
|
507
|
+
forbidden_module = name.split(".", 1)[0] in forbidden_modules
|
|
508
|
+
if not forbidden_module and module is not None and id(module) in engine_modules:
|
|
509
|
+
return
|
|
510
|
+
if not active():
|
|
511
|
+
return
|
|
512
|
+
if forbidden_module:
|
|
513
|
+
refuse(f"import {name}")
|
|
514
|
+
if module is not None:
|
|
515
|
+
path = getattr(module, "__file__", None)
|
|
516
|
+
if path:
|
|
517
|
+
path_call("read", path)
|
|
518
|
+
def importing(name, globals=None, locals=None, fromlist=(), level=0):
|
|
519
|
+
absolute = resolve_name("." * level + name, (globals or {}).get("__package__")) if level else name
|
|
520
|
+
inspect_import(absolute)
|
|
521
|
+
result = original_import(name, globals, locals, fromlist, level)
|
|
522
|
+
# Loader checks protect uncached modules; cache hits need this import-time check.
|
|
523
|
+
inspect_import(absolute)
|
|
524
|
+
for child in fromlist or ():
|
|
525
|
+
if child != "*" and absolute + "." + child in modules:
|
|
526
|
+
inspect_import(absolute + "." + child)
|
|
527
|
+
return result
|
|
528
|
+
builtins.__import__ = importing
|
|
529
|
+
def import_module(name, package=None):
|
|
530
|
+
absolute = resolve_name(name, package) if name.startswith(".") else name
|
|
531
|
+
inspect_import(absolute)
|
|
532
|
+
return original_import_module(name, package)
|
|
533
|
+
importlib.import_module = import_module
|
|
534
|
+
original_pkg_data = pkgutil.get_data
|
|
535
|
+
def pkg_data(package, resource):
|
|
536
|
+
if active():
|
|
537
|
+
module = original_import_module(package)
|
|
538
|
+
if module.__file__ is not None:
|
|
539
|
+
path_call("read", join_path(dirname(module.__file__), resource))
|
|
540
|
+
return original_pkg_data(package, resource)
|
|
541
|
+
pkgutil.get_data = pkg_data
|
|
542
|
+
original_files = importlib.resources.files
|
|
543
|
+
original_as_file = importlib.resources.as_file
|
|
544
|
+
files_parameters = __import__("inspect").signature(original_files).parameters
|
|
545
|
+
class Resource:
|
|
546
|
+
def __init__(self, value):
|
|
547
|
+
self._value = value
|
|
548
|
+
def __getattr__(self, name):
|
|
549
|
+
return getattr(self._value, name)
|
|
550
|
+
def __fspath__(self):
|
|
551
|
+
return fspath(self._value)
|
|
552
|
+
def __truediv__(self, child):
|
|
553
|
+
return self.joinpath(child)
|
|
554
|
+
def joinpath(self, *children):
|
|
555
|
+
return Resource(self._value.joinpath(*children))
|
|
556
|
+
def iterdir(self):
|
|
557
|
+
if active():
|
|
558
|
+
path_call("ls", str(self._value))
|
|
559
|
+
for child in self._value.iterdir():
|
|
560
|
+
yield Resource(child)
|
|
561
|
+
def read_text(self, *args, **kwargs):
|
|
562
|
+
if active():
|
|
563
|
+
path_call("read", str(self._value))
|
|
564
|
+
return self._value.read_text(*args, **kwargs)
|
|
565
|
+
def read_bytes(self):
|
|
566
|
+
if active():
|
|
567
|
+
path_call("read", str(self._value))
|
|
568
|
+
return self._value.read_bytes()
|
|
569
|
+
def open(self, mode="r", *args, **kwargs):
|
|
570
|
+
if not active():
|
|
571
|
+
return self._value.open(mode, *args, **kwargs)
|
|
572
|
+
allowed = opening(str(self._value), mode, {})
|
|
573
|
+
result = self._value.open(mode, *args, **kwargs)
|
|
574
|
+
wrapped = File(result, allowed)
|
|
575
|
+
if wrapped._fd is not None:
|
|
576
|
+
grants[wrapped._fd] = (str(self._value), allowed, reference(result))
|
|
577
|
+
return wrapped
|
|
578
|
+
def files(*args, **kwargs):
|
|
579
|
+
if "anchor" in files_parameters and (args[0] if args else kwargs.get("anchor", kwargs.get("package"))) is None:
|
|
580
|
+
caller = getframe(1).f_globals.get("__name__")
|
|
581
|
+
if args:
|
|
582
|
+
args = (caller, *args[1:])
|
|
583
|
+
else:
|
|
584
|
+
kwargs = {**kwargs, "anchor": caller}
|
|
585
|
+
value = original_files(*args, **kwargs)
|
|
586
|
+
return Resource(value) if active() else value
|
|
587
|
+
@contextlib.contextmanager
|
|
588
|
+
def as_file(path):
|
|
589
|
+
if isinstance(path, Resource):
|
|
590
|
+
if active():
|
|
591
|
+
path_call("read", str(path._value))
|
|
592
|
+
path = path._value
|
|
593
|
+
with original_as_file(path) as result:
|
|
594
|
+
yield result
|
|
595
|
+
importlib.resources.files = files
|
|
596
|
+
importlib.resources.as_file = as_file
|
|
597
|
+
for module_name in ("importlib.resources._common", "importlib.resources._functional", "importlib.resources._legacy"):
|
|
598
|
+
module = sys.modules.get(module_name)
|
|
599
|
+
if module is not None and hasattr(module, "files"):
|
|
600
|
+
setattr(module, "files", files)
|
|
601
|
+
|
|
602
|
+
original_connect: Callable[..., sqlite3.Connection] = sqlite3.connect
|
|
603
|
+
original_connection = sqlite3.Connection
|
|
604
|
+
databases = weakref.WeakKeyDictionary()
|
|
605
|
+
sqlite_denied = contextvars.ContextVar("senpi_sqlite_denied", default=False)
|
|
606
|
+
def database_path(database, uri):
|
|
607
|
+
path = fsdecode(fspath(database))
|
|
608
|
+
if path in ("", ":memory:"):
|
|
609
|
+
return None, ()
|
|
610
|
+
mode = None
|
|
611
|
+
if uri and path.startswith("file:"):
|
|
612
|
+
parsed = urlsplit(path)
|
|
613
|
+
mode = parse_qs(parsed.query).get("mode", [None])[0]
|
|
614
|
+
if mode == "memory":
|
|
615
|
+
return None, ()
|
|
616
|
+
path = unquote(parsed.path)
|
|
617
|
+
if path == ":memory:":
|
|
618
|
+
return None, ()
|
|
619
|
+
return path, ("read",) if mode == "ro" else ("read", "write")
|
|
620
|
+
class Cursor(sqlite3.Cursor):
|
|
621
|
+
pass
|
|
622
|
+
for name in ("execute", "executemany", "executescript", "fetchone", "fetchmany", "fetchall", "__next__"):
|
|
623
|
+
def cursor_method(original):
|
|
624
|
+
def wrapped(self, *args, **kwargs):
|
|
625
|
+
active()
|
|
626
|
+
sqlite_denied.set(False)
|
|
627
|
+
try:
|
|
628
|
+
return original(self, *args, **kwargs)
|
|
629
|
+
except DatabaseError:
|
|
630
|
+
if sqlite_denied.get():
|
|
631
|
+
refuse("SQLite ATTACH")
|
|
632
|
+
raise
|
|
633
|
+
return wrapped
|
|
634
|
+
setattr(Cursor, name, cursor_method(getattr(sqlite3.Cursor, name)))
|
|
635
|
+
class Connection(original_connection):
|
|
636
|
+
def __init__(self, database, *args, **kwargs):
|
|
637
|
+
uri = kwargs.get("uri", args[6] if len(args) > 6 else False)
|
|
638
|
+
path, allowed = database_path(database, uri)
|
|
639
|
+
if active() and path is not None:
|
|
640
|
+
for mode in allowed:
|
|
641
|
+
path_call(mode, path)
|
|
642
|
+
super().__init__(database, *args, **kwargs)
|
|
643
|
+
databases[self] = path
|
|
644
|
+
self.set_authorizer(None)
|
|
645
|
+
def set_authorizer(self, callback):
|
|
646
|
+
def authorizer(action, *args):
|
|
647
|
+
if active() and action == SQLITE_ATTACH:
|
|
648
|
+
sqlite_denied.set(True)
|
|
649
|
+
return SQLITE_DENY
|
|
650
|
+
return SQLITE_OK if callback is None else callback(action, *args)
|
|
651
|
+
return super().set_authorizer(authorizer)
|
|
652
|
+
def cursor(self, factory=None):
|
|
653
|
+
active()
|
|
654
|
+
return super().cursor(factory or Cursor)
|
|
655
|
+
def execute(self, *args, **kwargs):
|
|
656
|
+
return self.cursor().execute(*args, **kwargs)
|
|
657
|
+
def executemany(self, *args, **kwargs):
|
|
658
|
+
return self.cursor().executemany(*args, **kwargs)
|
|
659
|
+
def executescript(self, *args, **kwargs):
|
|
660
|
+
return self.cursor().executescript(*args, **kwargs)
|
|
661
|
+
def enable_load_extension(self, enabled):
|
|
662
|
+
if active() and enabled:
|
|
663
|
+
refuse("SQLite extension loading")
|
|
664
|
+
return super().enable_load_extension(enabled)
|
|
665
|
+
def load_extension(self, *args, **kwargs):
|
|
666
|
+
if active():
|
|
667
|
+
refuse("SQLite extension loading")
|
|
668
|
+
return super().load_extension(*args, **kwargs)
|
|
669
|
+
def backup(self, target, *args, **kwargs):
|
|
670
|
+
if active():
|
|
671
|
+
if target not in databases:
|
|
672
|
+
refuse("unattributed SQLite target")
|
|
673
|
+
for database, mode in ((self, "read"), (target, "write")):
|
|
674
|
+
path = databases.get(database)
|
|
675
|
+
if path is not None:
|
|
676
|
+
path_call(mode, path)
|
|
677
|
+
return super().backup(target, *args, **kwargs)
|
|
678
|
+
def close(self):
|
|
679
|
+
databases.pop(self, None)
|
|
680
|
+
return super().close()
|
|
681
|
+
def connect(database, *args, **kwargs):
|
|
682
|
+
if not active():
|
|
683
|
+
return original_connect(database, *args, **kwargs)
|
|
684
|
+
factory = kwargs.get("factory", args[4] if len(args) > 4 else original_connection)
|
|
685
|
+
if factory not in (original_connection, Connection):
|
|
686
|
+
if isinstance(factory, type) and issubclass(factory, Connection):
|
|
687
|
+
pass
|
|
688
|
+
elif isinstance(factory, type) and issubclass(factory, original_connection):
|
|
689
|
+
factory = type("PermissionConnection", (Connection, factory), {})
|
|
690
|
+
else:
|
|
691
|
+
# Preserve SQLite's validation/exception for an invalid factory.
|
|
692
|
+
# Callable factories receive the checked constructor via their usual module binding.
|
|
693
|
+
path, allowed = database_path(database, kwargs.get("uri", args[6] if len(args) > 6 else False))
|
|
694
|
+
if path is not None:
|
|
695
|
+
for mode in allowed:
|
|
696
|
+
path_call(mode, path)
|
|
697
|
+
return original_connect(database, *args, **kwargs)
|
|
698
|
+
else:
|
|
699
|
+
factory = Connection
|
|
700
|
+
if len(args) > 4:
|
|
701
|
+
args = (*args[:4], factory, *args[5:])
|
|
702
|
+
else:
|
|
703
|
+
kwargs["factory"] = factory
|
|
704
|
+
return original_connect(database, *args, **kwargs)
|
|
705
|
+
for namespace in (sqlite3, sys.modules["sqlite3.dbapi2"], sys.modules["_sqlite3"]):
|
|
706
|
+
setattr(namespace, "connect", connect)
|
|
707
|
+
setattr(namespace, "Connection", Connection)
|
|
708
|
+
setattr(namespace, "Cursor", Cursor)
|
|
709
|
+
|
|
710
|
+
def registry_mutation():
|
|
711
|
+
# Import machinery writes the real registries directly, through the
|
|
712
|
+
# references it reads from an import frame. A cell only ever holds one of
|
|
713
|
+
# the views below, so reaching here means cell code is the writer.
|
|
714
|
+
if active():
|
|
715
|
+
refuse("import registry mutation")
|
|
716
|
+
class Sys(types.ModuleType):
|
|
717
|
+
def __getattribute__(self, name):
|
|
718
|
+
value = module_getattribute(self, name)
|
|
719
|
+
if self is not sys or name not in ("modules", "meta_path", "path_hooks", "__dict__") or not shielding():
|
|
720
|
+
return value
|
|
721
|
+
# Import machinery owns the real registries. Cells get live views
|
|
722
|
+
# which have no dict/list base descriptor capable of mutating them.
|
|
723
|
+
if getframe(1).f_code in import_codes:
|
|
724
|
+
return value
|
|
725
|
+
if name == "__dict__":
|
|
726
|
+
return mapping_proxy({**value, "modules": module_view,
|
|
727
|
+
"meta_path": meta_view, "path_hooks": path_view})
|
|
728
|
+
return {"modules": module_view, "meta_path": meta_view, "path_hooks": path_view}[name]
|
|
729
|
+
def __setattr__(self, name, value):
|
|
730
|
+
if self is sys and name in ("modules", "meta_path", "path_hooks") and shielding():
|
|
731
|
+
refuse(f"sys.{name} replacement")
|
|
732
|
+
return super().__setattr__(name, value)
|
|
733
|
+
class ModuleView(MutableMapping[str, object]):
|
|
734
|
+
def __getitem__(self, key):
|
|
735
|
+
return modules[key]
|
|
736
|
+
def __iter__(self):
|
|
737
|
+
return iter(modules)
|
|
738
|
+
def __len__(self):
|
|
739
|
+
return len(modules)
|
|
740
|
+
def __setitem__(self, key, value):
|
|
741
|
+
registry_mutation()
|
|
742
|
+
modules[key] = value
|
|
743
|
+
def __delitem__(self, key):
|
|
744
|
+
registry_mutation()
|
|
745
|
+
del modules[key]
|
|
746
|
+
def copy(self):
|
|
747
|
+
return dict(modules)
|
|
748
|
+
def hook_view(hooks):
|
|
749
|
+
class HookView(MutableSequence[object]):
|
|
750
|
+
def __getitem__(self, key):
|
|
751
|
+
return hooks[key]
|
|
752
|
+
def __len__(self):
|
|
753
|
+
return len(hooks)
|
|
754
|
+
def __setitem__(self, key, value):
|
|
755
|
+
registry_mutation()
|
|
756
|
+
hooks[key] = value
|
|
757
|
+
def __delitem__(self, key):
|
|
758
|
+
registry_mutation()
|
|
759
|
+
del hooks[key]
|
|
760
|
+
def insert(self, index, value):
|
|
761
|
+
registry_mutation()
|
|
762
|
+
hooks.insert(index, value)
|
|
763
|
+
return HookView()
|
|
764
|
+
module_view, meta_view, path_view = ModuleView(), hook_view(meta_path), hook_view(path_hooks)
|
|
765
|
+
setattr(sys, "__class__", Sys)
|