A declarative, hermetic harness for Nix-defined agents.
0

Configure Feed

Select the types of activity you want to include in your feed.

tests/jail: fix CI hang from bash -c exec model

+19 -9
+19 -9
tests/test_jail.py
··· 6 6 """ 7 7 8 8 import asyncio 9 + import os 9 10 import shutil 10 11 import shlex 11 12 import subprocess ··· 23 24 shutil.which("bwrap") is None or shutil.which("nix") is None, 24 25 reason="requires bwrap and nix", 25 26 ) 27 + 28 + # Every jailed command runs under `bash -c` (JailBuilder._shell_argv), so `bash` 29 + # must be resolvable on the spec's PATH. The unrestricted (host-escape) tests 30 + # exec on the host, so they put the host bash dir on PATH; a real agent's 31 + # unrestricted grant carries the shell PATH, which already includes bash. 32 + _HOST_BASH_DIR = os.path.dirname(shutil.which("bash") or "") 26 33 27 34 28 35 def _exec(jail, *args, **kwargs): ··· 160 167 result = _exec(jail, jail.build(Grant()), "git --version") 161 168 162 169 assert result.code != 0 163 - assert "no such file" in result.stderr.lower() 170 + # bash resolves the bare name on PATH and finds nothing in the closure. 171 + assert "not found" in result.stderr.lower() 164 172 165 173 166 174 @_NEEDS_SANDBOX ··· 204 212 205 213 206 214 def test_network_grant_builds_proxy_spec(tmp_path): 207 - jail = JailBuilder(str(tmp_path), "/unused/bin") 215 + jail = JailBuilder(str(tmp_path), _HOST_BASH_DIR) 208 216 spec = jail.build(Grant(allowed_hosts=["example.com:443"])) 209 217 210 218 assert spec.network == "proxy" ··· 212 220 213 221 214 222 def test_proxy_jail_sets_proxy_environment(tmp_path): 215 - jail = JailBuilder(str(tmp_path), "/unused/bin") 223 + jail = JailBuilder(str(tmp_path), _HOST_BASH_DIR) 216 224 spec = jail.build(Grant(allowed_hosts=["example.com:443"])) 217 225 218 226 argv = jail._bwrap_argv(spec, "true", proxy_url="http://127.0.0.1:12345") ··· 360 368 ) 361 369 reader.chmod(0o755) 362 370 363 - jail = JailBuilder(str(work_tree), str(bin_dir)) 371 + jail = JailBuilder(str(work_tree), os.pathsep.join([str(bin_dir), _HOST_BASH_DIR])) 364 372 spec = jail.build(Grant(unrestricted=True)) 365 373 366 374 result = _exec(jail, spec, "read-secret") ··· 370 378 371 379 372 380 def test_exec_streams_unrestricted_output_lines(tmp_path): 373 - jail = JailBuilder(str(tmp_path), "/unused/bin") 381 + jail = JailBuilder(str(tmp_path), _HOST_BASH_DIR) 374 382 spec = jail.build(Grant(unrestricted=True)) 375 383 lines: list[str] = [] 376 384 code = "import sys; print('one', flush=True); print('two', flush=True)" ··· 388 396 389 397 390 398 def test_exec_streams_unrestricted_output_without_newlines(tmp_path): 391 - jail = JailBuilder(str(tmp_path), "/unused/bin") 399 + jail = JailBuilder(str(tmp_path), _HOST_BASH_DIR) 392 400 spec = jail.build(Grant(unrestricted=True)) 393 401 lines: list[str] = [] 394 402 payload = ("x" * 70_000) + "é" ··· 415 423 416 424 417 425 def test_exec_timeout_kills_unrestricted_process(tmp_path): 418 - jail = JailBuilder(str(tmp_path), "/unused/bin") 426 + jail = JailBuilder(str(tmp_path), _HOST_BASH_DIR) 419 427 spec = jail.build(Grant(unrestricted=True)) 420 428 421 429 result = _exec( ··· 431 439 432 440 433 441 def test_exec_cancellation_terminates_unrestricted_process(tmp_path): 434 - jail = JailBuilder(str(tmp_path), "/unused/bin") 442 + jail = JailBuilder(str(tmp_path), _HOST_BASH_DIR) 435 443 spec = jail.build(Grant(unrestricted=True)) 436 444 lines: list[str] = [] 437 445 command = f"{shlex.quote(sys.executable)} -c {shlex.quote(_SLOW_PROGRAM)}" ··· 440 448 task = asyncio.create_task( 441 449 jail.exec(spec, command, output_callback=lines.append) 442 450 ) 443 - while not lines: 451 + while not lines and not task.done(): 444 452 await asyncio.sleep(0.01) 453 + if task.done(): 454 + await task # surface a launch failure instead of spinning forever 445 455 task.cancel() 446 456 with pytest.raises(asyncio.CancelledError): 447 457 await task