"""Sandbox tests: VFS, shell, puzzles, fabrication, mirror, and the no-real-filesystem tripwire.""" from pathlib import Path import pytest from scrypt.sandbox.fabricate import fabricate_home from scrypt.sandbox.mirror import mirror_home from scrypt.sandbox.puzzles import plant_all from scrypt.sandbox.shell import Shell from scrypt.sandbox.vfs import VFS, VfsError def make_shell(seed: int = 1) -> Shell: vfs = VFS() fabricate_home(vfs, seed=seed) return Shell(vfs) # ---------------------------------------------------------------------- vfs def test_vfs_write_read_roundtrip(): vfs = VFS() vfs.write("/home/drifter/a/b.txt", "hello") assert vfs.read("/home/drifter/a/b.txt") == "hello" assert vfs.read("~/a/b.txt") == "hello" def test_vfs_relative_paths_and_dotdot(): vfs = VFS() vfs.write("~/x/deep/file.txt", "v") vfs.chdir("~/x/deep") assert vfs.read("file.txt") == "v" assert vfs.read("../deep/file.txt") == "v" vfs.chdir("..") assert vfs.cwd_path == "/home/drifter/x" def test_vfs_remove_counts_files(): vfs = VFS() for i in range(5): vfs.write(f"~/photos/{i}.jpg", "x") assert vfs.remove("~/photos", recursive=True) == 5 assert vfs.resolve("~/photos") is None with pytest.raises(VfsError): vfs.remove("~/photos") # -------------------------------------------------------------------- shell def test_shell_ls_hides_dotfiles_without_dash_a(): sh = make_shell() plain = sh.run("ls").out assert ".bash_history" not in plain assert ".bash_history" in sh.run("ls -a").out def test_shell_pipe_cat_grep(): sh = make_shell() out = sh.run("cat documents/todo.txt | grep insurance").out assert "insurance" in out and "gym" not in out def test_shell_grep_file_directly(): sh = make_shell() assert "insurance" in sh.run("grep insurance documents/todo.txt").out def test_shell_glob_expansion(): sh = make_shell() sh.vfs.chdir("~/photos/vacation") out = sh.run("ls *.jpg").out assert "IMG_" in out def test_shell_find_by_name(): sh = make_shell() out = sh.run("find / -name todo.txt").out assert "/home/drifter/documents/todo.txt" in out def test_shell_rm_reports_deletions(): sh = make_shell() sh.run("rm -r photos") assert sh.last_deletions >= 5 def test_shell_revocation(): sh = make_shell() sh.run("grep x documents/todo.txt") sh.revoke("grep", "no more needles.") result = sh.run("grep x documents/todo.txt") assert "command not found" in result.err and "needles" in result.err assert "grep" not in sh.available() def test_most_used_tracks_and_excludes_revoked(): sh = make_shell() for _ in range(3): sh.run("ls") sh.run("pwd") assert sh.most_used() == "ls" sh.revoke("ls") assert sh.most_used() == "pwd" def test_unknown_command(): sh = make_shell() assert "command not found" in sh.run("vim todo.txt").err # ------------------------------------------------------------------ puzzles def test_zip_puzzle_solvable_via_history(): sh = make_shell(seed=3) puzzles = {p.id: p for p in plant_all(sh.vfs, seed=3)} # The password is in .bash_history history = sh.run("cat ~/.bash_history").out password = next( word.rstrip(",") for line in history.splitlines() if "# pw" in line for word in [line.split()[2]] ) sh.run(f"cd ~/downloads") assert "inflating" in sh.run(f"unzip severance.zip {password}").out assert puzzles["zip_password"].poll(sh) is not None assert puzzles["zip_password"].solved def test_zip_rejects_wrong_password(): sh = make_shell(seed=3) plant_all(sh.vfs, seed=3) assert "incorrect password" in sh.run("unzip ~/downloads/severance.zip guess").err def test_hidden_dir_puzzle_requires_reading_manifest(): sh = make_shell(seed=3) puzzles = {p.id: p for p in plant_all(sh.vfs, seed=3)} assert puzzles["hidden_dir"].poll(sh) is None sh.run("cat ~/.warden/manifest.txt") reward = puzzles["hidden_dir"].poll(sh) assert reward is not None and reward.kind == "card" def test_cron_defusal_by_rm(): sh = make_shell(seed=3) puzzles = {p.id: p for p in plant_all(sh.vfs, seed=3)} assert puzzles["cron_defusal"].poll(sh) is None sh.run("rm /etc/cron.d/reinforcement") reward = puzzles["cron_defusal"].poll(sh) assert reward is not None and reward.kind == "mercy" # -------------------------------------------------------------- ingestion def test_fabricate_is_deterministic(): a, b = VFS(), VFS() fabricate_home(a, seed=9) fabricate_home(b, seed=9) assert [p for p, _ in a.iter_files()] == [p for p, _ in b.iter_files()] def test_mirror_filters_secrets_and_caps(tmp_path: Path): (tmp_path / "notes").mkdir() (tmp_path / "notes" / "diary.txt").write_text("dear diary") (tmp_path / ".ssh").mkdir() (tmp_path / ".ssh" / "id_rsa").write_text("PRIVATE KEY") (tmp_path / "api_token.txt").write_text("sk-12345") (tmp_path / "big.txt").write_text("x" * 100_000) (tmp_path / "photo.png").write_bytes(b"\x89PNG....") vfs = VFS() mirror_home(vfs, source=tmp_path) files = dict(vfs.iter_files()) assert files["/home/drifter/notes/diary.txt"].content == "dear diary" assert not any(".ssh" in p for p in files) assert not any("api_token" in p for p in files) # Oversized text and binary files come through as name-only stubs. assert files["/home/drifter/big.txt"].content == "" assert files["/home/drifter/photo.png"].content == "" def test_mirror_never_writes_to_host(tmp_path: Path): (tmp_path / "a.txt").write_text("hi") before = sorted(p.name for p in tmp_path.rglob("*")) vfs = VFS() mirror_home(vfs, source=tmp_path) vfs.remove("/home/drifter/a.txt") # the Warden deletes the COPY after = sorted(p.name for p in tmp_path.rglob("*")) assert before == after def test_vfs_and_shell_never_import_os(): """The tripwire: only mirror.py may touch the host filesystem.""" import scrypt.sandbox.puzzles as puzzles import scrypt.sandbox.shell as shell import scrypt.sandbox.vfs as vfs for module in (vfs, shell, puzzles): source = Path(module.__file__).read_text() assert "import os" not in source, f"{module.__name__} imports os" assert "pathlib" not in source, f"{module.__name__} imports pathlib" assert "open(" not in source, f"{module.__name__} calls open()"