@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.
Files changed (65) hide show
  1. package/CHANGELOG.md +46 -0
  2. package/package.json +4 -4
  3. package/src/bridge/http-server.ts +18 -5
  4. package/src/bridge/permission-check.ts +76 -0
  5. package/src/bridge/protocol.ts +7 -0
  6. package/src/kernels/jl/kernel.ts +3 -0
  7. package/src/kernels/js/context-manager.ts +26 -8
  8. package/src/kernels/js/inline-worker-entry.js +1 -23
  9. package/src/kernels/js/inline-worker.ts +7 -2
  10. package/src/kernels/js/interpose-bun.js +220 -0
  11. package/src/kernels/js/interpose-capabilities.js +29 -0
  12. package/src/kernels/js/interpose-code.js +8 -0
  13. package/src/kernels/js/interpose-fs.js +124 -0
  14. package/src/kernels/js/interpose-modules.js +107 -0
  15. package/src/kernels/js/interpose-paths.js +80 -0
  16. package/src/kernels/js/interpose-process.js +97 -0
  17. package/src/kernels/js/interpose-resources.js +59 -0
  18. package/src/kernels/js/interpose-sqlite.js +104 -0
  19. package/src/kernels/js/interpose.js +86 -0
  20. package/src/kernels/js/kernel-contract.ts +31 -0
  21. package/src/kernels/js/kernel-tools-pump.js +4 -2
  22. package/src/kernels/js/local-module-loader.ts +9 -4
  23. package/src/kernels/js/permission-authority.d.mts +10 -0
  24. package/src/kernels/js/permission-authority.mjs +11 -0
  25. package/src/kernels/js/permission-core.md +114 -0
  26. package/src/kernels/js/permission-host.ts +388 -0
  27. package/src/kernels/js/permission-runtime.js +205 -0
  28. package/src/kernels/js/permission-sessions.ts +46 -0
  29. package/src/kernels/js/permission-slots.d.mts +51 -0
  30. package/src/kernels/js/permission-slots.mjs +133 -0
  31. package/src/kernels/js/permission-worker-entry.js +1 -0
  32. package/src/kernels/js/permission-worker-frames.ts +55 -0
  33. package/src/kernels/js/process-tree.js +9 -1
  34. package/src/kernels/js/rewrite-imports.ts +44 -0
  35. package/src/kernels/js/windows-tree-kill.js +10 -2
  36. package/src/kernels/js/worker-core.d.ts +2 -0
  37. package/src/kernels/js/worker-core.js +5 -2
  38. package/src/kernels/js/worker-cwd.js +14 -8
  39. package/src/kernels/js/worker-entry.js +1 -23
  40. package/src/kernels/js/worker-file-helpers.js +58 -0
  41. package/src/kernels/js/worker-host.ts +24 -6
  42. package/src/kernels/js/worker-package-resolve.js +10 -2
  43. package/src/kernels/js/worker-runtime.js +5 -65
  44. package/src/kernels/js/worker-slot.ts +5 -1
  45. package/src/kernels/js/worker-startup.ts +7 -5
  46. package/src/kernels/js/worker-webview.js +17 -16
  47. package/src/kernels/py/interpose.py +765 -0
  48. package/src/kernels/py/kernel-contract.ts +4 -0
  49. package/src/kernels/py/kernel-tools-host.ts +5 -1
  50. package/src/kernels/py/kernel.ts +45 -5
  51. package/src/kernels/py/path_identity.py +162 -0
  52. package/src/kernels/py/path_identity_posix.py +74 -0
  53. package/src/kernels/py/path_identity_windows.py +93 -0
  54. package/src/kernels/py/permission-core.md +103 -0
  55. package/src/kernels/py/permission-host.ts +198 -0
  56. package/src/kernels/py/permission_bridge.py +145 -0
  57. package/src/kernels/py/prelude.py +40 -5
  58. package/src/kernels/py/transport.ts +15 -8
  59. package/src/kernels/rb/kernel.ts +3 -0
  60. package/src/kernels/shared/invocation-authority.ts +59 -0
  61. package/src/kernels/shared/kernel-tools-pump.ts +22 -1
  62. package/src/kernels/shared/path-identity.d.mts +42 -0
  63. package/src/kernels/shared/path-identity.mjs +149 -0
  64. package/src/kernels/shared/subprocess-contract.ts +2 -0
  65. 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)