fix(docker): support WebUI installs from read-only sources (#48541)

This commit is contained in:
Evo 2026-06-19 08:52:16 +08:00 committed by GitHub
parent d2c53ff558
commit 36851fa576
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 146 additions and 3 deletions

View file

@ -102,6 +102,3 @@ acp_registry/
.gitattributes
.hadolint.yaml
.mailmap
# Top-level LICENSE (not matched by *.md); not needed inside the container
LICENSE

View file

@ -2,13 +2,68 @@ from __future__ import annotations
from collections import defaultdict
from pathlib import Path
import tempfile
from setuptools import setup
from setuptools.command.build import build as _build
from setuptools.command.egg_info import egg_info as _egg_info
REPO_ROOT = Path(__file__).parent.resolve()
def _source_tree_is_writable() -> bool:
probe = REPO_ROOT / ".setuptools-write-probe"
try:
with probe.open("w", encoding="utf-8") as handle:
handle.write("")
probe.unlink()
except OSError:
try:
probe.unlink(missing_ok=True)
except OSError:
pass
return False
return True
def _temporary_build_dir(kind: str) -> str:
return tempfile.mkdtemp(prefix=f"hermes-agent-{kind}-")
def _would_write_under_source(path_value: str | None) -> bool:
if path_value is None:
return True
path = Path(path_value)
if not path.is_absolute():
path = REPO_ROOT / path
try:
path.resolve().relative_to(REPO_ROOT)
except ValueError:
return False
return True
class ReadOnlySourceBuild(_build):
def finalize_options(self) -> None:
if (
not _source_tree_is_writable()
and _would_write_under_source(self.build_base)
):
self.build_base = _temporary_build_dir("build")
super().finalize_options()
class ReadOnlySourceEggInfo(_egg_info):
def finalize_options(self) -> None:
if (
not _source_tree_is_writable()
and _would_write_under_source(self.egg_base)
):
self.egg_base = _temporary_build_dir("egg-info")
super().finalize_options()
def _data_file_tree(root_name: str) -> list[tuple[str, list[str]]]:
root = REPO_ROOT / root_name
grouped: defaultdict[str, list[str]] = defaultdict(list)
@ -21,6 +76,10 @@ def _data_file_tree(root_name: str) -> list[tuple[str, list[str]]]:
setup(
cmdclass={
"build": ReadOnlySourceBuild,
"egg_info": ReadOnlySourceEggInfo,
},
data_files=[
*_data_file_tree("skills"),
*_data_file_tree("optional-skills"),

View file

@ -0,0 +1,87 @@
"""Guards for the multi-container Hermes WebUI install surface."""
from __future__ import annotations
from pathlib import Path
import runpy
from setuptools import Distribution
import setuptools
REPO_ROOT = Path(__file__).resolve().parent.parent
def _is_under(path: str, root: Path) -> bool:
try:
Path(path).resolve().relative_to(root.resolve())
except ValueError:
return False
return True
def test_docker_context_includes_license_file() -> None:
"""PEP 639 license-files metadata must resolve inside the Docker image."""
dockerignore = (REPO_ROOT / ".dockerignore").read_text(encoding="utf-8")
active_lines = [
line.strip()
for line in dockerignore.splitlines()
if line.strip() and not line.lstrip().startswith("#")
]
assert "LICENSE" not in active_lines
def test_setup_uses_temporary_outputs_when_source_tree_is_read_only(
monkeypatch,
) -> None:
"""WebUI installs from read-only /opt/hermes must not write build metadata."""
captured: dict[str, object] = {}
def capture_setup(**kwargs: object) -> None:
captured.update(kwargs)
monkeypatch.setattr(setuptools, "setup", capture_setup)
namespace = runpy.run_path(str(REPO_ROOT / "setup.py"))
cmdclass = captured["cmdclass"]
monkeypatch.setitem(
cmdclass["build"].finalize_options.__globals__,
"_source_tree_is_writable",
lambda: False,
)
monkeypatch.setitem(
cmdclass["egg_info"].finalize_options.__globals__,
"_source_tree_is_writable",
lambda: False,
)
build_cmd = cmdclass["build"](Distribution())
build_cmd.initialize_options()
build_cmd.finalize_options()
assert not _is_under(build_cmd.build_base, REPO_ROOT)
assert Path(build_cmd.build_base).name.startswith("hermes-agent-build")
source_relative_build = cmdclass["build"](Distribution())
source_relative_build.initialize_options()
source_relative_build.build_base = "nested/build"
source_relative_build.finalize_options()
assert not _is_under(source_relative_build.build_base, REPO_ROOT)
assert Path(source_relative_build.build_base).name.startswith("hermes-agent-build")
egg_info_cmd = cmdclass["egg_info"](Distribution())
egg_info_cmd.initialize_options()
egg_info_cmd.finalize_options()
assert egg_info_cmd.egg_base is not None
assert not _is_under(egg_info_cmd.egg_base, REPO_ROOT)
assert Path(egg_info_cmd.egg_base).name.startswith("hermes-agent-egg-info")
source_relative_egg_info = cmdclass["egg_info"](Distribution())
source_relative_egg_info.initialize_options()
source_relative_egg_info.egg_base = "."
source_relative_egg_info.finalize_options()
assert source_relative_egg_info.egg_base is not None
assert not _is_under(source_relative_egg_info.egg_base, REPO_ROOT)
assert Path(source_relative_egg_info.egg_base).name.startswith(
"hermes-agent-egg-info"
)