From b95c3d662bc80118cf7f1316ef5d0b815819d87b Mon Sep 17 00:00:00 2001 From: fabolous005 Date: Thu, 5 Nov 2026 10:17:15 +0100 Subject: [PATCH] shellmcp: bwrap read-only sandbox with hidden paths --- shellmcp.py | 12 ++++++++++++ test_shellmcp.py | 14 ++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/shellmcp.py b/shellmcp.py index 6454119..6a599c4 100644 --- a/shellmcp.py +++ b/shellmcp.py @@ -44,3 +44,15 @@ def run(command, cwd, timeout=60, limit=8000, prefix=()): out = b"" head = f"timed out after {timeout} s, killed" return f"{head}\n{cut(out.decode(errors='replace'), limit)}" + +def sandbox(cwd, hide=()): + """The bwrap argv prefix of --mode readonly.""" + argv = ["bwrap", "--ro-bind", "/", "/", "--dev", "/dev", "--proc", "/proc", + "--tmpfs", "/tmp", "--tmpfs", "/run", "--unshare-net", "--unshare-ipc", + "--unshare-pid", "--die-with-parent", "--new-session"] + for path in hide: + if os.path.isdir(path): + argv += ["--tmpfs", path] + elif os.path.exists(path): + argv += ["--ro-bind", "/dev/null", path] + return argv + ["--chdir", cwd] diff --git a/test_shellmcp.py b/test_shellmcp.py index 2b9473e..9fa972e 100644 --- a/test_shellmcp.py +++ b/test_shellmcp.py @@ -30,3 +30,17 @@ class Run(unittest.TestCase): def test_no_stdin_and_no_pager(self): self.assertEqual(shellmcp.run("cat; echo $PAGER", HERE), "exit code 0\ncat\n") + +@unittest.skipUnless(shutil.which("bwrap"), "needs bwrap") +class Sandbox(unittest.TestCase): + def test_readonly_cannot_write_and_hides_paths(self): + with tempfile.TemporaryDirectory(dir=HERE) as d: # not /tmp: the sandbox replaces /tmp + os.mkdir(os.path.join(d, "secret")) + open(os.path.join(d, "secret", "key"), "w").close() + prefix = shellmcp.sandbox(d, [os.path.join(d, "secret")]) + out = shellmcp.run("touch new; ls secret; echo > /tmp/x && echo tmp-ok", d, + prefix=prefix) + self.assertIn("Read-only file system", out) + self.assertNotIn("key", out) + self.assertIn("tmp-ok", out) + self.assertFalse(os.path.exists(os.path.join(d, "new")))