mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 00:41:59 +00:00
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:
@@ -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),
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user