mirror of
https://github.com/ThearchyHelios/AgentLab.git
synced 2026-11-15 11:01:58 +00:00
6af4e396b9
- 服务端可以托管前端构建产物(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
106 lines
4.3 KiB
Python
106 lines
4.3 KiB
Python
"""装了 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
|