sandboxedjs 0.1.31 → 0.1.33

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/index.cjs CHANGED
@@ -8858,7 +8858,7 @@ async function runShell(ctxName, ctx) {
8858
8858
  const args = ctx.args.slice();
8859
8859
  let command;
8860
8860
  let scriptPath;
8861
- let readStdin2 = false;
8861
+ let readStdin = false;
8862
8862
  let interactive = false;
8863
8863
  let login = ctxName.startsWith("-");
8864
8864
  const options = {};
@@ -8871,7 +8871,7 @@ async function runShell(ctxName, ctx) {
8871
8871
  break;
8872
8872
  }
8873
8873
  if (arg === "-s") {
8874
- readStdin2 = true;
8874
+ readStdin = true;
8875
8875
  continue;
8876
8876
  }
8877
8877
  if (arg === "-i") {
@@ -8908,7 +8908,7 @@ async function runShell(ctxName, ctx) {
8908
8908
  break;
8909
8909
  }
8910
8910
  const rest = args.slice(i);
8911
- if (command === void 0 && !readStdin2 && rest.length > 0) {
8911
+ if (command === void 0 && !readStdin && rest.length > 0) {
8912
8912
  scriptPath = rest[0];
8913
8913
  }
8914
8914
  const positional = command !== void 0 ? rest.slice(1) : scriptPath !== void 0 ? rest.slice(1) : rest;
@@ -17803,6 +17803,693 @@ function mountContainerFs(FS, opts) {
17803
17803
  }
17804
17804
  }
17805
17805
 
17806
+ // src/runtime/python-syscalls.ts
17807
+ var EMPTY = new Uint8Array(0);
17808
+ function createStdinHost(ctx) {
17809
+ let pending = EMPTY;
17810
+ let eof = false;
17811
+ const take = (size) => {
17812
+ const n = Math.min(size, pending.length);
17813
+ const out = pending.subarray(0, n);
17814
+ pending = pending.subarray(n);
17815
+ return out;
17816
+ };
17817
+ return {
17818
+ /**
17819
+ * Drain a non-interactive stdin up front.
17820
+ *
17821
+ * A pipe ends, so reading it eagerly costs nothing and leaves every read
17822
+ * answerable without suspending — which is what keeps piped input working
17823
+ * on hosts with no stack-switching. A terminal never ends, so priming one
17824
+ * would hang before the program had printed its prompt.
17825
+ */
17826
+ async prime() {
17827
+ if (ctx.stdin.isTTY || ctx.stdin.interactive) return;
17828
+ try {
17829
+ pending = await ctx.stdin.readAll();
17830
+ } catch {
17831
+ pending = EMPTY;
17832
+ }
17833
+ eof = true;
17834
+ },
17835
+ buffered(size) {
17836
+ if (pending.length > 0) return take(size);
17837
+ return eof ? EMPTY : void 0;
17838
+ },
17839
+ async read(size) {
17840
+ if (pending.length > 0) return take(size);
17841
+ if (eof) return EMPTY;
17842
+ const chunk = await ctx.stdin.read(size);
17843
+ if (chunk === null || chunk.length === 0) {
17844
+ eof = true;
17845
+ return EMPTY;
17846
+ }
17847
+ pending = chunk;
17848
+ return take(size);
17849
+ },
17850
+ isatty() {
17851
+ return Boolean(ctx.stdin.isTTY);
17852
+ }
17853
+ };
17854
+ }
17855
+ var BRIDGE_PY = `
17856
+ import builtins, io, sys
17857
+ import _sbx_host as _host
17858
+ from pyodide.ffi import can_run_sync, run_sync
17859
+
17860
+
17861
+ def _to_bytes(value):
17862
+ if value is None:
17863
+ return b""
17864
+ to_bytes = getattr(value, "to_bytes", None)
17865
+ if to_bytes is not None:
17866
+ return to_bytes()
17867
+ return bytes(value.to_py())
17868
+
17869
+
17870
+ class _SbxStdin(io.RawIOBase):
17871
+ """The container's standard input, as a blocking raw stream."""
17872
+
17873
+ def readable(self):
17874
+ return True
17875
+
17876
+ def fileno(self):
17877
+ return 0
17878
+
17879
+ def isatty(self):
17880
+ return bool(_host.isatty())
17881
+
17882
+ def readinto(self, target):
17883
+ want = len(target)
17884
+ if want == 0:
17885
+ return 0
17886
+ ready = _host.buffered(want)
17887
+ if ready is None:
17888
+ if not can_run_sync():
17889
+ raise OSError(
17890
+ "this host cannot wait for interactive input: WebAssembly "
17891
+ "stack switching (JSPI) is unavailable. Pipe the input in, "
17892
+ "or use a browser or Node build that supports it."
17893
+ )
17894
+ ready = run_sync(_host.read(want))
17895
+ data = _to_bytes(ready)
17896
+ target[: len(data)] = data
17897
+ return len(data)
17898
+
17899
+
17900
+ def _install():
17901
+ stdin = io.TextIOWrapper(
17902
+ io.BufferedReader(_SbxStdin()), encoding="utf-8", errors="replace", line_buffering=True
17903
+ )
17904
+ sys.stdin = stdin
17905
+ sys.__stdin__ = stdin
17906
+
17907
+ def input(prompt=""):
17908
+ # CPython writes the prompt to stdout and strips exactly one trailing
17909
+ # newline; anything else changes what a program reads back.
17910
+ if prompt != "":
17911
+ sys.stdout.write(str(prompt))
17912
+ sys.stdout.flush()
17913
+ line = sys.stdin.readline()
17914
+ if not line:
17915
+ raise EOFError("EOF when reading a line")
17916
+ if line.endswith("\\n"):
17917
+ line = line[:-1]
17918
+ if line.endswith("\\r"):
17919
+ line = line[:-1]
17920
+ return line
17921
+
17922
+ builtins.input = input
17923
+
17924
+
17925
+ _install()
17926
+ `;
17927
+ var PROCESS_PY = `
17928
+ import io, os, shlex, subprocess, sys
17929
+ import _sbx_host as _host
17930
+ from pyodide.ffi import can_run_sync, run_sync
17931
+
17932
+ _PIPE = subprocess.PIPE
17933
+ _DEVNULL = subprocess.DEVNULL
17934
+ _STDOUT = subprocess.STDOUT
17935
+
17936
+
17937
+ def _argv_for(args, shell):
17938
+ if shell:
17939
+ line = args if isinstance(args, str) else " ".join(str(a) for a in args)
17940
+ return ["sh", "-c", line]
17941
+ if isinstance(args, (str, bytes)):
17942
+ return shlex.split(args if isinstance(args, str) else args.decode())
17943
+ return [str(a) for a in args]
17944
+
17945
+
17946
+ def _require_blocking(what):
17947
+ if not can_run_sync():
17948
+ raise OSError(
17949
+ "cannot " + what + " on this host: WebAssembly stack switching "
17950
+ "(JSPI) is unavailable."
17951
+ )
17952
+
17953
+
17954
+ def _to_bytes_result(value):
17955
+ if value is None:
17956
+ return b""
17957
+ to_bytes = getattr(value, "to_bytes", None)
17958
+ return to_bytes() if to_bytes is not None else bytes(value.to_py())
17959
+
17960
+
17961
+ class _ChildReader(io.RawIOBase):
17962
+ """One of a running child's output streams.
17963
+
17964
+ Reads suspend only for as long as the child takes to produce the next
17965
+ bytes, so \`for line in p.stdout\` follows a child that is still running
17966
+ instead of waiting for it to exit.
17967
+ """
17968
+
17969
+ def __init__(self, child_id, which):
17970
+ self._id = child_id
17971
+ self._which = which
17972
+
17973
+ def readable(self):
17974
+ return True
17975
+
17976
+ def readinto(self, target):
17977
+ want = len(target)
17978
+ if want == 0:
17979
+ return 0
17980
+ _require_blocking("read from a child process")
17981
+ data = _to_bytes_result(run_sync(_host.proc_read(self._id, self._which, want)))
17982
+ target[: len(data)] = data
17983
+ return len(data)
17984
+
17985
+
17986
+ class _ChildWriter(io.RawIOBase):
17987
+ """A running child's standard input."""
17988
+
17989
+ def __init__(self, child_id):
17990
+ self._id = child_id
17991
+
17992
+ def writable(self):
17993
+ return True
17994
+
17995
+ def write(self, data):
17996
+ if isinstance(data, str):
17997
+ text = data
17998
+ else:
17999
+ text = bytes(data).decode("utf-8", "replace")
18000
+ _host.proc_write(self._id, text)
18001
+ return len(data)
18002
+
18003
+ def close(self):
18004
+ if not self.closed:
18005
+ _host.proc_close_stdin(self._id)
18006
+ super().close()
18007
+
18008
+
18009
+ class SbxPopen:
18010
+ """A child process, run through the container's kernel.
18011
+
18012
+ The child is a real, live process: it starts when \`Popen\` is constructed
18013
+ and runs on the host's event loop while Python carries on. The parent only
18014
+ suspends when it actually reads, waits, or communicates \u2014 which is what
18015
+ makes streaming from a long-running child work rather than deadlocking on
18016
+ a single interpreter stack.
18017
+ """
18018
+
18019
+ def __init__(self, args, bufsize=-1, executable=None, stdin=None, stdout=None,
18020
+ stderr=None, preexec_fn=None, close_fds=True, shell=False, cwd=None,
18021
+ env=None, universal_newlines=None, startupinfo=None, creationflags=0,
18022
+ restore_signals=True, start_new_session=False, pass_fds=(), *,
18023
+ text=None, encoding=None, errors=None, **kwargs):
18024
+ self.args = args
18025
+ self.returncode = None
18026
+ self._text = bool(text or encoding or errors or universal_newlines)
18027
+ self._encoding = encoding or "utf-8"
18028
+ self._errors = errors or "replace"
18029
+ self._merge_err = stderr == _STDOUT
18030
+ self._closed = False
18031
+
18032
+ argv = _argv_for(args, shell)
18033
+ env_map = None if env is None else {str(k): str(v) for k, v in dict(env).items()}
18034
+ self._id = _host.proc_start(
18035
+ argv, None if cwd is None else str(cwd), env_map, self._merge_err
18036
+ )
18037
+ self.pid = int(_host.proc_pid(self._id))
18038
+
18039
+ self.stdin = self._wrap_writer() if stdin == _PIPE else None
18040
+ self.stdout = self._wrap_reader(1) if stdout == _PIPE else None
18041
+ # When merged there is only one stream, and it is stream 1.
18042
+ self.stderr = self._wrap_reader(2) if stderr == _PIPE and not self._merge_err else None
18043
+
18044
+ # Output nobody captured belongs on the parent's own streams, or a
18045
+ # program's diagnostics would vanish rather than being seen.
18046
+ self._drain_out = stdout is None
18047
+ self._drain_err = stderr is None and not self._merge_err
18048
+
18049
+ def _wrap_reader(self, which):
18050
+ raw = io.BufferedReader(_ChildReader(self._id, which))
18051
+ if self._text:
18052
+ return io.TextIOWrapper(raw, encoding=self._encoding, errors=self._errors)
18053
+ return raw
18054
+
18055
+ def _wrap_writer(self):
18056
+ raw = io.BufferedWriter(_ChildWriter(self._id))
18057
+ if self._text:
18058
+ return io.TextIOWrapper(raw, encoding=self._encoding, errors=self._errors)
18059
+ return raw
18060
+
18061
+ def _read_all(self, which):
18062
+ _require_blocking("read from a child process")
18063
+ parts = []
18064
+ while True:
18065
+ chunk = _to_bytes_result(run_sync(_host.proc_read(self._id, which, 65536)))
18066
+ if not chunk:
18067
+ break
18068
+ parts.append(chunk)
18069
+ return b"".join(parts)
18070
+
18071
+ def _decode(self, raw):
18072
+ return raw.decode(self._encoding, self._errors) if self._text else raw
18073
+
18074
+ def communicate(self, input=None, timeout=None):
18075
+ if input is not None and self.stdin is not None:
18076
+ if isinstance(input, bytes):
18077
+ input = input.decode(self._encoding, "replace")
18078
+ self.stdin.write(input)
18079
+ if self.stdin is not None:
18080
+ try:
18081
+ self.stdin.close()
18082
+ except Exception:
18083
+ pass
18084
+
18085
+ out = self.stdout.read() if self.stdout is not None else None
18086
+ err = self.stderr.read() if self.stderr is not None else None
18087
+ self.wait()
18088
+ self._flush_uncaptured()
18089
+ return out, err
18090
+
18091
+ def _flush_uncaptured(self):
18092
+ if self._drain_out:
18093
+ text = self._decode_raw(self._read_all(1))
18094
+ if text:
18095
+ sys.stdout.write(text)
18096
+ if self._drain_err:
18097
+ text = self._decode_raw(self._read_all(2))
18098
+ if text:
18099
+ sys.stderr.write(text)
18100
+ self._drain_out = False
18101
+ self._drain_err = False
18102
+
18103
+ def _decode_raw(self, raw):
18104
+ return raw.decode(self._encoding, "replace")
18105
+
18106
+ def poll(self):
18107
+ code = _host.proc_poll(self._id)
18108
+ if code is not None:
18109
+ self.returncode = int(code)
18110
+ return self.returncode
18111
+
18112
+ def wait(self, timeout=None):
18113
+ _require_blocking("wait for a child process")
18114
+ self.returncode = int(run_sync(_host.proc_wait(self._id)))
18115
+ return self.returncode
18116
+
18117
+ def kill(self):
18118
+ self.send_signal("SIGKILL")
18119
+
18120
+ def terminate(self):
18121
+ self.send_signal("SIGTERM")
18122
+
18123
+ def send_signal(self, sig):
18124
+ _host.proc_kill(self._id, sig if isinstance(sig, str) else "SIGTERM")
18125
+
18126
+ def __del__(self):
18127
+ try:
18128
+ _host.proc_release(self._id)
18129
+ except Exception:
18130
+ pass
18131
+
18132
+ def __enter__(self):
18133
+ return self
18134
+
18135
+ def __exit__(self, *exc):
18136
+ if self.stdin is not None:
18137
+ try:
18138
+ self.stdin.close()
18139
+ except Exception:
18140
+ pass
18141
+ self.wait()
18142
+ self._flush_uncaptured()
18143
+ for stream in (self.stdout, self.stderr):
18144
+ if stream is not None:
18145
+ try:
18146
+ stream.close()
18147
+ except Exception:
18148
+ pass
18149
+ # The host holds the process and its pipes until it is told the child
18150
+ # is finished with; a loop spawning children would otherwise keep every
18151
+ # one of them alive. Safe here only because the child has been waited
18152
+ # for and everything anyone wanted has been read.
18153
+ _host.proc_release(self._id)
18154
+ return False
18155
+
18156
+
18157
+ def _system(command):
18158
+ with SbxPopen(command, shell=True) as child:
18159
+ pass
18160
+ # os.system returns a wait status, not an exit code.
18161
+ return (child.returncode or 0) << 8
18162
+
18163
+
18164
+ def _popen(command, mode="r", buffering=-1):
18165
+ if "w" in mode:
18166
+ raise OSError("os.popen with mode 'w' is not supported in this container")
18167
+ child = SbxPopen(command, shell=True, stdout=_PIPE, text=True)
18168
+ stream = child.stdout
18169
+ inner = stream.close
18170
+
18171
+ def close():
18172
+ inner()
18173
+ code = child.wait()
18174
+ return None if code == 0 else code << 8
18175
+
18176
+ stream.close = close
18177
+ return stream
18178
+
18179
+
18180
+ def _install_processes():
18181
+ subprocess.Popen = SbxPopen
18182
+ os.system = _system
18183
+ os.popen = _popen
18184
+
18185
+
18186
+ _install_processes()
18187
+ `;
18188
+ var NETWORK_PY = `
18189
+ import builtins, email.message, io, sys, urllib.error, urllib.request, urllib.response
18190
+ import _sbx_host as _host
18191
+ from pyodide.ffi import can_run_sync, run_sync
18192
+
18193
+
18194
+ def _fetch(request):
18195
+ if not can_run_sync():
18196
+ raise urllib.error.URLError(
18197
+ "cannot make a request on this host: WebAssembly stack switching "
18198
+ "(JSPI) is unavailable."
18199
+ )
18200
+ body = request.data
18201
+ if isinstance(body, str):
18202
+ body = body.encode()
18203
+ try:
18204
+ result = run_sync(
18205
+ _host.http(
18206
+ request.full_url,
18207
+ request.get_method(),
18208
+ [[k, v] for k, v in request.header_items()],
18209
+ body,
18210
+ )
18211
+ )
18212
+ except Exception as error:
18213
+ raise urllib.error.URLError(str(error)) from None
18214
+
18215
+ headers = email.message.Message()
18216
+ for pair in result.headers.to_py():
18217
+ headers[str(pair[0])] = str(pair[1])
18218
+ payload = result.body.to_bytes()
18219
+ response = urllib.response.addinfourl(
18220
+ io.BytesIO(payload), headers, str(result.url), int(result.status)
18221
+ )
18222
+ response.msg = str(result.statusText)
18223
+ return response
18224
+
18225
+
18226
+ class _SbxHTTPHandler(urllib.request.HTTPHandler):
18227
+ """Both schemes, from one handler.
18228
+
18229
+ Pyodide builds urllib without \`ssl\`, so \`HTTPSHandler\` does not exist to
18230
+ subclass \u2014 and https is the scheme most callers actually want. Serving
18231
+ both from a subclass of the one handler that does exist keeps
18232
+ \`build_opener\` treating this as a replacement rather than stacking it
18233
+ alongside the original.
18234
+ """
18235
+
18236
+ def http_open(self, request):
18237
+ return _fetch(request)
18238
+
18239
+ def https_open(self, request):
18240
+ return _fetch(request)
18241
+
18242
+ https_request = urllib.request.HTTPHandler.do_request_
18243
+
18244
+
18245
+ def _raw_fetch(url, method, headers, body):
18246
+ """One request, through the container's network path."""
18247
+ if not can_run_sync():
18248
+ raise urllib.error.URLError(
18249
+ "cannot make a request on this host: WebAssembly stack switching "
18250
+ "(JSPI) is unavailable."
18251
+ )
18252
+ if isinstance(body, str):
18253
+ body = body.encode()
18254
+ result = run_sync(
18255
+ _host.http(url, method, [[str(k), str(v)] for k, v in dict(headers).items()], body)
18256
+ )
18257
+ return {
18258
+ "status": int(result.status),
18259
+ "reason": str(result.statusText),
18260
+ "headers": [(str(p[0]), str(p[1])) for p in result.headers.to_py()],
18261
+ "body": result.body.to_bytes(),
18262
+ "url": str(result.url),
18263
+ }
18264
+
18265
+
18266
+ # \u2500\u2500 Routing third-party HTTP stacks \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
18267
+ #
18268
+ # A library that brings its own transport does not go through urllib, and in
18269
+ # Pyodide several of them reach JavaScript's fetch directly \u2014 which leaves the
18270
+ # container's network policy behind entirely. They cannot all be patched at
18271
+ # boot either, because pip installs them later.
18272
+ #
18273
+ # So: adapters are registered by module name and applied the moment that module
18274
+ # is first imported, whenever that happens. Supporting another stack is a small
18275
+ # function registered here, not a change to the machinery.
18276
+
18277
+ _ADAPTERS = {}
18278
+
18279
+
18280
+ def _register_adapter(name):
18281
+ def decorate(fn):
18282
+ _ADAPTERS[name] = fn
18283
+ return fn
18284
+
18285
+ return decorate
18286
+
18287
+
18288
+ def _apply_adapter(name):
18289
+ fn = _ADAPTERS.pop(name, None)
18290
+ if fn is None:
18291
+ return
18292
+ try:
18293
+ fn()
18294
+ except Exception:
18295
+ # A stack we cannot adapt must not stop the import that triggered it.
18296
+ pass
18297
+
18298
+
18299
+ def _install_import_hook():
18300
+ real_import = builtins.__import__
18301
+
18302
+ def hooked(name, globals=None, locals=None, fromlist=(), level=0):
18303
+ module = real_import(name, globals, locals, fromlist, level)
18304
+ root = name.split(".")[0] if level == 0 else None
18305
+ if root in _ADAPTERS:
18306
+ _apply_adapter(root)
18307
+ return module
18308
+
18309
+ builtins.__import__ = hooked
18310
+
18311
+
18312
+ @_register_adapter("requests")
18313
+ def _adapt_requests():
18314
+ """Replace the transport, not the API.
18315
+
18316
+ \`HTTPAdapter.send\` is the single seam every requests call passes through,
18317
+ below sessions, redirects, cookies and retries and above the urllib3
18318
+ transport that would otherwise reach the network on its own. Replacing it
18319
+ leaves everything callers actually use intact.
18320
+ """
18321
+ import requests
18322
+ from requests.adapters import HTTPAdapter
18323
+ from requests.structures import CaseInsensitiveDict
18324
+
18325
+ native_send = HTTPAdapter.send
18326
+
18327
+ def send(self, request, stream=False, timeout=None, verify=True, cert=None, proxies=None):
18328
+ if not can_run_sync():
18329
+ # Without stack switching this transport cannot run at all. Falling
18330
+ # back to the library's own is better than breaking requests on
18331
+ # such a host \u2014 but the container's network policy is not the part
18332
+ # that degrades, so a forbidden host is still refused.
18333
+ if not _host.policy_allows(request.url):
18334
+ raise requests.exceptions.ConnectionError(
18335
+ "outbound network access is disabled for this container "
18336
+ "(enable with network: { allowOutbound: true })"
18337
+ )
18338
+ return native_send(self, request, stream, timeout, verify, cert, proxies)
18339
+ try:
18340
+ result = _raw_fetch(request.url, request.method, request.headers, request.body)
18341
+ except Exception as error:
18342
+ raise requests.exceptions.ConnectionError(str(error)) from None
18343
+ response = requests.Response()
18344
+ response.status_code = result["status"]
18345
+ response.reason = result["reason"]
18346
+ response.headers = CaseInsensitiveDict(result["headers"])
18347
+ response.url = result["url"]
18348
+ response.request = request
18349
+ response.raw = io.BytesIO(result["body"])
18350
+ response.encoding = requests.utils.get_encoding_from_headers(response.headers)
18351
+ return response
18352
+
18353
+ HTTPAdapter.send = send
18354
+
18355
+
18356
+ def _install_network():
18357
+ urllib.request.HTTPHandler = _SbxHTTPHandler
18358
+ urllib.request.HTTPSHandler = _SbxHTTPHandler
18359
+ urllib.request.install_opener(urllib.request.build_opener(_SbxHTTPHandler))
18360
+ _install_import_hook()
18361
+ for already_imported in [n for n in _ADAPTERS if n in sys.modules]:
18362
+ _apply_adapter(already_imported)
18363
+
18364
+
18365
+ _install_network()
18366
+ `;
18367
+ async function httpForPython(slot, url, method, headers, body) {
18368
+ const current = slot.current;
18369
+ if (!current) throw new Error("no program is bound");
18370
+ const result = await performRequest(current.ctx, new URL(url), {
18371
+ method,
18372
+ headers: Object.fromEntries(headers),
18373
+ ...body ? { body } : {}
18374
+ });
18375
+ return {
18376
+ status: result.status,
18377
+ statusText: result.statusText,
18378
+ headers: Object.entries(result.headers),
18379
+ body: result.body,
18380
+ url: result.url
18381
+ };
18382
+ }
18383
+ async function resuming(py, slot, work) {
18384
+ const parent = slot.current;
18385
+ try {
18386
+ return await work();
18387
+ } finally {
18388
+ if (parent && slot.current !== parent) bindProgram(py, parent);
18389
+ }
18390
+ }
18391
+ function installSyscallBridge(py) {
18392
+ const slot = py.__sbxSlot ??= { current: null };
18393
+ const children = /* @__PURE__ */ new Map();
18394
+ let lastChildId = 0;
18395
+ py.registerJsModule("_sbx_host", {
18396
+ /* Not `?.` and `??`: `buffered` returns undefined to mean "you will have
18397
+ * to wait", and a nullish default would quietly turn that into end-of-file —
18398
+ * which is exactly the silent EOF this bridge exists to remove. */
18399
+ buffered: (size) => slot.current ? slot.current.stdin.buffered(size) : EMPTY,
18400
+ read: (size) => slot.current ? slot.current.stdin.read(size) : Promise.resolve(EMPTY),
18401
+ isatty: () => slot.current?.stdin.isatty() ?? false,
18402
+ proc_start: (argv, cwd, env2, merge) => {
18403
+ const current = slot.current;
18404
+ if (!current) throw new Error("no program is bound");
18405
+ const { ctx } = current;
18406
+ const stdin = new Pipe();
18407
+ stdin.interactive = true;
18408
+ const stdout = new Pipe();
18409
+ const stderr = merge ? stdout : new Pipe();
18410
+ const proc = ctx.kernel.spawn(argv, {
18411
+ cwd: cwd ?? ctx.cwd,
18412
+ env: env2 ?? ctx.env,
18413
+ cred: ctx.cred,
18414
+ stdin,
18415
+ stdout,
18416
+ stderr
18417
+ });
18418
+ void proc.wait().then(() => {
18419
+ stdout.end();
18420
+ if (stderr !== stdout) stderr.end();
18421
+ });
18422
+ const id = ++lastChildId;
18423
+ children.set(id, { proc, stdin, stdout, stderr });
18424
+ return id;
18425
+ },
18426
+ proc_read: (id, which2, size) => {
18427
+ const child = children.get(id);
18428
+ if (!child) return Promise.resolve(EMPTY);
18429
+ const pipe = which2 === 2 ? child.stderr : child.stdout;
18430
+ return resuming(py, slot, async () => await pipe.read(size) ?? EMPTY);
18431
+ },
18432
+ proc_write: (id, text2) => {
18433
+ children.get(id)?.stdin.write(text2);
18434
+ },
18435
+ proc_close_stdin: (id) => {
18436
+ children.get(id)?.stdin.end();
18437
+ },
18438
+ /* Non-blocking on purpose: `poll()` must be able to say "still running". */
18439
+ proc_poll: (id) => children.get(id)?.proc.exitCode ?? void 0,
18440
+ proc_pid: (id) => children.get(id)?.proc.pid ?? 0,
18441
+ proc_wait: (id) => {
18442
+ const child = children.get(id);
18443
+ if (!child) return Promise.resolve(0);
18444
+ return resuming(py, slot, () => child.proc.wait());
18445
+ },
18446
+ proc_kill: (id, signal) => {
18447
+ const child = children.get(id);
18448
+ if (!child) return;
18449
+ const current = slot.current;
18450
+ current?.ctx.kernel.procs.signal(child.proc.pid, signal);
18451
+ },
18452
+ proc_release: (id) => {
18453
+ children.delete(id);
18454
+ },
18455
+ http: (url, method, headers, body) => resuming(py, slot, () => httpForPython(slot, url, method, headers, body)),
18456
+ /* So Python can enforce the container's policy even on a host where it
18457
+ * cannot route the request itself. */
18458
+ policy_allows: (url) => {
18459
+ const current = slot.current;
18460
+ if (!current) return false;
18461
+ const net = current.ctx.kernel.net;
18462
+ try {
18463
+ return net.isLocal(new URL(url).hostname) || net.outboundAllowed(url);
18464
+ } catch {
18465
+ return false;
18466
+ }
18467
+ }
18468
+ });
18469
+ py.runPython(BRIDGE_PY);
18470
+ py.runPython(PROCESS_PY);
18471
+ py.runPython(NETWORK_PY);
18472
+ }
18473
+ function bindProgram(py, binding) {
18474
+ py.__sbxSlot.current = binding;
18475
+ const { ctx, stdin } = binding;
18476
+ const decoder8 = new TextDecoder();
18477
+ py.setStdout({
18478
+ write: (buffer) => (ctx.write(decoder8.decode(buffer)), buffer.length)
18479
+ });
18480
+ py.setStderr({
18481
+ write: (buffer) => (ctx.stderr.write(decoder8.decode(buffer)), buffer.length)
18482
+ });
18483
+ py.setStdin({
18484
+ read: (buffer) => {
18485
+ const ready = stdin.buffered(buffer.length);
18486
+ if (!ready || ready.length === 0) return 0;
18487
+ buffer.set(ready);
18488
+ return ready.length;
18489
+ }
18490
+ });
18491
+ }
18492
+
17806
18493
  // src/runtime/cpython.ts
17807
18494
  var RESERVED_FOR_INTERPRETER = /* @__PURE__ */ new Set(["lib", "dev", "proc"]);
17808
18495
  var pyodideModule = null;
@@ -17875,6 +18562,7 @@ async function interpreterFor(ctx) {
17875
18562
  });
17876
18563
  mountContainerDirs(py, ctx);
17877
18564
  installRunner(py);
18565
+ installSyscallBridge(py);
17878
18566
  return py;
17879
18567
  })();
