git: f2cde2ee1b3e - main - misc/hermes-agent: update to 0.22.5
- Go to: [ bottom of page ] [ top of archives ] [ this month ]
Date: Mon, 26 Oct 2026 23:17:45 UTC
The branch main has been updated by olivier:
URL: https://cgit.FreeBSD.org/ports/commit/?id=f2cde2ee1b3e8cd3f75aaa0efa06abebe3f8f325
commit f2cde2ee1b3e8cd3f75aaa0efa06abebe3f8f325
Author: Olivier Cochard <olivier@FreeBSD.org>
AuthorDate: 2026-10-26 23:12:22 +0000
Commit: Olivier Cochard <olivier@FreeBSD.org>
CommitDate: 2026-10-26 23:14:08 +0000
misc/hermes-agent: update to 0.22.5
PR: 300106
Reported by: Ghislain Potier <ghislain.potier@gpotier.org>
---
misc/hermes-agent/Makefile | 63 ++++++++++++++--------
misc/hermes-agent/distinfo | 10 ++--
.../files/patch-agent_anthropic__adapter.py | 22 ++++----
.../files/patch-agent_azure__identity__adapter.py | 20 +++----
.../hermes-agent/files/patch-agent_skill__utils.py | 16 +++---
.../files/patch-hermes__cli_config__defaults.py | 12 ++---
.../files/patch-hermes__cli_gateway.py | 40 +++-----------
...atch-hermes__cli_gateway__runtime__discovery.py | 12 +++++
.../patch-hermes__cli_gateway__runtime__service.py | 14 +++++
.../patch-hermes__cli_gateway__setup__service.py | 30 +++++++++++
.../patch-hermes__cli_gateway__setup__wizard.py | 4 +-
.../files/patch-hermes__cli_main__agent__cmds.py | 4 +-
.../files/patch-hermes__cli_setup__platforms.py | 6 +--
.../files/patch-hermes__cli_uninstall.py | 8 +--
misc/hermes-agent/files/patch-pm_install.py | 14 +++++
misc/hermes-agent/files/patch-tools_lazy__deps.py | 13 -----
16 files changed, 166 insertions(+), 122 deletions(-)
diff --git a/misc/hermes-agent/Makefile b/misc/hermes-agent/Makefile
index 909a5d4607ec..18f8c71c24df 100644
--- a/misc/hermes-agent/Makefile
+++ b/misc/hermes-agent/Makefile
@@ -1,5 +1,5 @@
PORTNAME= hermes-agent
-PORTVERSION= 0.21.5
+PORTVERSION= 0.22.5
CATEGORIES= misc python
MASTER_SITES+= LOCAL/olivier:webdist
DISTFILES+= ${PORTNAME}-web-dist-${PORTVERSION}${EXTRACT_SUFX}:webdist
@@ -11,11 +11,13 @@ WWW= https://github.com/NousResearch/hermes-agent
LICENSE= MIT
LICENSE_FILE= ${WRKSRC}/LICENSE
-# Upstream splits deps into a small `dependencies` core plus lazy-installed
-# extras (tools/lazy_deps.py). The port ships the provider-specific extras
-# (anthropic, edge-tts, exa-py, fal-client, firecrawl-py, parallel-web) as
-# hard RUN_DEPENDS because patch-tools_lazy__deps.py turns lazy installs off
-# on FreeBSD -- pkg owns site-packages, so there is no runtime pip fallback.
+# Upstream splits deps into a small `dependencies` core plus optional extras
+# prepared by the bundled `pm` package manager (pm/, uv-backed; the old
+# tools/lazy_deps.py pip path is a retired shim). The port ships the
+# provider-specific extras (anthropic, edge-tts, exa-py, fal-client,
+# firecrawl-py, parallel-web) as hard RUN_DEPENDS because
+# patch-pm_install.py turns lazy installs off by default on FreeBSD -- pkg
+# owns site-packages, so there is no runtime pip/uv fallback.
#
# firecrawl-anydoc (upstream core since v2026.8.31, 0.2.4 as of this release)
# is deliberately NOT listed: no port exists, and tools/read_extract.py
@@ -25,7 +27,7 @@ LICENSE_FILE= ${WRKSRC}/LICENSE
RUN_DEPENDS= ${PYTHON_PKGNAMEPREFIX}anthropic>=0.87.0:misc/py-anthropic@${PY_FLAVOR} \
${PYTHON_PKGNAMEPREFIX}certifi>=2026.5.20:security/py-certifi@${PY_FLAVOR} \
${PYTHON_PKGNAMEPREFIX}croniter>=6.0.0:sysutils/py-croniter@${PY_FLAVOR} \
- ${PYTHON_PKGNAMEPREFIX}cryptography>=50.0.0:security/py-cryptography@${PY_FLAVOR} \
+ ${PYTHON_PKGNAMEPREFIX}cryptography>=50.0.1:security/py-cryptography@${PY_FLAVOR} \
${PYTHON_PKGNAMEPREFIX}edge-tts>=7.2.7:audio/py-edge-tts@${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} \
@@ -46,7 +48,7 @@ RUN_DEPENDS= ${PYTHON_PKGNAMEPREFIX}anthropic>=0.87.0:misc/py-anthropic@${PY_FLA
${PYTHON_PKGNAMEPREFIX}psutil>=7.2.2:sysutils/py-psutil@${PY_FLAVOR} \
${PYTHON_PKGNAMEPREFIX}ptyprocess>=0.7.0:sysutils/py-ptyprocess@${PY_FLAVOR} \
${PYTHON_PKGNAMEPREFIX}pydantic2>=2.13.4:devel/py-pydantic2@${PY_FLAVOR} \
- ${PYTHON_PKGNAMEPREFIX}pyjwt>=2.13.0:www/py-pyjwt@${PY_FLAVOR} \
+ ${PYTHON_PKGNAMEPREFIX}pyjwt>=2.14.0:www/py-pyjwt@${PY_FLAVOR} \
${PYTHON_PKGNAMEPREFIX}pysocks>0:net/py-pysocks@${PY_FLAVOR} \
${PYTHON_PKGNAMEPREFIX}python-dotenv>=1.2.2:www/py-python-dotenv@${PY_FLAVOR} \
${PYTHON_PKGNAMEPREFIX}python-multipart>=0.0.32:www/py-python-multipart@${PY_FLAVOR} \
@@ -58,16 +60,18 @@ RUN_DEPENDS= ${PYTHON_PKGNAMEPREFIX}anthropic>=0.87.0:misc/py-anthropic@${PY_FLA
${PYTHON_PKGNAMEPREFIX}socksio>0:net/py-socksio@${PY_FLAVOR} \
${PYTHON_PKGNAMEPREFIX}starlette>=1.3.1:www/py-starlette@${PY_FLAVOR} \
${PYTHON_PKGNAMEPREFIX}tenacity>=9.1.4:devel/py-tenacity@${PY_FLAVOR} \
- ${PYTHON_PKGNAMEPREFIX}urllib3>=2.7.0:net/py-urllib3@${PY_FLAVOR} \
+ ${PYTHON_PKGNAMEPREFIX}tomli-w>=1.2.0:textproc/py-tomli-w@${PY_FLAVOR} \
+ ${PYTHON_PKGNAMEPREFIX}truststore>=0.10.4:security/py-truststore@${PY_FLAVOR} \
+ ${PYTHON_PKGNAMEPREFIX}urllib3>=2.8.0:net/py-urllib3@${PY_FLAVOR} \
${PYTHON_PKGNAMEPREFIX}uvicorn>=0.41.0:www/py-uvicorn@${PY_FLAVOR} \
${PYTHON_PKGNAMEPREFIX}watchfiles>=0.20:devel/py-watchfiles@${PY_FLAVOR} \
${PYTHON_PKGNAMEPREFIX}websockets>=15.0.1:devel/py-websockets@${PY_FLAVOR}
-USES= python:3.11+,run shebangfix
+USES= python:3.11+ shebangfix
USE_GITHUB= yes
GH_ACCOUNT= NousResearch
GH_PROJECT= hermes-agent
-GH_TAGNAME= v2026.9.24
+GH_TAGNAME= v0.22.5
USE_RC_SUBR= hermes_dashboard hermes_gateway
@@ -105,14 +109,20 @@ PLIST_SUB+= HERMES_LIBDIR=${HERMES_LIBDIR:S,^${PREFIX}/,,}
# 1. Extract the upstream source tarball:
# tar xzf ${DISTDIR}/NousResearch-${PORTNAME}-${PORTVERSION}-${GH_TAGNAME}_GH0.tar.gz
# cd ${PORTNAME}-*/web
-# 2. Install deps and build the SPA:
-# npm install --no-audit --no-fund
-# npm run build
+# 2. Install deps and build the SPA (the repo root is an npm-workspaces
+# monorepo as of 0.22.5; --ignore-scripts skips the ui-tui node-pty and
+# electron downloads that the dashboard does not need -- bun works too:
+# `bun install --ignore-scripts` + `(cd web && bun run build)` with a
+# node->bun PATH shim, since the build script spawns node):
+# npm install --ignore-scripts --no-audit --no-fund
+# npm run build -w web
# This writes the bundle to ../hermes_cli/web_dist/.
-# (v2026.8.31 dropped web/package-lock.json, so `npm install` is the
-# only option -- `npm ci` requires a lockfile. Earlier releases also
-# needed --engine-strict=false for a too-narrow npm range in
-# package.json; the engines gate is gone as of this release.)
+# (web/package-lock.json is gone but the root lockfile is back as of
+# 0.22.5. The build also trips TS18048 in web/vite.config.ts under the
+# pinned typescript 6 (.filter is typed optional though populated at
+# runtime): rewrite `preset.rolldown.filter.code = /re/;` as
+# `preset.rolldown.filter = { ...preset.rolldown.filter, code: /re/ };`
+# (build-only, behaviour-identical). update-hermes.sh applies this.)
# 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 \
@@ -130,11 +140,12 @@ PLIST_SUB+= HERMES_LIBDIR=${HERMES_LIBDIR:S,^${PREFIX}/,,}
#
# This list has the same hazard and no glob to save it: a package added
# upstream must be added here or the app dies at import. On every bump,
-# diff the top-level dirs holding __init__.py against this list (0.21.5
-# added hermes_platform). evals/ and tests/ are dev-only, deliberately
-# excluded.
+# diff the top-level dirs holding __init__.py against this list (0.22.5
+# added pm, upstream's uv-backed dependency manager; it replaces the retired
+# tools/lazy_deps.py pip path). evals/ and tests/ are dev-only,
+# deliberately excluded.
HERMES_PKGS= acp_adapter agent cron gateway hermes_cli hermes_platform \
- plugins providers tools tui_gateway
+ plugins pm providers tools tui_gateway
SHEBANG_FILES= *.py
@@ -190,6 +201,14 @@ do-install:
-e 's|%%ENTRY_MODULE%%|acp_adapter.entry|g' \
-e 's|%%ENTRY_FUNC%%|main|g' \
${FILESDIR}/wrapper.in > ${STAGEDIR}${PREFIX}/bin/hermes-acp
+# Since 0.22.5 the version comes from install-stamp.json beside pm/;
+# without it hermes reports 0.0.0. GIT_DIR keeps the script from
+# reading the ports tree's git state when WRKDIR is inside it.
+ cd ${WRKSRC} && ${SETENV} GIT_DIR=/nonexistent ${PYTHON_CMD} \
+ scripts/write_install_stamp.py \
+ -o ${STAGEDIR}${HERMES_LIBDIR}/install-stamp.json \
+ --commit ${GH_TAGNAME} --base-version ${PORTVERSION} \
+ --distance 0 --source freebsd-ports --update-mechanism external
${MKDIR} ${STAGEDIR}${DATADIR}
cd ${WRKSRC} && ${COPYTREE_SHARE} skills ${STAGEDIR}${DATADIR}
cd ${WRKSRC} && ${COPYTREE_SHARE} optional-skills ${STAGEDIR}${DATADIR}
diff --git a/misc/hermes-agent/distinfo b/misc/hermes-agent/distinfo
index b22cc301b390..75b378d5a0e7 100644
--- a/misc/hermes-agent/distinfo
+++ b/misc/hermes-agent/distinfo
@@ -1,5 +1,5 @@
-TIMESTAMP = 1790724236
-SHA256 (hermes-agent-web-dist-0.21.5.tar.gz) = 12acf5c53eaab4bd312fc118dbfaf9f490a2324a6728e6123b4f9b24768dbd83
-SIZE (hermes-agent-web-dist-0.21.5.tar.gz) = 1545955
-SHA256 (NousResearch-hermes-agent-0.21.5-v2026.9.24_GH0.tar.gz) = 15b15ce4e6ec8ea424a081823709d1e17f0943e7b42b59597d24ebb94cbd1742
-SIZE (NousResearch-hermes-agent-0.21.5-v2026.9.24_GH0.tar.gz) = 75257742
+TIMESTAMP = 1793048705
+SHA256 (hermes-agent-web-dist-0.22.5.tar.gz) = adf298117afe3b3995d32e98a3f1717b75937cfc24926f7ad629a329adc7e4c6
+SIZE (hermes-agent-web-dist-0.22.5.tar.gz) = 1549280
+SHA256 (NousResearch-hermes-agent-0.22.5-v0.22.5_GH0.tar.gz) = a8f67e5ec14fea681acef6cc7ff1553f002bb6101fd2df22bfdd4b7eecc54eb2
+SIZE (NousResearch-hermes-agent-0.22.5-v0.22.5_GH0.tar.gz) = 87135743
diff --git a/misc/hermes-agent/files/patch-agent_anthropic__adapter.py b/misc/hermes-agent/files/patch-agent_anthropic__adapter.py
index 09ddb26194bf..746cf7daf20d 100644
--- a/misc/hermes-agent/files/patch-agent_anthropic__adapter.py
+++ b/misc/hermes-agent/files/patch-agent_anthropic__adapter.py
@@ -1,6 +1,6 @@
---- agent/anthropic_adapter.py.orig 2026-09-24 10:08:47 UTC
+--- agent/anthropic_adapter.py.orig 2026-10-23 02:24:27 UTC
+++ agent/anthropic_adapter.py
-@@ -338,9 +338,37 @@ def _attribution_headers() -> Dict[str, str]:
+@@ -375,9 +375,37 @@ def _attribution_headers() -> dict[str, str]:
}
@@ -40,14 +40,12 @@
read = timeout if (isinstance(timeout, (int, float)) and timeout > 0) else 900.0
return Timeout(timeout=float(read), connect=10.0)
-@@ -372,7 +400,9 @@ def _build_anthropic_client_with_bearer_hook(
- normalize_proxy_env_vars()
- from agent.azure_identity_adapter import build_bearer_http_client
- normalized_base_url, kwargs = _base_client_kwargs(base_url, timeout)
-- kwargs["http_client"] = build_bearer_http_client(token_provider, timeout=kwargs["timeout"])
-+ kwargs["http_client"] = build_bearer_http_client(
-+ token_provider, httpx_mod=_sdk_httpx(), timeout=kwargs["timeout"]
-+ )
+@@ -414,7 +442,7 @@ def _build_anthropic_client_with_bearer_hook(
+ # auth header follows each mint, the OAuth identity below the first one: a key_cmd switching
+ # between OAuth and a Console key mid-session needs a client rebuild.
+ kwargs["http_client"] = build_bearer_http_client(
+- token_provider, timeout=kwargs["timeout"],
++ token_provider, httpx_mod=_sdk_httpx(), timeout=kwargs["timeout"],
+ sends_as_api_key=(lambda token: not _is_oauth_token(token)) if is_native_anthropic_host(base_url) else None,
+ )
kwargs["auth_token"] = "entra-id-bearer-via-http-hook"
- betas = _common_betas_for_base_url(normalized_base_url, drop_context_1m_beta=drop_context_1m_beta)
- from agent.anthropic_credentials import anthropic_route_is_oauth
diff --git a/misc/hermes-agent/files/patch-agent_azure__identity__adapter.py b/misc/hermes-agent/files/patch-agent_azure__identity__adapter.py
index 1f9ec5da5a2a..85baacbf63ad 100644
--- a/misc/hermes-agent/files/patch-agent_azure__identity__adapter.py
+++ b/misc/hermes-agent/files/patch-agent_azure__identity__adapter.py
@@ -1,17 +1,19 @@
---- agent/azure_identity_adapter.py.orig 2026-09-24 10:08:47 UTC
+--- agent/azure_identity_adapter.py.orig 2026-10-26 16:08:51 UTC
+++ agent/azure_identity_adapter.py
-@@ -304,14 +304,21 @@ def _strip_auth_headers(request: Any) -> None:
- request.headers.pop(header_name, None)
+@@ -311,16 +311,23 @@
--def build_bearer_http_client(token_provider: Callable[[], str], **httpx_kwargs: Any) -> Any:
-+def build_bearer_http_client(token_provider: Callable[[], str], *, httpx_mod: Any = None,
-+ **httpx_kwargs: Any) -> Any:
+ def build_bearer_http_client(
+- token_provider: Callable[[], str], *, sends_as_api_key: Callable[[str], bool] | None = None, **httpx_kwargs: Any
++ token_provider: Callable[[], str], *, httpx_mod: Any = None,
++ sends_as_api_key: Callable[[str], bool] | None = None, **httpx_kwargs: Any
+ ) -> Any:
"""``httpx.Client`` minting a fresh Entra bearer JWT per outbound request. The Anthropic SDK computes
``Authorization`` once at construction, so per-request refresh needs a ``request`` hook: mint (cheap —
azure-identity caches), strip pre-set auth headers, set ``Authorization: Bearer``. ``httpx_kwargs`` are
-- forwarded verbatim (``timeout``, ``transport``...)."""
-+ forwarded verbatim (``timeout``, ``transport``...).
+ forwarded verbatim (``timeout``, ``transport``...). ``sends_as_api_key(token)`` True puts the minted
+- token in ``x-api-key`` instead (a ``key_cmd`` printing a native Anthropic Console key)."""
++ token in ``x-api-key`` instead (a ``key_cmd`` printing a native Anthropic Console key).
+
+ ``httpx_mod`` lets the caller pass the httpx flavour its SDK is built against (anthropic 1.x
+ vendors ``httpx2``); the client object must come from that same module or the SDK rejects it.
@@ -23,5 +25,5 @@
+ import httpx as httpx_mod # type: ignore[no-redef]
+ httpx = httpx_mod
- def _inject_bearer(request: "httpx.Request") -> None:
+ def _inject_auth(request: httpx.Request) -> None:
try:
diff --git a/misc/hermes-agent/files/patch-agent_skill__utils.py b/misc/hermes-agent/files/patch-agent_skill__utils.py
index db34f2b4cf7c..bce7a6803c15 100644
--- a/misc/hermes-agent/files/patch-agent_skill__utils.py
+++ b/misc/hermes-agent/files/patch-agent_skill__utils.py
@@ -1,16 +1,12 @@
---- agent/skill_utils.py.orig 2026-09-24 10:08:47 UTC
+--- agent/skill_utils.py.orig 2026-10-26 16:08:51 UTC
+++ agent/skill_utils.py
-@@ -135,7 +135,12 @@ def skill_matches_platform_list(platforms: Any) -> boo
+@@ -97,7 +97,8 @@
+ for platform in platforms:
+ normalized = str(platform).lower().strip()
mapped = PLATFORM_MAP.get(normalized, normalized)
- # Termux is a Linux userland on Android: accept linux-tagged skills
- # whether sys.platform is "linux" (pre-3.13) or "android" (3.13+).
-- if sys.platform.startswith(mapped) or (running_in_termux and mapped in ("linux", "termux", "android")):
+- if current.startswith(mapped):
+ # FreeBSD is POSIX-compatible: accept linux-tagged skills too.
-+ if (
-+ sys.platform.startswith(mapped)
-+ or (running_in_termux and mapped in ("linux", "termux", "android"))
-+ or (sys.platform.startswith("freebsd") and mapped == "linux")
-+ ):
++ if current.startswith(mapped) or (sys.platform.startswith("freebsd") and mapped == "linux"):
return True
return False
diff --git a/misc/hermes-agent/files/patch-hermes__cli_config__defaults.py b/misc/hermes-agent/files/patch-hermes__cli_config__defaults.py
index b104283b9987..2adc6f85686f 100644
--- a/misc/hermes-agent/files/patch-hermes__cli_config__defaults.py
+++ b/misc/hermes-agent/files/patch-hermes__cli_config__defaults.py
@@ -1,16 +1,16 @@
---- hermes_cli/config_defaults.py.orig 2026-09-24 10:08:47 UTC
+--- hermes_cli/config_defaults.py.orig 2026-10-23 02:24:27 UTC
+++ hermes_cli/config_defaults.py
-@@ -4,7 +4,9 @@ docs of config.yaml.
+@@ -4,7 +4,9 @@ Pure-data leaf module — must not import from hermes_cl
docs of config.yaml.
"""
+import sys
+
- def _aux(timeout, *, reasoning_effort=True, **extra):
- """Standard auxiliary-task model block (see DEFAULT_CONFIG["auxiliary"]).
-
-@@ -1761,8 +1763,10 @@ DEFAULT_CONFIG = {
+ #: Image every container terminal backend (docker/modal/daytona, the singularity plugin) uses unless the
+ #: user pins one. LEGACY_SANDBOX_IMAGES are the plain defaults that preceded the desktop stack
+ #: (the 3.14 pin shipped between the two without a migration); a saved config still holding one
+@@ -1807,8 +1809,10 @@ DEFAULT_CONFIG = {
"acked_advisories": [],
# Lazy-install opt-in backend packages from PyPI when a backend that needs them is first
# enabled (e.g. `elevenlabs`). False = require explicit pip install for everything beyond
diff --git a/misc/hermes-agent/files/patch-hermes__cli_gateway.py b/misc/hermes-agent/files/patch-hermes__cli_gateway.py
index 2c19edb6f9fa..07057d7cd311 100644
--- a/misc/hermes-agent/files/patch-hermes__cli_gateway.py
+++ b/misc/hermes-agent/files/patch-hermes__cli_gateway.py
@@ -1,34 +1,6 @@
---- hermes_cli/gateway.py.orig 2026-09-24 10:08:47 UTC
+--- hermes_cli/gateway.py.orig 2026-10-26 16:08:51 UTC
+++ hermes_cli/gateway.py
-@@ -2710,7 +2710,8 @@ def ensure_gateway_service(context: str = "setup") ->
- return False
-
- supports_systemd = supports_systemd_services()
-- if not (supports_systemd or is_macos() or is_windows()):
-+ supports_rc = supports_freebsd_rc()
-+ if not (supports_systemd or supports_rc or is_macos() or is_windows()):
- print_info(" No supported service manager found on this host.")
- print_info(" Run the gateway in the foreground with: hermes gateway")
- return False
-@@ -2728,6 +2729,8 @@ def ensure_gateway_service(context: str = "setup") ->
- print_info(" Installing the gateway background service ...")
- if supports_systemd:
- systemd_install(force=False, non_interactive=True)
-+ elif supports_rc:
-+ freebsd_rc_install(force=False, non_interactive=True)
- elif is_macos():
- launchd_install(force=False)
- else:
-@@ -2736,6 +2739,8 @@ def ensure_gateway_service(context: str = "setup") ->
- return True
- if supports_systemd:
- systemd_start()
-+ elif supports_rc:
-+ freebsd_rc_start()
- elif is_macos():
- launchd_start()
- else:
-@@ -3459,6 +3464,29 @@ def systemd_status(deep: bool = False, system: bool =
+@@ -3921,6 +3921,29 @@
# =============================================================================
@@ -58,7 +30,7 @@
# Launchd (macOS)
# =============================================================================
-@@ -4317,6 +4345,8 @@ def _service_backend(*, windows: bool = True) -> str |
+@@ -4807,6 +4830,8 @@
predicate order every subcommand routes on. ``windows=False`` never probes ``is_windows()``."""
if supports_systemd_services():
return "systemd"
@@ -67,8 +39,8 @@
if is_macos():
return "launchd"
if windows and is_windows():
-@@ -4329,6 +4359,8 @@ def _service_call(backend: str, verb: str, system: boo
- can monkeypatch them; only systemd takes a scope, and ``system=None`` omits it (wizard restart)."""
+@@ -4820,6 +4845,8 @@
+ Each systemd/launchd verb refuses a definition pinning another HERMES_HOME itself."""
if backend == "windows":
return getattr(_gw_windows(), verb)()
+ if backend == "freebsd_rc":
@@ -76,7 +48,7 @@
if backend == "launchd":
return globals()[f"launchd_{verb}"]()
fn = globals()[f"systemd_{verb}"]
-@@ -4506,6 +4538,8 @@ def _installed_service_kind_for(windows) -> str | None
+@@ -4997,6 +5024,8 @@
(a thunk so it runs last, like every caller's original ladder), else None."""
if _systemd_unit_installed():
return "systemd"
diff --git a/misc/hermes-agent/files/patch-hermes__cli_gateway__runtime__discovery.py b/misc/hermes-agent/files/patch-hermes__cli_gateway__runtime__discovery.py
new file mode 100644
index 000000000000..50a3733c9d63
--- /dev/null
+++ b/misc/hermes-agent/files/patch-hermes__cli_gateway__runtime__discovery.py
@@ -0,0 +1,12 @@
+--- hermes_cli/gateway_runtime_discovery.py.orig 2026-10-26 21:56:11 UTC
++++ hermes_cli/gateway_runtime_discovery.py
+@@ -109,7 +109,8 @@ def socket_peer_uid(sock: socket.socket) -> int | None
+ if hasattr(socket, "SO_PEERCRED"):
+ import struct
+ return struct.unpack("3i", sock.getsockopt(socket.SOL_SOCKET, socket.SO_PEERCRED, 12))[1]
+- if sys.platform != "darwin":
++ # FreeBSD libc has getpeereid(3) too.
++ if sys.platform != "darwin" and not sys.platform.startswith("freebsd"):
+ return None
+ import ctypes
+ uid, gid = ctypes.c_uint(), ctypes.c_uint()
diff --git a/misc/hermes-agent/files/patch-hermes__cli_gateway__runtime__service.py b/misc/hermes-agent/files/patch-hermes__cli_gateway__runtime__service.py
new file mode 100644
index 000000000000..cf8188ef3ae4
--- /dev/null
+++ b/misc/hermes-agent/files/patch-hermes__cli_gateway__runtime__service.py
@@ -0,0 +1,14 @@
+--- hermes_cli/gateway_runtime_service.py.orig 2026-10-26 21:50:19 UTC
++++ hermes_cli/gateway_runtime_service.py
+@@ -261,8 +261,10 @@ def discover_existing_gateway_service(home: Path, *, d
+ def discover_existing_gateway_service(home: Path, *, deadline: float) -> ExistingService | None:
+ from hermes_cli.service_manager import _s6_running
+
++ # FreeBSD: sys.platform carries the major version; no supervisor probe, unmanaged start.
++ platform = "freebsd" if sys.platform.startswith("freebsd") else sys.platform
+ backend = _s6 if _s6_running() else {"linux": _systemd, "darwin": _launchd, "win32": _windows,
+- "android": _no_supervisor}.get(sys.platform)
++ "android": _no_supervisor, "freebsd": _no_supervisor}.get(platform)
+ if backend is None:
+ raise RuntimeStartError("unsupported_supervisor_platform")
+ try:
diff --git a/misc/hermes-agent/files/patch-hermes__cli_gateway__setup__service.py b/misc/hermes-agent/files/patch-hermes__cli_gateway__setup__service.py
new file mode 100644
index 000000000000..e27df253c265
--- /dev/null
+++ b/misc/hermes-agent/files/patch-hermes__cli_gateway__setup__service.py
@@ -0,0 +1,30 @@
+--- hermes_cli/gateway_setup_service.py.orig 2026-10-26 16:08:51 UTC
++++ hermes_cli/gateway_setup_service.py
+@@ -55,7 +55,8 @@
+ gw.print_info("Run hermes gateway run as the container main process; use a Docker restart policy for persistence.")
+ return False
+ supports_systemd = gw.supports_systemd_services()
+- if not (supports_systemd or gw.is_macos() or gw.is_windows()):
++ supports_rc = gw.supports_freebsd_rc()
++ if not (supports_systemd or supports_rc or gw.is_macos() or gw.is_windows()):
+ gw.print_info("No supported service manager found. Run: hermes gateway run")
+ return False
+ try:
+@@ -75,6 +76,8 @@
+ return False
+ if supports_systemd:
+ gw.systemd_install(force=False, non_interactive=True)
++ elif supports_rc:
++ gw.freebsd_rc_install(force=False, non_interactive=True)
+ elif gw.is_macos():
+ gw.launchd_install(force=False)
+ else:
+@@ -86,6 +89,8 @@
+ record_service_choice("install", config)
+ if supports_systemd:
+ gw.systemd_start()
++ elif supports_rc:
++ gw.freebsd_rc_start()
+ elif gw.is_macos():
+ gw.launchd_start()
+ else:
diff --git a/misc/hermes-agent/files/patch-hermes__cli_gateway__setup__wizard.py b/misc/hermes-agent/files/patch-hermes__cli_gateway__setup__wizard.py
index a6587b2ede5e..e5601c7a34bf 100644
--- a/misc/hermes-agent/files/patch-hermes__cli_gateway__setup__wizard.py
+++ b/misc/hermes-agent/files/patch-hermes__cli_gateway__setup__wizard.py
@@ -1,6 +1,6 @@
---- hermes_cli/gateway_setup_wizard.py.orig 2026-09-24 10:08:47 UTC
+--- hermes_cli/gateway_setup_wizard.py.orig 2026-10-23 02:24:27 UTC
+++ hermes_cli/gateway_setup_wizard.py
-@@ -776,7 +776,7 @@ _WIZARD_BANNER = (
+@@ -392,7 +392,7 @@ _WIZARD_BANNER = (
)
diff --git a/misc/hermes-agent/files/patch-hermes__cli_main__agent__cmds.py b/misc/hermes-agent/files/patch-hermes__cli_main__agent__cmds.py
index 3582872db02d..c9e3d5633d2e 100644
--- a/misc/hermes-agent/files/patch-hermes__cli_main__agent__cmds.py
+++ b/misc/hermes-agent/files/patch-hermes__cli_main__agent__cmds.py
@@ -1,6 +1,6 @@
---- hermes_cli/main_agent_cmds.py.orig 2026-09-24 10:08:47 UTC
+--- hermes_cli/main_agent_cmds.py.orig 2026-10-23 02:24:27 UTC
+++ hermes_cli/main_agent_cmds.py
-@@ -168,6 +168,15 @@ def cmd_skills(args):
+@@ -186,6 +186,15 @@ def cmd_skills(args):
def cmd_skills(args):
diff --git a/misc/hermes-agent/files/patch-hermes__cli_setup__platforms.py b/misc/hermes-agent/files/patch-hermes__cli_setup__platforms.py
index f1d25c492482..a25018f208d3 100644
--- a/misc/hermes-agent/files/patch-hermes__cli_setup__platforms.py
+++ b/misc/hermes-agent/files/patch-hermes__cli_setup__platforms.py
@@ -1,6 +1,6 @@
---- hermes_cli/setup_platforms.py.orig 2026-09-24 10:08:47 UTC
+--- hermes_cli/setup_platforms.py.orig 2026-10-23 02:24:27 UTC
+++ hermes_cli/setup_platforms.py
-@@ -290,6 +290,7 @@ def _restart_running_gateway(any_messaging: bool, supp
+@@ -258,6 +258,7 @@ def _restart_running_gateway(any_messaging: bool, supp
from hermes_cli.gateway import (
systemd_restart, launchd_restart, UserSystemdUnavailableError, SystemScopeRequiresRootError,
_system_scope_wizard_would_need_root, _print_system_scope_remediation,
@@ -8,7 +8,7 @@
)
import platform as _platform
if supports_systemd and _system_scope_wizard_would_need_root():
-@@ -300,6 +301,8 @@ def _restart_running_gateway(any_messaging: bool, supp
+@@ -268,6 +269,8 @@ def _restart_running_gateway(any_messaging: bool, supp
try:
if supports_systemd:
systemd_restart()
diff --git a/misc/hermes-agent/files/patch-hermes__cli_uninstall.py b/misc/hermes-agent/files/patch-hermes__cli_uninstall.py
index 6e2827f4d4d7..7197f022bdf6 100644
--- a/misc/hermes-agent/files/patch-hermes__cli_uninstall.py
+++ b/misc/hermes-agent/files/patch-hermes__cli_uninstall.py
@@ -1,7 +1,7 @@
---- hermes_cli/uninstall.py.orig 2026-09-24 10:08:47 UTC
+--- hermes_cli/uninstall.py.orig 2026-10-23 02:24:27 UTC
+++ hermes_cli/uninstall.py
-@@ -229,6 +229,16 @@ def _remove_launchd_gateway() -> bool:
- return True
+@@ -334,6 +334,16 @@ def _remove_launchd_gateway() -> bool:
+ return bool(plists)
+def _remove_freebsd_gateway() -> bool:
@@ -17,7 +17,7 @@
def _remove_windows_gateway() -> bool:
"""Windows: uninstall Scheduled Task + Startup-folder entry via ``gateway_windows`` (it owns
schtasks /Delete, the .cmd unlink and stopping the detached pythonw gateway)."""
-@@ -252,6 +262,7 @@ _GATEWAY_SERVICE_REMOVERS = {
+@@ -357,6 +367,7 @@ _GATEWAY_SERVICE_REMOVERS = {
_GATEWAY_SERVICE_REMOVERS = {
"Linux": (_remove_systemd_gateway, "Could not check systemd gateway services"),
"Darwin": (_remove_launchd_gateway, "Could not remove launchd gateway service"),
diff --git a/misc/hermes-agent/files/patch-pm_install.py b/misc/hermes-agent/files/patch-pm_install.py
new file mode 100644
index 000000000000..e9864ac739a2
--- /dev/null
+++ b/misc/hermes-agent/files/patch-pm_install.py
@@ -0,0 +1,14 @@
+--- pm/install.py.orig 2026-10-26 16:08:51 UTC
++++ pm/install.py
+@@ -188,7 +188,10 @@
+ # The normal loader falls back to defaults on invalid YAML. A broken
+ # security policy must not grant permission to acquire dependencies.
+ require_readable_config_before_write()
+- return cfg_get(load_config_readonly(), "security", "allow_lazy_installs", default=True) is True
++ # FreeBSD packages own their deps; users can still opt in.
++ import sys
++ _default = not sys.platform.startswith("freebsd")
++ return cfg_get(load_config_readonly(), "security", "allow_lazy_installs", default=_default) is True
+ except Exception:
+ return False
+
diff --git a/misc/hermes-agent/files/patch-tools_lazy__deps.py b/misc/hermes-agent/files/patch-tools_lazy__deps.py
deleted file mode 100644
index c69ce5af022e..000000000000
--- a/misc/hermes-agent/files/patch-tools_lazy__deps.py
+++ /dev/null
@@ -1,13 +0,0 @@
---- tools/lazy_deps.py.orig 2026-09-24 10:08:47 UTC
-+++ tools/lazy_deps.py
-@@ -330,7 +330,9 @@ def _allow_lazy_installs() -> bool:
- with contextlib.suppress(Exception):
- from hermes_cli.config import load_config
- cfg = load_config()
-- if cfg is not None and not bool((cfg.get("security") or {}).get("allow_lazy_installs", True)):
-+ # FreeBSD packages own their deps; users can still opt in.
-+ _lazy_default = not sys.platform.startswith("freebsd")
-+ if cfg is not None and not bool((cfg.get("security") or {}).get("allow_lazy_installs", _lazy_default)):
- return False
- if os.environ.get("HERMES_DISABLE_LAZY_INSTALLS") == "1":
- return _lazy_install_target() is not None