2727
2828import asyncio
2929import os
30+ import shutil
3031from collections .abc import AsyncIterator
3132from dataclasses import dataclass
3233from typing import Annotated , Literal
3334
3435from pydantic import Field
3536
36- # Env vars stripped before forking a user-space subprocess. The runtime
37- # is a Nix-built binary; os.environ is pre-loaded with Nix runtime paths
38- # (LD_LIBRARY_PATH pointing at Nix-store libs, NIX_*, PYTHONPATH,
39- # FONTCONFIG_*). Leaking those into a host-image subprocess causes glibc
40- # ABI mismatches and silent library override bugs.
41- _RUNTIME_ONLY_ENV = {
42- "LD_LIBRARY_PATH" ,
43- "LD_PRELOAD" ,
44- "PYTHONPATH" ,
45- "PYTHONHOME" ,
46- "LOCALE_ARCHIVE" ,
47- "FONTCONFIG_FILE" ,
48- "FONTCONFIG_PATH" ,
49- "SSL_CERT_FILE" ,
50- "NIX_SSL_CERT_FILE" ,
51- }
37+ _BUNDLE_BASH = "/nix/runtime/bin/bash"
5238
5339
5440def _clean_env (extra : dict [str , str ] | None ) -> dict [str , str ]:
55- """Build a subprocess env: scrubbed base + caller overrides."""
56- env = {
57- k : v
58- for k , v in os .environ .items ()
59- if k not in _RUNTIME_ONLY_ENV and not k .startswith ("NIX_" )
60- }
41+ """Build a subprocess env: inherited runtime env + caller overrides."""
42+ env = dict (os .environ )
6143 if extra :
6244 env .update (extra )
6345 return env
6446
6547
48+ def _shell_executable (executable : str | None , env : dict [str , str ]) -> str :
49+ if executable :
50+ return shutil .which (executable , path = env .get ("PATH" )) or executable
51+ if os .access (_BUNDLE_BASH , os .X_OK ):
52+ return _BUNDLE_BASH
53+ return shutil .which ("bash" , path = env .get ("PATH" )) or "/bin/bash"
54+
55+
6656async def _read_capped (stream : asyncio .StreamReader , limit : int ) -> str :
6757 """Drain a subprocess stream, retaining at most `limit` bytes.
6858
@@ -155,6 +145,7 @@ async def run(
155145 env : dict [str , str ] | None = None ,
156146 timeout : float | None = None ,
157147 max_output : int = 10 * 1024 * 1024 ,
148+ executable : str | None = None ,
158149) -> BashResult :
159150 """Run a shell command in the sandbox and return its captured output."""
160151 sub_env = _clean_env (env )
@@ -164,6 +155,7 @@ async def run(
164155 stderr = asyncio .subprocess .PIPE ,
165156 cwd = cwd ,
166157 env = sub_env ,
158+ executable = _shell_executable (executable , sub_env ),
167159 )
168160 assert proc .stdout is not None and proc .stderr is not None
169161 stdout_task = asyncio .create_task (_read_capped (proc .stdout , max_output ))
@@ -193,6 +185,7 @@ async def run_stream(
193185 cwd : str | None = None ,
194186 env : dict [str , str ] | None = None ,
195187 timeout : float | None = None ,
188+ executable : str | None = None ,
196189) -> AsyncIterator [BashEvent ]:
197190 """Run a shell command, yielding events as the subprocess emits them.
198191
@@ -206,6 +199,7 @@ async def run_stream(
206199 stderr = asyncio .subprocess .PIPE ,
207200 cwd = cwd ,
208201 env = sub_env ,
202+ executable = _shell_executable (executable , sub_env ),
209203 )
210204
211205 async def _pump (stream , tag , queue ):
0 commit comments