17880
18568
  interpreters.set(ctx.vfs, starting);
@@ -17969,7 +18657,7 @@ function reportError(ctx, error) {
17969
18657
  `);
17970
18658
  return 1;
17971
18659
  }
17972
- async function runCPythonProgram(ctx, source, argv, scriptDir, stdinText) {
18660
+ async function runCPythonProgram(ctx, source, argv, scriptDir = null) {
17973
18661
  let py;
17974
18662
  try {
17975
18663
  py = await interpreterFor(ctx);
@@ -17990,34 +18678,9 @@ async function runCPythonProgram(ctx, source, argv, scriptDir, stdinText) {
17990
18678
  });
17991
18679
  } catch {
17992
18680
  }
17993
- const encoder8 = new TextEncoder();
17994
- const decoder8 = new TextDecoder();
17995
- py.setStdout({
17996
- write: (buffer) => {
17997
- ctx.write(decoder8.decode(buffer));
17998
- return buffer.length;
17999
- }
18000
- });
18001
- py.setStderr({
18002
- write: (buffer) => {
18003
- ctx.stderr.write(decoder8.decode(buffer));
18004
- return buffer.length;
18005
- }
18006
- });
18007
- if (stdinText !== null) {
18008
- const bytes2 = encoder8.encode(stdinText);
18009
- let offset = 0;
18010
- py.setStdin({
18011
- read: (buffer) => {
18012
- const take = Math.min(buffer.length, bytes2.length - offset);
18013
- buffer.set(bytes2.subarray(offset, offset + take));
18014
- offset += take;
18015
- return take;
18016
- }
18017
- });
18018
- } else {
18019
- py.setStdin({ read: () => 0 });
18020
- }
18681
+ const stdinHost = createStdinHost(ctx);
18682
+ await stdinHost.prime();
18683
+ bindProgram(py, { ctx, stdin: stdinHost });
18021
18684
  try {
18022
18685
  bootstrap(py, ctx, argv, scriptDir);
18023
18686
  } catch (error) {
@@ -18056,9 +18719,7 @@ async function runCPythonRepl(ctx) {
18056
18719
  }
18057
18720
  mountContainerDirs(py, ctx);
18058
18721
  bootstrap(py, ctx, [""], null);
18059
- const decoder8 = new TextDecoder();
18060
- py.setStdout({ write: (data) => (ctx.write(decoder8.decode(data)), data.length) });
18061
- py.setStderr({ write: (data) => (ctx.stderr.write(decoder8.decode(data)), data.length) });
18722
+ bindProgram(py, { ctx, stdin: createStdinHost(ctx) });
18062
18723
  ctx.line(`Python ${String(py.runPython("import sys; sys.version.split()[0]"))} (Pyodide)`);
18063
18724
  ctx.line('Type "help()" for more information.');
18064
18725
  let source = "";
@@ -18118,6 +18779,13 @@ var micropip = defineCommand({
18118
18779
  ctx.stderr.write("pip: no packages specified\n");
18119
18780
  return 1;
18120
18781
  }
18782
+ const index = "https://pypi.org";
18783
+ if (!ctx.kernel.net.outboundAllowed(index)) {
18784
+ ctx.stderr.write(
18785
+ "pip: outbound network access is disabled for this container (enable with network: { allowOutbound: true })\n"
18786
+ );
18787
+ return 1;
18788
+ }
18121
18789
  let py;
18122
18790
  try {
18123
18791
  py = await interpreterFor(ctx);
@@ -18150,11 +18818,6 @@ function configurePython(options = {}) {
18150
18818
  configureCPython({ indexURL: options.indexURL, moduleURL: options.pyodideURL });
18151
18819
  }
18152
18820
  var isPythonAvailable = isCPythonAvailable;
18153
- async function readStdin(ctx) {
18154
- if (ctx.stdin.isTTY || ctx.stdin.interactive) return null;
18155
- const bytes2 = await ctx.stdin.readAll();
18156
- return bytes2.length ? new TextDecoder().decode(bytes2) : null;
18157
- }
18158
18821
  var python = defineCommand({
18159
18822
  name: "python3",
18160
18823
  path: "/usr/bin/python3",
@@ -18203,7 +18866,7 @@ compatible packages can be installed with pip (micropip).`,
18203
18866
  }
