mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-20 10:11:58 +00:00
fix(docker): support WebUI installs from read-only sources (#48541)
This commit is contained in:
parent
d2c53ff558
commit
36851fa576
3 changed files with 146 additions and 3 deletions
|
|
@ -102,6 +102,3 @@ acp_registry/
|
|||
.gitattributes
|
||||
.hadolint.yaml
|
||||
.mailmap
|
||||
|
||||
# Top-level LICENSE (not matched by *.md); not needed inside the container
|
||||
LICENSE
|
||||
|
|
|
|||
59
setup.py
59
setup.py
|
|
@ -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"),
|
||||
|
|
|
|||
87
tests/test_docker_webui_install_surface.py
Normal file
87
tests/test_docker_webui_install_surface.py
Normal 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"
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue