Files
AgentLab/backend/tests/test_bubblewrap_probe.py
T
ThearchyHelios 6af4e396b9 生产部署:prod.sh 直接运行,package-docker.sh 打 Docker 离线包(镜像 + 一键部署脚本 + 配置 + 说明)
- 服务端可以托管前端构建产物(AGENTLAB_WEB_DIST):页面路由回退到 index.html,/api 下不存在的路径照旧是 JSON 404,
  挡住路径穿越;index.html 不缓存,assets/ 带哈希的文件长缓存;没设时行为完全不变
- scripts/prod.sh:不用 Docker 的生产运行。按需构建前端,单进程、不热重载,默认只听本机,--host 开放到局域网并提示没有登录
- Dockerfile(多阶段构建,Linux 镜像):前端在构建机架构上构建,运行阶段用 python:3.12-slim,依赖装进独立的 venv,
  以非 root 用户(uid 10001)运行,数据在 /data,带健康检查;.dockerignore 排除 .env、data/、各种库文件,镜像里实测没有密钥和数据库
- scripts/package-docker.sh:构建指定平台的镜像(默认按本机架构,可选 linux/amd64),打成
  release/agentlab-<版本>-linux-<架构>.tar,里面有 image.tar、deploy.sh、agentlab.env、README.md、SHA256SUMS
- deploy/deploy.sh:目标机上只依赖 Docker,离线可用。install / start / stop / restart / status / logs / upgrade / uninstall;
  检查架构和配置,自动导入镜像,替换容器时保留旧容器以便回退,数据永远保留,日志轮转
- 修正 bubblewrap 的可用性判断:以前只看有没有装 bwrap,容器里和 Ubuntu 24.04 默认配置下装了却建不了命名空间,
  每次执行代码都失败;现在真起一次试,不能用就退回下一个后端,并写明原因
- README 增加「生产部署」一节,部署细节见 deploy/README.md
2026-09-30 09:48:16 +08:00

106 lines
4.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""装了 bwrap 不等于建得了隔离环境。
Docker 默认的 seccomp 配置不许非特权进程建 user namespace,Ubuntu 24.04 的 AppArmor 默认也限制它。
以前 available() 只看 bwrap 在不在:容器里 auto 选中 bubblewrap,每次执行都报
「No permissions to create a new namespace」,而本地子进程明明能用。现在真起一次 bwrap 试,
起不来就算不可用,auto 往下退。
"""
from __future__ import annotations
import subprocess
from typing import Any
import pytest
from app.sandbox import bubblewrap_sandbox as bw
from app.sandbox import manager as mgr
from app.sandbox.local_sandbox import LocalSandbox
DENIED = (
"bwrap: No permissions to create a new namespace, likely because the kernel does not "
"allow non-privileged user namespaces.\n"
)
@pytest.fixture(autouse=True)
def _fresh_probe(monkeypatch: pytest.MonkeyPatch):
bw._probe.cache_clear()
monkeypatch.setattr(bw, "_on_linux", lambda: True)
yield
bw._probe.cache_clear()
def _fake_run(returncode: int, stderr: str = "", calls: list[Any] | None = None):
def run(argv, **kwargs): # noqa: ANN001, ANN003, ANN202
if calls is not None:
calls.append(argv)
return subprocess.CompletedProcess(argv, returncode, stdout="", stderr=stderr)
return run
def test_binary_present_but_namespaces_denied(monkeypatch: pytest.MonkeyPatch) -> None:
calls: list[Any] = []
monkeypatch.setattr(bw.shutil, "which", lambda name: f"/usr/bin/{name}")
monkeypatch.setattr(bw.subprocess, "run", _fake_run(1, DENIED, calls))
assert bw.BubblewrapSandbox.available() is False
assert "No permissions" in bw.BubblewrapSandbox.probe_failure()
# 探测真的在起 bwrap,并且带上执行时同样要用的命名空间和 /proc
argv = calls[0]
assert argv[0] == "/usr/bin/bwrap"
assert "--unshare-all" in argv and "--proc" in argv
# 结果缓存:内核和容器权限在进程生命周期里不会变
bw.BubblewrapSandbox.available()
assert len(calls) == 1
async def test_health_explains_why(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(bw.shutil, "which", lambda name: f"/usr/bin/{name}")
monkeypatch.setattr(bw.subprocess, "run", _fake_run(1, DENIED))
info = await bw.BubblewrapSandbox().health()
assert info["available"] is False
assert "No permissions" in info["error"]
def test_binary_present_and_working(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(bw.shutil, "which", lambda name: f"/usr/bin/{name}")
monkeypatch.setattr(bw.subprocess, "run", _fake_run(0))
assert bw.BubblewrapSandbox.available() is True
assert bw.BubblewrapSandbox.probe_failure() == ""
def test_probe_that_cannot_start_counts_as_unavailable(monkeypatch: pytest.MonkeyPatch) -> None:
def boom(argv, **kwargs): # noqa: ANN001, ANN003, ANN202
raise PermissionError(13, "Permission denied")
monkeypatch.setattr(bw.shutil, "which", lambda name: f"/usr/bin/{name}")
monkeypatch.setattr(bw.subprocess, "run", boom)
assert bw.BubblewrapSandbox.available() is False
def test_no_binary_skips_probe(monkeypatch: pytest.MonkeyPatch) -> None:
calls: list[Any] = []
monkeypatch.setattr(bw.shutil, "which", lambda name: None)
monkeypatch.setattr(bw.subprocess, "run", _fake_run(0, calls=calls))
assert bw.BubblewrapSandbox.available() is False
assert bw.BubblewrapSandbox.probe_failure() == ""
assert calls == []
def test_auto_falls_back_to_local_and_says_why(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setattr(bw.shutil, "which", lambda name: f"/usr/bin/{name}")
monkeypatch.setattr(bw.subprocess, "run", _fake_run(1, DENIED))
monkeypatch.setattr(mgr.MicroVMSandbox, "available", staticmethod(lambda: False))
monkeypatch.setattr(mgr.SeatbeltSandbox, "available", staticmethod(lambda: False))
monkeypatch.setattr(mgr.settings, "sandbox_backend", "auto")
manager = mgr.SandboxManager()
backend = manager._resolve()
assert isinstance(backend, LocalSandbox)
assert "bwrap" in manager._resolved_from
assert "本地子进程" in manager._resolved_from
# 节点上选 fast 档也不能点到一个起不来的 bubblewrap
monkeypatch.setattr(mgr.sys, "platform", "linux")
assert manager._resolve_named("fast") is None