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