diff --git a/misc/hermes-agent/Makefile b/misc/hermes-agent/Makefile index 68032e3a5ce6..37576d6099c2 100644 --- a/misc/hermes-agent/Makefile +++ b/misc/hermes-agent/Makefile @@ -1,183 +1,167 @@ PORTNAME= hermes-agent -PORTVERSION= 0.12.0 +PORTVERSION= 0.14.0 CATEGORIES= misc python -MASTER_SITES+= LOCAL/olivier:webcache -DISTFILES+= ${PORTNAME}-web-offline-cache-${PORTVERSION}${EXTRACT_SUFX}:webcache +MASTER_SITES+= LOCAL/olivier:webdist +DISTFILES+= ${PORTNAME}-web-dist-${PORTVERSION}${EXTRACT_SUFX}:webdist MAINTAINER= olivier@FreeBSD.org COMMENT= AI agent with built-in learning loop WWW= https://github.com/NousResearch/hermes-agent LICENSE= MIT LICENSE_FILE= ${WRKSRC}/LICENSE -BUILD_DEPENDS= npm:www/npm -RUN_DEPENDS= ${PYTHON_PKGNAMEPREFIX}anthropic>=0.39.0:misc/py-anthropic@${PY_FLAVOR} \ +RUN_DEPENDS= ${PYTHON_PKGNAMEPREFIX}anthropic>=0.86.0:misc/py-anthropic@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}croniter>=6.0.0:sysutils/py-croniter@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}edge-tts>=7.2.7:audio/py-edge-tts@${PY_FLAVOR} \ - ${PYTHON_PKGNAMEPREFIX}exa-py>=2.9.0:www/py-exa-py@${PY_FLAVOR} \ + ${PYTHON_PKGNAMEPREFIX}exa-py>=2.10.2:www/py-exa-py@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}fal-client>=0.13.1:misc/py-fal-client@${PY_FLAVOR} \ - ${PYTHON_PKGNAMEPREFIX}fastapi>=0.104.0:www/py-fastapi@${PY_FLAVOR} \ + ${PYTHON_PKGNAMEPREFIX}fastapi>=0.133.1:www/py-fastapi@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}fire>=0.7.0:devel/py-fire@${PY_FLAVOR} \ - ${PYTHON_PKGNAMEPREFIX}firecrawl-py>=4.16.0:www/py-firecrawl-py@${PY_FLAVOR} \ + ${PYTHON_PKGNAMEPREFIX}firecrawl-py>=4.17.0:www/py-firecrawl-py@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}httpx>=0.28.1:www/py-httpx@${PY_FLAVOR} \ - ${PYTHON_PKGNAMEPREFIX}Jinja2>=3.1.5:devel/py-Jinja2@${PY_FLAVOR} \ - ${PYTHON_PKGNAMEPREFIX}openai>=2.21.0:misc/py-openai@${PY_FLAVOR} \ + ${PYTHON_PKGNAMEPREFIX}Jinja2>=3.1.6:devel/py-Jinja2@${PY_FLAVOR} \ + ${PYTHON_PKGNAMEPREFIX}openai>=2.24.0:misc/py-openai@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}parallel-web>=0.4.2:www/py-parallel-web@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}prompt-toolkit>=3.0.52:devel/py-prompt-toolkit@${PY_FLAVOR} \ + ${PYTHON_PKGNAMEPREFIX}psutil>=7.2.2:sysutils/py-psutil@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}pydantic2>=2.12.5:devel/py-pydantic2@${PY_FLAVOR} \ - ${PYTHON_PKGNAMEPREFIX}pyjwt>=2.12.0:www/py-pyjwt@${PY_FLAVOR} \ + ${PYTHON_PKGNAMEPREFIX}pyjwt>=2.12.1:www/py-pyjwt@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}pysocks>0:net/py-pysocks@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}python-dotenv>=1.2.1:www/py-python-dotenv@${PY_FLAVOR} \ - ${PYTHON_PKGNAMEPREFIX}pyyaml>=6.0.2:devel/py-pyyaml@${PY_FLAVOR} \ + ${PYTHON_PKGNAMEPREFIX}pyyaml>=6.0.3:devel/py-pyyaml@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}requests>=2.33.0:www/py-requests@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}rich>=14.3.3:textproc/py-rich@${PY_FLAVOR} \ + ${PYTHON_PKGNAMEPREFIX}ruamel.yaml>=0.18.17:devel/py-ruamel.yaml@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}socksio>0:net/py-socksio@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}tenacity>=9.1.4:devel/py-tenacity@${PY_FLAVOR} \ - ${PYTHON_PKGNAMEPREFIX}uvicorn>=0.24.0:www/py-uvicorn@${PY_FLAVOR} + ${PYTHON_PKGNAMEPREFIX}uvicorn>=0.41.0:www/py-uvicorn@${PY_FLAVOR} -USES= python:3.11+,run shebangfix nodejs:lts,build +USES= python:3.11+,run shebangfix USE_GITHUB= yes GH_ACCOUNT= NousResearch GH_PROJECT= hermes-agent -GH_TAGNAME= v2026.4.30 +GH_TAGNAME= v2026.5.16 USE_RC_SUBR= hermes_dashboard hermes_gateway SUB_FILES= pkg-message NO_ARCH= yes +NO_BUILD= yes # Hermes is an application, not a Python library. Upstream's Dockerfile, # Nix flake, and Homebrew formula all install it into a private directory # (/opt/hermes, the Nix store, libexec/ respectively) rather than into # site-packages, because the project ships top-level packages with generic # names (tools, agent, gateway, plugins, ...) and bare modules (cli.py, # utils.py, ...) that would collide with other Python packages. We follow # the same convention: install the source tree under HERMES_LIBDIR and # create thin wrapper scripts in ${PREFIX}/bin that inject HERMES_LIBDIR # into sys.path before calling each entry point. HERMES_LIBDIR= ${PREFIX}/lib/${PORTNAME} PLIST_SUB+= HERMES_LIBDIR=${HERMES_LIBDIR:S,^${PREFIX}/,,} # Web dashboard SPA (Vite/React) — upstream's release tarball does NOT ship -# a prebuilt web_dist/, only the source under web/. We bring our own npm -# offline mirror as a second distfile (LOCAL/:webcache) and run -# `npm ci --offline && npm run build` in do-build to produce -# hermes_cli/web_dist/, which the dashboard serves at runtime -# (web_server.py defaults to ${HERMES_LIBDIR}/hermes_cli/web_dist). +# a prebuilt web_dist/, only the source under web/. Building it requires +# native node modules (lightningcss, @tailwindcss/oxide) that upstream only +# publishes for freebsd-x64 — no freebsd-arm64 binaries exist on npm — +# making the build impossible on non-amd64. The bundle itself is static +# HTML/CSS/JS (NO_ARCH-safe), so we prebuild it once on amd64, ship it as +# a second distfile (LOCAL/:webdist), and drop it into place +# during extract. web_server.py serves it from +# ${HERMES_LIBDIR}/hermes_cli/web_dist at runtime. # -# How to (re)generate hermes-agent-web-offline-cache-${PORTVERSION}.tar.gz -# on every PORTVERSION bump (run on a connected host with npm 10+ installed): +# How to (re)generate ${PORTNAME}-web-dist-${PORTVERSION}.tar.gz on every +# PORTVERSION bump (run on an amd64 host with npm 10+ and node 20+): # # 1. Extract the upstream source tarball: -# tar xzf ${DISTDIR}/NousResearch-hermes-agent-${PORTVERSION}-${GH_TAGNAME}_GH0.tar.gz -# cd hermes-agent-*/web -# 2. Populate a fresh npm cache from web/package-lock.json: -# rm -rf /tmp/hermes-cache && mkdir -p /tmp/hermes-cache -# HOME=/tmp npm_config_cache=/tmp/hermes-cache \ -# npm ci --no-audit --no-fund --prefer-offline -# 3. Strip non-deterministic bits (logs, last-checked stamps): -# rm -rf /tmp/hermes-cache/_logs /tmp/hermes-cache/_update-notifier-last-checked -# 4. Repackage with a top-level dir whose name matches the distfile: -# mv /tmp/hermes-cache /tmp/${PORTNAME}-web-offline-cache-${PORTVERSION} +# tar xzf ${DISTDIR}/NousResearch-${PORTNAME}-${PORTVERSION}-${GH_TAGNAME}_GH0.tar.gz +# cd ${PORTNAME}-*/web +# 2. Install deps and build the SPA: +# npm ci --no-audit --no-fund +# npm run build +# This writes the bundle to ../hermes_cli/web_dist/. +# 3. Repackage with a top-level dir whose name matches the distfile: +# cp -a ../hermes_cli/web_dist /tmp/${PORTNAME}-web-dist-${PORTVERSION} # cd /tmp && tar --no-acls --no-xattrs --no-fflags --uid=0 --gid=0 \ -# -czf ${PORTNAME}-web-offline-cache-${PORTVERSION}.tar.gz \ -# ${PORTNAME}-web-offline-cache-${PORTVERSION} -# 5. Upload to LOCAL/'s distcache directory and drop a copy +# -czf ${PORTNAME}-web-dist-${PORTVERSION}.tar.gz \ +# ${PORTNAME}-web-dist-${PORTVERSION} +# 4. Upload to LOCAL/'s distcache directory and drop a copy # into ${DISTDIR} so `make makesum` picks it up locally. -# 6. cd ${.CURDIR} && make makesum -# -# `npm ci --offline` in do-build refuses any registry call, so a missing -# entry in the mirror fails fast instead of silently going to the network. -# npm reads cacache content from ${npm_config_cache}/_cacache. Point -# npm_config_cache at the *parent* of the _cacache/ tree we shipped — if -# you point it at _cacache/ directly, npm appends _cacache/ a second time -# and silently sees an empty cache (Index entries: 0), then fails every -# install with ENOTCACHED. -WEB_CACHE_DIR= ${WRKDIR}/${PORTNAME}-web-offline-cache-${PORTVERSION} -WEB_NPM_ENV= HOME=${WRKDIR} \ - npm_config_cache=${WEB_CACHE_DIR} \ - npm_config_update_notifier=false \ - npm_config_audit=false \ - npm_config_fund=false +# 5. cd ${.CURDIR} && make makesum # Python packages and bare modules that constitute the runtime app. -HERMES_PKGS= acp_adapter agent cron gateway hermes_cli plugins tools tui_gateway -HERMES_MODS= batch_runner.py cli.py hermes_constants.py hermes_logging.py \ - hermes_state.py hermes_time.py model_tools.py rl_cli.py \ +HERMES_PKGS= acp_adapter agent cron gateway hermes_cli plugins providers \ + tools tui_gateway +HERMES_MODS= batch_runner.py cli.py hermes_bootstrap.py hermes_constants.py \ + hermes_logging.py hermes_state.py hermes_time.py model_tools.py \ run_agent.py toolset_distributions.py toolsets.py \ trajectory_compressor.py utils.py SHEBANG_FILES= ${HERMES_MODS} PORTDOCS= README.md SECURITY.md CONTRIBUTING.md AGENTS.md OPTIONS_DEFINE= DOCS PLIST_FILES= "@(,,0755) bin/hermes" \ "@(,,0755) bin/hermes-agent" \ "@(,,0755) bin/hermes-acp" -# Build the web dashboard SPA from the offline npm mirror. npm reads its -# package cache from npm_config_cache; --offline forbids any network call -# so a missing dep fails fast instead of silently going to the registry. -# The vite config writes the bundle to ../hermes_cli/web_dist (relative to -# web/), which is then picked up by do-install below. -do-build: - cd ${WRKSRC}/web && \ - ${SETENV} ${WEB_NPM_ENV} \ - npm ci --offline --no-audit --no-fund - cd ${WRKSRC}/web && \ - ${SETENV} ${WEB_NPM_ENV} \ - npm run build +# Move the prebuilt web bundle (extracted to ${WRKDIR}/${PORTNAME}-web-dist- +# ${PORTVERSION}/ by bsd.port.mk) into hermes_cli/web_dist so do-install +# picks it up alongside the rest of the hermes_cli package. +post-extract: + ${MV} ${WRKDIR}/${PORTNAME}-web-dist-${PORTVERSION} \ + ${WRKSRC}/hermes_cli/web_dist do-install: ${MKDIR} ${STAGEDIR}${HERMES_LIBDIR} .for d in ${HERMES_PKGS} cd ${WRKSRC} && ${COPYTREE_SHARE} ${d} ${STAGEDIR}${HERMES_LIBDIR} \ "! -name __pycache__ ! -name *.pyc" .endfor .for f in ${HERMES_MODS} ${INSTALL_DATA} ${WRKSRC}/${f} ${STAGEDIR}${HERMES_LIBDIR} .endfor ${MKDIR} ${STAGEDIR}${PREFIX}/bin ${SED} -e 's|%%HERMES_LIBDIR%%|${HERMES_LIBDIR}|g' \ -e 's|%%PYTHON_CMD%%|${PYTHON_CMD}|g' \ -e 's|%%ENTRY_MODULE%%|hermes_cli.main|g' \ -e 's|%%ENTRY_FUNC%%|main|g' \ ${FILESDIR}/wrapper.in > ${STAGEDIR}${PREFIX}/bin/hermes ${SED} -e 's|%%HERMES_LIBDIR%%|${HERMES_LIBDIR}|g' \ -e 's|%%PYTHON_CMD%%|${PYTHON_CMD}|g' \ -e 's|%%ENTRY_MODULE%%|run_agent|g' \ -e 's|%%ENTRY_FUNC%%|main|g' \ ${FILESDIR}/wrapper.in > ${STAGEDIR}${PREFIX}/bin/hermes-agent ${SED} -e 's|%%HERMES_LIBDIR%%|${HERMES_LIBDIR}|g' \ -e 's|%%PYTHON_CMD%%|${PYTHON_CMD}|g' \ -e 's|%%ENTRY_MODULE%%|acp_adapter.entry|g' \ -e 's|%%ENTRY_FUNC%%|main|g' \ ${FILESDIR}/wrapper.in > ${STAGEDIR}${PREFIX}/bin/hermes-acp ${MKDIR} ${STAGEDIR}${DATADIR} cd ${WRKSRC} && ${COPYTREE_SHARE} skills ${STAGEDIR}${DATADIR} cd ${WRKSRC} && ${COPYTREE_SHARE} optional-skills ${STAGEDIR}${DATADIR} # Walk the staged HERMES_LIBDIR and DATADIR trees and append every file # (and every directory we created) to the plist. This avoids hand- # maintaining a 500-line pkg-plist for skill templates that change every # release. post-install: @cd ${STAGEDIR}${PREFIX} && \ ${FIND} ${HERMES_LIBDIR:S,^${PREFIX}/,,} ${DATADIR:S,^${PREFIX}/,,} \ -type f >> ${TMPPLIST} @cd ${STAGEDIR}${PREFIX} && \ ${FIND} ${HERMES_LIBDIR:S,^${PREFIX}/,,} ${DATADIR:S,^${PREFIX}/,,} \ -type d -mindepth 1 | ${SORT} -r | \ ${SED} 's|^|@dir |' >> ${TMPPLIST} post-install-DOCS-on: ${MKDIR} ${STAGEDIR}${DOCSDIR} .for f in ${PORTDOCS} ${INSTALL_DATA} ${WRKSRC}/${f} ${STAGEDIR}${DOCSDIR} .endfor .include diff --git a/misc/hermes-agent/distinfo b/misc/hermes-agent/distinfo index 9238c06174f8..f18c2c688132 100644 --- a/misc/hermes-agent/distinfo +++ b/misc/hermes-agent/distinfo @@ -1,5 +1,5 @@ -TIMESTAMP = 1778071003 -SHA256 (hermes-agent-web-offline-cache-0.12.0.tar.gz) = b176b6ce7de35e7720d6c3363911944cfb9eca2c8e4bd1af621207d45f0ad225 -SIZE (hermes-agent-web-offline-cache-0.12.0.tar.gz) = 50966122 -SHA256 (NousResearch-hermes-agent-0.12.0-v2026.4.30_GH0.tar.gz) = 3743db721cf6c93631f8446bdc8b77fd53e0c439ee8c42ec821ebfd6874c3949 -SIZE (NousResearch-hermes-agent-0.12.0-v2026.4.30_GH0.tar.gz) = 18882013 +TIMESTAMP = 1779181415 +SHA256 (hermes-agent-web-dist-0.14.0.tar.gz) = 661b2f4065b54f81be6c399b3c518d70859db8ba5f8780e276726ecdf4101b76 +SIZE (hermes-agent-web-dist-0.14.0.tar.gz) = 5324961 +SHA256 (NousResearch-hermes-agent-0.14.0-v2026.5.16_GH0.tar.gz) = c0a554050a50ee9a62f3fa5cd288a167ba5640c42d647d100cdea084b7294143 +SIZE (NousResearch-hermes-agent-0.14.0-v2026.5.16_GH0.tar.gz) = 28019973 diff --git a/misc/hermes-agent/files/patch-hermes__cli_config.py b/misc/hermes-agent/files/patch-hermes__cli_config.py new file mode 100644 index 000000000000..9dca8b0bcfc9 --- /dev/null +++ b/misc/hermes-agent/files/patch-hermes__cli_config.py @@ -0,0 +1,19 @@ +--- hermes_cli/config.py.orig 2026-05-16 09:59:15 UTC ++++ hermes_cli/config.py 2026-05-19 11:15:59.334355000 +0200 +@@ -1421,7 +1421,15 @@ + # ``pip install`` for everything beyond the base set — appropriate + # for restricted networks, audited environments, or air-gapped + # systems where any runtime install is unacceptable. +- "allow_lazy_installs": True, ++ # FreeBSD: default disabled. This port runs hermes against ++ # the system Python, not a venv, so ``pip install`` would ++ # either fail outright (PEP 668 / EXTERNALLY-MANAGED blocks ++ # both ${PREFIX} and the --user fallback) or, if forced, ++ # write into ${PREFIX} outside pkg's manifest. Either way ++ # the feature is unusable here; turn it off so users get a ++ # clean "install the py-* port" message instead of a pip ++ # traceback. ++ "allow_lazy_installs": False, + }, + + "cron": { diff --git a/misc/hermes-agent/files/patch-hermes__cli_gateway.py b/misc/hermes-agent/files/patch-hermes__cli_gateway.py index cc8791634d1a..d4e6c7ea0332 100644 --- a/misc/hermes-agent/files/patch-hermes__cli_gateway.py +++ b/misc/hermes-agent/files/patch-hermes__cli_gateway.py @@ -1,284 +1,275 @@ ---- hermes_cli/gateway.py.orig 2026-05-06 08:29:55 UTC -+++ hermes_cli/gateway.py -@@ -733,7 +733,175 @@ def is_windows() -> bool: - return sys.platform == 'win32' - - +--- hermes_cli/gateway.py.orig 2026-05-16 09:59:15 UTC ++++ hermes_cli/gateway.py 2026-05-19 11:14:21.477531000 +0200 +@@ -1245,9 +1245,177 @@ + try: + return not bool(sys.stdin and sys.stdin.isatty()) + except (ValueError, OSError): ++ return True ++ ++ +def is_freebsd() -> bool: + return sys.platform.startswith('freebsd') + + - # ============================================================================= ++# ============================================================================= +# FreeBSD rc.d service helpers +# ============================================================================= +# +# On FreeBSD the rc.d scripts (hermes_gateway, hermes_dashboard) are shipped by +# the FreeBSD port at /usr/local/etc/rc.d/. Hermes does NOT generate or write +# them at runtime — there is no per-user scope on FreeBSD, only the system one, +# and editing rc.d / rc.conf requires root. These helpers therefore: +# * detect whether the rc script is present (port installed) +# * shell out to service(8) for start/stop/restart/status +# * shell out to sysrc(8) to flip hermes_gateway_enable=YES / -x in rc.conf +# * print exact sudo commands when running as a non-root user +# This mirrors the systemd_*/launchd_* command shape so the dispatcher in +# gateway_command() can branch on platform without duplicating logic. + +FREEBSD_RC_SCRIPT_NAME = "hermes_gateway" +FREEBSD_RC_SCRIPT_PATH = Path("/usr/local/etc/rc.d") / FREEBSD_RC_SCRIPT_NAME +FREEBSD_RC_VAR = "hermes_gateway_enable" + + +def supports_freebsd_rc() -> bool: + """Return True when running on FreeBSD with the hermes_gateway rc.d script + installed by the port. We do NOT try to install the rc script ourselves — + that's the package manager's job.""" + if not is_freebsd(): + return False + if shutil.which("service") is None: + return False + return FREEBSD_RC_SCRIPT_PATH.exists() + + +def _freebsd_is_root() -> bool: + try: + return os.geteuid() == 0 + except AttributeError: + return False + + +def _freebsd_sudo_prefix() -> list[str]: + """Return [] if already root, ['sudo'] otherwise.""" + return [] if _freebsd_is_root() else ["sudo"] + + +def _freebsd_run_or_print(cmd: list[str], *, action: str) -> bool: + """Run *cmd* (prepending sudo when needed). If sudo is required and + unavailable in batch mode, print the exact command for the user to run. + Returns True on success, False otherwise. *action* is a short verb used + for log messages (e.g. 'start', 'enable').""" + if _freebsd_is_root(): + try: + subprocess.run(cmd, check=True) + return True + except subprocess.CalledProcessError as e: + print(f"✗ Failed to {action} hermes_gateway: exit {e.returncode}") + return False + + if shutil.which("sudo") is None: + print(f" Run as root: {' '.join(cmd)}") + return False + + sudo_cmd = ["sudo"] + cmd + try: + subprocess.run(sudo_cmd, check=True) -+ return True + return True + except subprocess.CalledProcessError as e: + print(f"✗ Failed to {action} hermes_gateway: exit {e.returncode}") + return False + + +def freebsd_rc_install(force: bool = False, system: bool = False, run_as_user: str | None = None): + """Enable hermes_gateway in /etc/rc.conf and start it. + + The rc.d script itself is provided by the FreeBSD port; this function only + flips the rcvar and (re)starts the service. --system is accepted for + parity with the systemd dispatcher but is a no-op on FreeBSD (rc.d only + has system scope). --run-as-user is also a no-op: the rc script reads + hermes_gateway_user from rc.conf, set it there with sysrc. + """ + del force, system # noqa: F841 — accepted for dispatcher parity + + if not FREEBSD_RC_SCRIPT_PATH.exists(): + print(f"✗ {FREEBSD_RC_SCRIPT_PATH} not found.") + print(" The hermes_gateway rc.d script is shipped by the FreeBSD port.") + print(" Install with: sudo pkg install hermes-agent") + sys.exit(1) + + import getpass + target_user = run_as_user or getpass.getuser() + + print(f"Enabling {FREEBSD_RC_VAR}=YES in /etc/rc.conf...") + if not _freebsd_run_or_print( + ["sysrc", f"{FREEBSD_RC_VAR}=YES", f"hermes_gateway_user={target_user}"], + action="enable", + ): + return + print(f"Starting {FREEBSD_RC_SCRIPT_NAME}...") + _freebsd_run_or_print( + ["service", FREEBSD_RC_SCRIPT_NAME, "start"], + action="start", + ) -+ -+ + + +def freebsd_rc_uninstall(system: bool = False): + """Stop hermes_gateway and remove its rcvar from /etc/rc.conf. Does NOT + delete the rc.d script itself — that belongs to the FreeBSD package.""" + del system # noqa: F841 + print(f"Stopping {FREEBSD_RC_SCRIPT_NAME}...") + _freebsd_run_or_print( + ["service", FREEBSD_RC_SCRIPT_NAME, "stop"], + action="stop", + ) + print(f"Removing {FREEBSD_RC_VAR} from /etc/rc.conf...") + _freebsd_run_or_print( + ["sysrc", "-x", FREEBSD_RC_VAR], + action="disable", + ) + print(f" (The rc.d script {FREEBSD_RC_SCRIPT_PATH} is owned by the package") + print(" manager — use 'pkg delete hermes-agent' to remove it.)") + + +def freebsd_rc_start(system: bool = False): + del system + _freebsd_run_or_print( + ["service", FREEBSD_RC_SCRIPT_NAME, "start"], + action="start", + ) + + +def freebsd_rc_stop(system: bool = False): + del system + _freebsd_run_or_print( + ["service", FREEBSD_RC_SCRIPT_NAME, "stop"], + action="stop", + ) + + +def freebsd_rc_restart(system: bool = False): + del system + _freebsd_run_or_print( + ["service", FREEBSD_RC_SCRIPT_NAME, "restart"], + action="restart", + ) + + +def freebsd_rc_status(deep: bool = False, system: bool = False, full: bool = False): + del deep, system, full + # `service X status` doesn't need root; run directly without sudo. + try: + result = subprocess.run( + ["service", FREEBSD_RC_SCRIPT_NAME, "status"], + check=False, + ) + if result.returncode != 0: + print() + print("To start the gateway:") + if _freebsd_is_root(): + print(" hermes gateway start") + else: + print(f" sudo service {FREEBSD_RC_SCRIPT_NAME} start") + print(f" sudo sysrc {FREEBSD_RC_VAR}=YES # start at boot") + except FileNotFoundError: + print("✗ service(8) not found — is this really FreeBSD?") + + -+# ============================================================================= + # ============================================================================= # Service Configuration # ============================================================================= - -@@ -4083,6 +4251,8 @@ def _gateway_command_inner(args): +@@ -5057,6 +5225,8 @@ print_info(" Or use tmux/screen for persistence: tmux new -s hermes 'hermes gateway run'") print() systemd_install(force=force, system=system, run_as_user=run_as_user) + elif supports_freebsd_rc(): + freebsd_rc_install(force=force, system=system, run_as_user=run_as_user) elif is_macos(): launchd_install(force) - elif is_wsl(): -@@ -4119,6 +4289,8 @@ def _gateway_command_inner(args): + elif is_windows(): +@@ -5096,6 +5266,8 @@ sys.exit(1) if supports_systemd_services(): systemd_uninstall(system=system) + elif supports_freebsd_rc(): + freebsd_rc_uninstall(system=system) elif is_macos(): launchd_uninstall() - elif is_container(): -@@ -4149,6 +4321,8 @@ def _gateway_command_inner(args): + elif is_windows(): +@@ -5129,6 +5301,8 @@ sys.exit(1) if supports_systemd_services(): systemd_start(system=system) + elif supports_freebsd_rc(): + freebsd_rc_start(system=system) elif is_macos(): launchd_start() - elif is_wsl(): -@@ -4187,6 +4361,12 @@ def _gateway_command_inner(args): - service_available = True - except subprocess.CalledProcessError: - pass -+ elif supports_freebsd_rc(): -+ try: -+ freebsd_rc_stop(system=system) + elif is_windows(): +@@ -5167,9 +5341,21 @@ + if supports_systemd_services() and (get_systemd_unit_path(system=False).exists() or get_systemd_unit_path(system=True).exists()): + try: + systemd_stop(system=system) + service_available = True + except subprocess.CalledProcessError: + pass - elif is_macos() and get_launchd_plist_path().exists(): - try: - launchd_stop() -@@ -4208,6 +4388,12 @@ def _gateway_command_inner(args): ++ elif supports_freebsd_rc(): ++ try: ++ freebsd_rc_stop(system=system) service_available = True except subprocess.CalledProcessError: pass + elif supports_freebsd_rc(): + try: + freebsd_rc_stop(system=system) + service_available = True + except subprocess.CalledProcessError: + pass elif is_macos() and get_launchd_plist_path().exists(): try: launchd_stop() -@@ -4240,6 +4426,12 @@ def _gateway_command_inner(args): - service_stopped = True - except subprocess.CalledProcessError: - pass -+ elif supports_freebsd_rc(): -+ try: -+ freebsd_rc_stop(system=system) +@@ -5236,6 +5422,12 @@ + if supports_systemd_services() and (get_systemd_unit_path(system=False).exists() or get_systemd_unit_path(system=True).exists()): + try: + systemd_stop(system=system) + service_stopped = True + except subprocess.CalledProcessError: + pass - elif is_macos() and get_launchd_plist_path().exists(): - try: - launchd_stop() -@@ -4256,12 +4448,14 @@ def _gateway_command_inner(args): ++ elif supports_freebsd_rc(): ++ try: ++ freebsd_rc_stop(system=system) + service_stopped = True + except subprocess.CalledProcessError: + pass +@@ -5263,6 +5455,8 @@ print("Starting gateway...") if supports_systemd_services() and (get_systemd_unit_path(system=False).exists() or get_systemd_unit_path(system=True).exists()): systemd_start(system=system) + elif supports_freebsd_rc(): + freebsd_rc_start(system=system) elif is_macos() and get_launchd_plist_path().exists(): launchd_start() - else: - run_gateway(verbose=0) - return -- -+ - if supports_systemd_services() and (get_systemd_unit_path(system=False).exists() or get_systemd_unit_path(system=True).exists()): - service_configured = True - try: -@@ -4269,6 +4463,13 @@ def _gateway_command_inner(args): + elif is_windows(): +@@ -5282,6 +5476,13 @@ service_available = True except subprocess.CalledProcessError: pass + elif supports_freebsd_rc(): + service_configured = True + try: + freebsd_rc_restart(system=system) + service_available = True + except subprocess.CalledProcessError: + pass elif is_macos() and get_launchd_plist_path().exists(): service_configured = True try: -@@ -4320,6 +4521,9 @@ def _gateway_command_inner(args): - # Check for service first +@@ -5347,6 +5548,9 @@ if supports_systemd_services() and (get_systemd_unit_path(system=False).exists() or get_systemd_unit_path(system=True).exists()): systemd_status(deep, system=system, full=full) -+ _print_gateway_process_mismatch(snapshot) + _print_gateway_process_mismatch(snapshot) + elif supports_freebsd_rc(): + freebsd_rc_status(deep=deep, system=system, full=full) - _print_gateway_process_mismatch(snapshot) ++ _print_gateway_process_mismatch(snapshot) elif is_macos() and get_launchd_plist_path().exists(): launchd_status(deep) + _print_gateway_process_mismatch(snapshot) diff --git a/misc/hermes-agent/files/patch-hermes__cli_main.py b/misc/hermes-agent/files/patch-hermes__cli_main.py index 557c100f3aff..e53957e8e42e 100644 --- a/misc/hermes-agent/files/patch-hermes__cli_main.py +++ b/misc/hermes-agent/files/patch-hermes__cli_main.py @@ -1,66 +1,66 @@ ---- hermes_cli/main.py.orig 2026-05-06 08:29:55 UTC -+++ hermes_cli/main.py -@@ -8118,7 +8118,7 @@ def main(): +--- hermes_cli/main.py.orig 2026-05-16 09:59:15 UTC ++++ hermes_cli/main.py 2026-05-19 11:13:44.567947000 +0200 +@@ -9838,7 +9838,7 @@ gateway_start.add_argument( "--system", action="store_true", - help="Target the Linux system-level gateway service", + help="Target the system-level gateway service (Linux only; ignored on FreeBSD/macOS)", ) gateway_start.add_argument( "--all", -@@ -8131,7 +8131,7 @@ def main(): +@@ -9851,7 +9851,7 @@ gateway_stop.add_argument( "--system", action="store_true", - help="Target the Linux system-level gateway service", + help="Target the system-level gateway service (Linux only; ignored on FreeBSD/macOS)", ) gateway_stop.add_argument( "--all", -@@ -8146,7 +8146,7 @@ def main(): +@@ -9866,7 +9866,7 @@ gateway_restart.add_argument( "--system", action="store_true", - help="Target the Linux system-level gateway service", + help="Target the system-level gateway service (Linux only; ignored on FreeBSD/macOS)", ) gateway_restart.add_argument( "--all", -@@ -8166,23 +8166,23 @@ def main(): +@@ -9886,23 +9886,23 @@ gateway_status.add_argument( "--system", action="store_true", - help="Target the Linux system-level gateway service", + help="Target the system-level gateway service (Linux only; ignored on FreeBSD/macOS)", ) # gateway install gateway_install = gateway_subparsers.add_parser( - "install", help="Install gateway as a systemd/launchd background service" + "install", help="Install gateway as a systemd/launchd/rc.d background service" ) gateway_install.add_argument("--force", action="store_true", help="Force reinstall") gateway_install.add_argument( "--system", action="store_true", - help="Install as a Linux system-level service (starts at boot)", + help="Install as a system-level service (Linux only; FreeBSD rc.d is always system-scoped)", ) gateway_install.add_argument( "--run-as-user", dest="run_as_user", - help="User account the Linux system service should run as", + help="User account the service should run as (Linux systemd / FreeBSD rc.d)", ) # gateway uninstall -@@ -8192,7 +8192,7 @@ def main(): +@@ -9912,7 +9912,7 @@ gateway_uninstall.add_argument( "--system", action="store_true", - help="Target the Linux system-level gateway service", + help="Target the system-level gateway service (Linux only; ignored on FreeBSD/macOS)", ) - # gateway setup + # gateway list diff --git a/misc/hermes-agent/files/patch-hermes__cli_setup.py b/misc/hermes-agent/files/patch-hermes__cli_setup.py index b3543dae6ca5..f38c3bd846a5 100644 --- a/misc/hermes-agent/files/patch-hermes__cli_setup.py +++ b/misc/hermes-agent/files/patch-hermes__cli_setup.py @@ -1,95 +1,91 @@ ---- hermes_cli/setup.py.orig 2026-05-06 08:29:55 UTC -+++ hermes_cli/setup.py -@@ -2409,11 +2409,13 @@ def setup_gateway(config: dict): - +--- hermes_cli/setup.py.orig 2026-05-16 09:59:15 UTC ++++ hermes_cli/setup.py 2026-05-19 11:15:38.531786000 +0200 +@@ -2547,11 +2547,13 @@ _is_linux = _platform.system() == "Linux" _is_macos = _platform.system() == "Darwin" + _is_windows = _platform.system() == "Windows" + _is_freebsd = _platform.system() == "FreeBSD" from hermes_cli.gateway import ( _is_service_installed, _is_service_running, supports_systemd_services, + supports_freebsd_rc, has_conflicting_systemd_units, has_legacy_hermes_units, install_linux_gateway_from_setup, -@@ -2424,13 +2426,17 @@ def setup_gateway(config: dict): +@@ -2562,6 +2564,9 @@ launchd_install, launchd_start, launchd_restart, + freebsd_rc_install, + freebsd_rc_start, + freebsd_rc_restart, UserSystemdUnavailableError, - ) - + SystemScopeRequiresRootError, + _system_scope_wizard_would_need_root, +@@ -2571,7 +2576,8 @@ service_installed = _is_service_installed() service_running = _is_service_running() supports_systemd = supports_systemd_services() -- supports_service_manager = supports_systemd or _is_macos +- supports_service_manager = supports_systemd or _is_macos or _is_windows + supports_freebsd = supports_freebsd_rc() -+ supports_service_manager = supports_systemd or _is_macos or supports_freebsd ++ supports_service_manager = supports_systemd or _is_macos or _is_windows or supports_freebsd print() if supports_systemd and has_conflicting_systemd_units(): -@@ -2446,6 +2452,8 @@ def setup_gateway(config: dict): +@@ -2589,6 +2595,8 @@ try: if supports_systemd: systemd_restart() + elif supports_freebsd: + freebsd_rc_restart() elif _is_macos: launchd_restart() - except UserSystemdUnavailableError as e: -@@ -2459,6 +2467,8 @@ def setup_gateway(config: dict): + elif _is_windows: +@@ -2614,6 +2622,8 @@ try: if supports_systemd: systemd_start() + elif supports_freebsd: + freebsd_rc_start() elif _is_macos: launchd_start() - except UserSystemdUnavailableError as e: -@@ -2468,7 +2478,12 @@ def setup_gateway(config: dict): - except Exception as e: - print_error(f" Start failed: {e}") + elif _is_windows: +@@ -2631,6 +2641,8 @@ elif supports_service_manager: -- svc_name = "systemd" if supports_systemd else "launchd" -+ if supports_systemd: -+ svc_name = "systemd" + if supports_systemd: + svc_name = "systemd" + elif supports_freebsd: + svc_name = "FreeBSD rc.d" -+ else: -+ svc_name = "launchd" - if prompt_yes_no( - f" Install the gateway as a {svc_name} service? (runs in background, starts on boot)", - True, -@@ -2478,6 +2493,9 @@ def setup_gateway(config: dict): - did_install = False + elif _is_macos: + svc_name = "launchd" + else: +@@ -2645,6 +2657,9 @@ + started_inline = False if supports_systemd: installed_scope, did_install = install_linux_gateway_from_setup(force=False) + elif supports_freebsd: + freebsd_rc_install(force=False) + did_install = True - else: + elif _is_macos: launchd_install(force=False) did_install = True -@@ -2486,6 +2504,8 @@ def setup_gateway(config: dict): +@@ -2662,6 +2677,8 @@ try: if supports_systemd: systemd_start(system=installed_scope == "system") + elif supports_freebsd: + freebsd_rc_start() elif _is_macos: launchd_start() except UserSystemdUnavailableError as e: -@@ -2501,6 +2521,8 @@ def setup_gateway(config: dict): +@@ -2680,6 +2697,8 @@ print_info(" You can install later: hermes gateway install") if supports_systemd: print_info(" Or as a boot-time service: sudo hermes gateway install --system") + elif supports_freebsd: + print_info(" (FreeBSD rc.d install runs sysrc + service start with sudo.)") print_info(" Or run in foreground: hermes gateway") else: from hermes_constants import is_container diff --git a/misc/hermes-agent/files/patch-hermes__cli_uninstall.py b/misc/hermes-agent/files/patch-hermes__cli_uninstall.py index c971198c7ae1..e08d46052911 100644 --- a/misc/hermes-agent/files/patch-hermes__cli_uninstall.py +++ b/misc/hermes-agent/files/patch-hermes__cli_uninstall.py @@ -1,35 +1,35 @@ ---- hermes_cli/uninstall.py.orig 2026-05-06 08:29:55 UTC -+++ hermes_cli/uninstall.py -@@ -201,6 +201,32 @@ def uninstall_gateway_service(): +--- hermes_cli/uninstall.py.orig 2026-05-16 09:59:15 UTC ++++ hermes_cli/uninstall.py 2026-05-19 11:13:44.570355000 +0200 +@@ -224,6 +224,32 @@ + log_warn(f"Could not fully uninstall Windows gateway: {e}") except Exception as e: - log_warn(f"Could not remove launchd gateway service: {e}") - + log_warn(f"Could not check Windows gateway service: {e}") ++ + # 4. FreeBSD: stop the rc.d service and remove its rcvar from rc.conf. + # The rc script itself (/usr/local/etc/rc.d/hermes_gateway) is owned + # by the FreeBSD package — `pkg delete hermes-agent` removes it. + elif system == "FreeBSD": + try: + from hermes_cli.gateway import ( + supports_freebsd_rc, + FREEBSD_RC_SCRIPT_NAME, + FREEBSD_RC_VAR, + ) + if supports_freebsd_rc(): + sudo = [] if os.geteuid() == 0 else (["sudo"] if shutil.which("sudo") else None) + if sudo is None: + log_warn(f" Need root to disable {FREEBSD_RC_SCRIPT_NAME}. " + f"Run: sudo service {FREEBSD_RC_SCRIPT_NAME} stop && " + f"sudo sysrc -x {FREEBSD_RC_VAR}") + else: + subprocess.run(sudo + ["service", FREEBSD_RC_SCRIPT_NAME, "stop"], + capture_output=True, check=False) + subprocess.run(sudo + ["sysrc", "-x", FREEBSD_RC_VAR], + capture_output=True, check=False) + log_success(f"Stopped and disabled {FREEBSD_RC_SCRIPT_NAME}") + stopped_something = True + except Exception as e: + log_warn(f"Could not disable FreeBSD gateway service: {e}") -+ - return stopped_something + return stopped_something