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

fix/verification-admin-route-recovery
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 .gitattributes
.hadolint.yaml .hadolint.yaml
.mailmap .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 collections import defaultdict
from pathlib import Path from pathlib import Path
import tempfile
from setuptools import setup 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() 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]]]: def _data_file_tree(root_name: str) -> list[tuple[str, list[str]]]:
root = REPO_ROOT / root_name root = REPO_ROOT / root_name
grouped: defaultdict[str, list[str]] = defaultdict(list) grouped: defaultdict[str, list[str]] = defaultdict(list)
@ -21,6 +76,10 @@ def _data_file_tree(root_name: str) -> list[tuple[str, list[str]]]:
setup( setup(
cmdclass={
"build": ReadOnlySourceBuild,
"egg_info": ReadOnlySourceEggInfo,
},
data_files=[ data_files=[
*_data_file_tree("skills"), *_data_file_tree("skills"),
*_data_file_tree("optional-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"
)