18204
18867
  const rest = argv.slice(i);
18205
18868
  if (command !== void 0) {
18206
- return (await runCPythonProgram(ctx, command, ["-c", ...rest], null, await readStdin(ctx))).exitCode;
18869
+ return (await runCPythonProgram(ctx, command, ["-c", ...rest], null)).exitCode;
18207
18870
  }
18208
18871
  if (moduleName !== void 0) {
18209
18872
  const program = `
@@ -18215,16 +18878,16 @@ except ImportError:
18215
18878
  print("/usr/bin/python3: No module named " + ${JSON.stringify(moduleName)}, file=sys.stderr)
18216
18879
  raise SystemExit(1)
18217
18880
  `;
18218
- return (await runCPythonProgram(ctx, program, [moduleName, ...rest], null, await readStdin(ctx))).exitCode;
18881
+ return (await runCPythonProgram(ctx, program, [moduleName, ...rest], null)).exitCode;
18219
18882
  }
18220
18883
  if (script === void 0) {
18221
18884
  if (ctx.stdin.isTTY) return runCPythonRepl(ctx);
18222
18885
  const source2 = new TextDecoder().decode(await ctx.stdin.readAll());
18223
- return source2.trim() ? (await runCPythonProgram(ctx, source2, ["", ...rest], null, null)).exitCode : 0;
18886
+ return source2.trim() ? (await runCPythonProgram(ctx, source2, ["", ...rest])).exitCode : 0;
18224
18887
  }
18225
18888
  if (script === "-") {
18226
18889
  const source2 = new TextDecoder().decode(await ctx.stdin.readAll());
18227
- return (await runCPythonProgram(ctx, source2, ["-", ...rest], null, null)).exitCode;
18890
+ return (await runCPythonProgram(ctx, source2, ["-", ...rest])).exitCode;
18228
18891
  }
18229
18892
  const abs = ctx.path(script);
18230
18893
  let source;
@@ -18235,7 +18898,7 @@ except ImportError:
18235
18898
  `);
18236
18899
  return 2;
18237
18900
  }
18238
- return (await runCPythonProgram(ctx, source, [abs, ...rest], dirname(abs), await readStdin(ctx))).exitCode;
18901
+ return (await runCPythonProgram(ctx, source, [abs, ...rest], dirname(abs))).exitCode;
18239
18902
  }
18240
18903
  });
18241
18904
  function printHelp2(ctx) {