Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 43 additions & 8 deletions src/coder_eval/sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,45 @@
logger = logging.getLogger(__name__)


# Top-level entries excluded from Sandbox.capture_to (docker WORKDIR-alignment).
# The WORKDIR can be HOME or overlap framework mounts, so skip credentials and
# sandbox-created bulk. `.claude` is the RW lean copy of ~/.claude (carries
# .credentials.json). The rest are sandbox infra the standard artifacts path
# also drops. Matched by basename at every level (shutil.ignore_patterns).
_WORKSPACE_CAPTURE_IGNORE = (".claude", ".venv", ".npm-prefix", "node_modules")
# Entries excluded from Sandbox.capture_to (docker WORKDIR-alignment).
# Two classes of exclusion:
#
# 1. SECURITY denylist: credential files/dirs that must never leak into
# captured artifacts (which get uploaded). Defense-in-depth -- the eval
# images don't bake credentials, but any future image that does should
# not silently expose them.
#
# 2. NOISE suppression: sandbox-created bulk and home-dir infrastructure
# written by tools (uv, pip, npm, shell) when WORKDIR overlaps HOME
# (e.g. /root). These are never task deliverables.
#
# Matched by basename at every level via shutil.ignore_patterns.
_WORKSPACE_CAPTURE_IGNORE = (
# --- Security: credential stores ---
".claude", # RW lean copy of host ~/.claude (carries .credentials.json)
".aws", # AWS credentials / config
".ssh", # SSH keys
".gnupg", # GPG keys
".docker", # Docker auth (config.json)
".azure", # Azure CLI credentials
".netrc", # FTP/curl/git credentials
".gitconfig", # May embed PATs via credential.helper
# --- Noise: Python / JS build infra ---
".venv",
".npm-prefix",
"node_modules",
# --- Noise: home-dir caches & config (uv, pip, npm, etc.) ---
".cache",
".config",
".npm",
".local",
# --- Noise: shell dotfiles pre-baked into the image ---
".bashrc",
".bash_history",
".bash_logout",
".profile",
".wget-hsts",
)


def _grant_read_traverse(root: Path) -> None:
Expand Down Expand Up @@ -1080,8 +1113,10 @@ def capture_to(self, artifact_dir: Path) -> Path:
:data:`_WORKSPACE_CAPTURE_IGNORE` -- most importantly ``.claude`` (the
RW lean copy of the host ``~/.claude`` carries ``.credentials.json``;
without this a ``/root`` WORKDIR would leak it into artifacts), plus
``.venv``/``node_modules``/``.npm-prefix`` (sandbox-created bulk, already
stripped by the standard artifacts path's post-run cleanup).
``.venv``/``node_modules``/``.npm-prefix`` (sandbox-created bulk), and
Linux home-directory noise (``.cache``, ``.config``, ``.npm``,
``.local``, shell dotfiles) written by tools like uv/pip/npm when
HOME == WORKDIR.

Returns the destination path; unlike preserve_to it does NOT repoint
``self.sandbox_dir`` -- the workspace persists in-container and is reaped
Expand Down
45 changes: 43 additions & 2 deletions tests/test_sandbox.py
Original file line number Diff line number Diff line change
Expand Up @@ -1196,13 +1196,36 @@ def test_capture_to_copies_and_tolerates_dangling_symlink(tmp_path):
(ws / "real.txt").write_text("hello", encoding="utf-8")
# A dangling symlink is the exact failure the old `cp -a` bridge hit.
(ws / "dangling").symlink_to(ws / "does_not_exist")
# Framework/sensitive entries that must NOT be captured (WORKDIR may be HOME).
# Security denylist: credential stores that must never leak into artifacts.
(ws / ".claude").mkdir()
(ws / ".claude" / ".credentials.json").write_text("SECRET", encoding="utf-8")
(ws / ".aws").mkdir()
(ws / ".aws" / "credentials").write_text("[default]\naws_access_key_id=FAKE", encoding="utf-8")
(ws / ".ssh").mkdir()
(ws / ".ssh" / "id_rsa").write_text("PRIVATE KEY", encoding="utf-8")
(ws / ".gnupg").mkdir()
(ws / ".docker").mkdir()
(ws / ".docker" / "config.json").write_text('{"auths":{}}', encoding="utf-8")
(ws / ".azure").mkdir()
(ws / ".netrc").write_text("machine github.com login user password TOKEN", encoding="utf-8")
(ws / ".gitconfig").write_text("[user]\n\tname = Test", encoding="utf-8")
# Noise: Python / JS build infra.
(ws / ".venv").mkdir()
(ws / ".venv" / "pyvenv.cfg").write_text("x", encoding="utf-8")
(ws / "node_modules").mkdir()
(ws / "node_modules" / "pkg.js").write_text("x", encoding="utf-8")
# Noise: home-dir caches written by uv/pip/npm/shell when WORKDIR == HOME.
(ws / ".cache").mkdir()
(ws / ".cache" / "uv").mkdir()
(ws / ".config").mkdir()
(ws / ".config" / "uv").mkdir()
(ws / ".npm").mkdir()
(ws / ".local").mkdir()
(ws / ".bashrc").write_text("# bash", encoding="utf-8")
(ws / ".bash_history").write_text("ls\n", encoding="utf-8")
(ws / ".bash_logout").write_text("# logout", encoding="utf-8")
(ws / ".profile").write_text("# profile", encoding="utf-8")
(ws / ".wget-hsts").write_text("", encoding="utf-8")

artifacts = tmp_path / "artifacts"
dest = sandbox.capture_to(artifacts)
Expand All @@ -1211,10 +1234,28 @@ def test_capture_to_copies_and_tolerates_dangling_symlink(tmp_path):
assert (dest / "real.txt").read_text(encoding="utf-8") == "hello"
# Dangling symlink skipped (ignore_dangling_symlinks) -> did not raise.
assert not (dest / "dangling").exists()
# Credentials / sandbox bulk excluded from the captured artifacts.
# Security denylist: no credential stores in artifacts.
assert not (dest / ".claude").exists()
assert not (dest / ".aws").exists()
assert not (dest / ".ssh").exists()
assert not (dest / ".gnupg").exists()
assert not (dest / ".docker").exists()
assert not (dest / ".azure").exists()
assert not (dest / ".netrc").exists()
assert not (dest / ".gitconfig").exists()
# Noise: build infra excluded.
assert not (dest / ".venv").exists()
assert not (dest / "node_modules").exists()
# Noise: home-dir bulk excluded.
assert not (dest / ".cache").exists()
assert not (dest / ".config").exists()
assert not (dest / ".npm").exists()
assert not (dest / ".local").exists()
assert not (dest / ".bashrc").exists()
assert not (dest / ".bash_history").exists()
assert not (dest / ".bash_logout").exists()
assert not (dest / ".profile").exists()
assert not (dest / ".wget-hsts").exists()
# Source workspace is COPIED, not moved (originals untouched).
assert (ws / "real.txt").exists()
assert (ws / ".claude" / ".credentials.json").exists()
Expand Down
Loading