git: f2cde2ee1b3e - main - misc/hermes-agent: update to 0.22.5

From: Olivier Cochard <olivier_at_FreeBSD.org>
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