bubblewrap 沙箱不再继承后端的环境变量(复核 R-a5):bwrap 参数最前面加 --clearenv,再按缺省(HOME、TMPDIR、PATH、LANG、PYTHONDONTWRITEBYTECODE)和调用方给的逐条 --setenv;bwrap 进程本身也只拿 PATH

以前被执行的代码看得到后端进程的全部环境变量:模型的 API Key,现在又多一条带密码的 AGENTLAB_DATABASE_URL。local、seatbelt 一直只给最小的一套。调用方给的变量以前插在最前面、会被缺省的盖掉,现在放在最后、能盖过缺省的,同另外两种沙箱。测试(本机没有 bwrap,测参数和进程环境的组装):--clearenv 在所有 --setenv 前面,AGENTLAB_* / *_API_KEY 的名字和值都不出现,bwrap 进程的环境只有 PATH
This commit is contained in:
ThearchyHelios
2026-10-19 12:07:31 +08:00
parent 0d9f1d9339
commit ba87a93ef7
2 changed files with 67 additions and 4 deletions
+12 -4
View File
@@ -132,9 +132,14 @@ class BubblewrapSandbox(Sandbox):
return _apply
def _argv(self, workdir: Path, limits: SandboxLimits, cmd: list[str]) -> list[str]:
def _argv(self, workdir: Path, limits: SandboxLimits, cmd: list[str],
env: dict[str, str] | None = None) -> list[str]:
argv = [
self._bwrap or "bwrap",
# 先清空环境变量,再一条条设:bwrap 按参数的先后处理,--clearenv 要在所有 --setenv 前面。以前不清,被执行的
# 代码看得到后端进程的全部环境变量(模型的 API Key、带密码的 AGENTLAB_DATABASE_URL);local、seatbelt 一直只给
# 最小的一套
"--clearenv",
"--unshare-all", # mount/pid/ipc/uts/cgroup/net 全部隔离
*(["--share-net"] if limits.network else []),
"--die-with-parent", # 父进程没了,沙箱跟着走
@@ -147,8 +152,11 @@ class BubblewrapSandbox(Sandbox):
"--setenv", "HOME", "/work",
"--setenv", "TMPDIR", "/tmp",
"--setenv", "PATH", "/usr/bin:/bin",
"--setenv", "LANG", "C.UTF-8",
"--setenv", "PYTHONDONTWRITEBYTECODE", "1",
]
for key, value in (env or {}).items():
argv += ["--setenv", key, value]
for path in _RO_BINDS:
if Path(path).exists():
argv += ["--ro-bind", path, path]
@@ -185,14 +193,14 @@ class BubblewrapSandbox(Sandbox):
target.parent.mkdir(parents=True, exist_ok=True)
atomic.write_text(target, content)
argv = self._argv(workdir, limits, cmd)
for key, value in (env or {}).items():
argv[1:1] = ["--setenv", key, value]
argv = self._argv(workdir, limits, cmd, env)
started = time.perf_counter()
try:
proc = await asyncio.create_subprocess_exec(
*argv,
# bwrap 自己也只拿最小的环境:沙箱里的进程靠上面的 --setenv,不从这里继承
env={"PATH": os.environ.get("PATH", "/usr/bin:/bin")},
stdout=asyncio.subprocess.PIPE,
stderr=asyncio.subprocess.PIPE,
preexec_fn=self._preexec(limits),
+55
View File
@@ -103,3 +103,58 @@ def test_auto_falls_back_to_local_and_says_why(monkeypatch: pytest.MonkeyPatch)
# 节点上选 fast 档也不能点到一个起不来的 bubblewrap
monkeypatch.setattr(mgr.sys, "platform", "linux")
assert manager._resolve_named("fast") is None
# --------------------------------------------------------------------------
# 环境变量:沙箱里的代码看不到后端进程的(模型的 API Key、带密码的 AGENTLAB_DATABASE_URL)
# --------------------------------------------------------------------------
_SECRETS = {"AGENTLAB_DATABASE_URL": "postgresql+asyncpg://u:not-a-real-password@h/db",
"ANTHROPIC_API_KEY": "sk-ant-not-real", "OPENAI_API_KEY": "sk-not-real"}
def _setenvs(argv: list[str]) -> dict[str, str]:
return {argv[i + 1]: argv[i + 2] for i, a in enumerate(argv) if a == "--setenv"}
def test_the_sandbox_starts_from_a_cleared_environment(monkeypatch, tmp_path):
from app.sandbox.base import SandboxLimits
for key, value in _SECRETS.items():
monkeypatch.setenv(key, value)
argv = bw.BubblewrapSandbox()._argv(tmp_path, SandboxLimits(), ["/usr/bin/python3", "/work/x.py"],
{"RUN_FLAG": "1", "HOME": "/work/custom"})
# --clearenv 在所有 --setenv 前面:bwrap 按先后处理,放后面就把设好的也清掉了
assert argv[1] == "--clearenv" and argv.index("--clearenv") < argv.index("--setenv")
env = _setenvs(argv)
assert env["RUN_FLAG"] == "1" and env["HOME"] == "/work/custom", "调用方给的照旧生效,能盖过缺省的"
assert env["PATH"] == "/usr/bin:/bin"
assert not any(k.startswith("AGENTLAB_") or k.endswith("_API_KEY") for k in env)
assert not any(v in " ".join(argv) for v in _SECRETS.values())
async def test_bwrap_itself_gets_only_a_minimal_environment(monkeypatch, tmp_path):
for key, value in _SECRETS.items():
monkeypatch.setenv(key, value)
seen: dict[str, Any] = {}
class Done:
returncode = 0
pid = 1
async def communicate(self):
return b"", b""
async def fake_exec(*argv, **kwargs):
seen["argv"], seen["env"] = list(argv), kwargs.get("env")
return Done()
box = bw.BubblewrapSandbox()
monkeypatch.setattr(bw.BubblewrapSandbox, "available", staticmethod(lambda: True))
monkeypatch.setattr(box, "_session_dir", lambda _sid: tmp_path)
monkeypatch.setattr(bw.asyncio, "create_subprocess_exec", fake_exec)
result = await box.run("print(1)", env={"RUN_FLAG": "1"})
assert result.ok, result.error
assert seen["env"] is not None and set(seen["env"]) == {"PATH"}, "bwrap 进程本身不继承后端的环境变量"
assert not any(v in " ".join(seen["argv"]) for v in _SECRETS.values())
assert _setenvs(seen["argv"])["RUN_FLAG"] == "1"