diff --git a/misc/hermes-agent/Makefile b/misc/hermes-agent/Makefile index 37576d6099c2..c691cc4a8709 100644 --- a/misc/hermes-agent/Makefile +++ b/misc/hermes-agent/Makefile @@ -1,167 +1,166 @@ PORTNAME= hermes-agent -PORTVERSION= 0.14.0 +PORTVERSION= 0.15.0 CATEGORIES= misc python 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 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.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.133.1:www/py-fastapi@${PY_FLAVOR} \ ${PYTHON_PKGNAMEPREFIX}fire>=0.7.0:devel/py-fire@${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.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.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.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.41.0:www/py-uvicorn@${PY_FLAVOR} USES= python:3.11+,run shebangfix USE_GITHUB= yes GH_ACCOUNT= NousResearch GH_PROJECT= hermes-agent -GH_TAGNAME= v2026.5.16 +GH_TAGNAME= v2026.7.7.2 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/. 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 ${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-${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-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. # 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 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" # 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|%%DATADIR%%|${DATADIR}|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|%%DATADIR%%|${DATADIR}|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|%%DATADIR%%|${DATADIR}|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 f18c2c688132..c8be9a9685e5 100644 --- a/misc/hermes-agent/distinfo +++ b/misc/hermes-agent/distinfo @@ -1,5 +1,5 @@ -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 +TIMESTAMP = 1784325231 +SHA256 (hermes-agent-web-dist-0.15.0.tar.gz) = 4a74b4d3a3d4c2e4eaa5f8750627ed6a5ddfa3c7e403b16f5b84e360302235da +SIZE (hermes-agent-web-dist-0.15.0.tar.gz) = 1453377 +SHA256 (NousResearch-hermes-agent-0.15.0-v2026.7.7.2_GH0.tar.gz) = f5d1022eed3763a768cf7b0f0844831f0170a35f54eb8d18223f2e93f503025e +SIZE (NousResearch-hermes-agent-0.15.0-v2026.7.7.2_GH0.tar.gz) = 64174593 diff --git a/misc/hermes-agent/files/patch-agent_skill__utils.py b/misc/hermes-agent/files/patch-agent_skill__utils.py new file mode 100644 index 000000000000..907292438bff --- /dev/null +++ b/misc/hermes-agent/files/patch-agent_skill__utils.py @@ -0,0 +1,12 @@ +--- agent/skill_utils.py.orig 2026-07-08 03:11:08 UTC ++++ agent/skill_utils.py +@@ -192,6 +192,9 @@ def skill_matches_platform(frontmatter: Dict[str, Any] + mapped = PLATFORM_MAP.get(normalized, normalized) + if current.startswith(mapped): + return True ++ # FreeBSD is POSIX-compatible; accept linux-tagged skills. ++ if current.startswith("freebsd") and mapped == "linux": ++ return True + # Termux runs a Linux userland on Android. Accept linux-tagged + # skills regardless of whether sys.platform is "linux" (pre-3.13 + # Termux) or "android" (Python 3.13+ Termux, and any other diff --git a/misc/hermes-agent/files/patch-hermes__cli_config.py b/misc/hermes-agent/files/patch-hermes__cli_config.py index 9dca8b0bcfc9..517d341280d1 100644 --- a/misc/hermes-agent/files/patch-hermes__cli_config.py +++ b/misc/hermes-agent/files/patch-hermes__cli_config.py @@ -1,19 +1,15 @@ ---- 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 @@ +--- hermes_cli/config.py.orig 2026-07-08 03:11:08 UTC ++++ hermes_cli/config.py +@@ -2554,8 +2554,10 @@ DEFAULT_CONFIG = { + # their TTS provider). Set to false to require explicit # ``pip install`` for everything beyond the base set — appropriate # for restricted networks, audited environments, or air-gapped - # systems where any runtime install is unacceptable. +- # 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, ++ # systems where any runtime install is unacceptable. Defaults to ++ # False on FreeBSD: the package manager owns the site-packages tree, ++ # and runtime pip writes there create pkg-invisible files. ++ "allow_lazy_installs": not sys.platform.startswith("freebsd"), }, "cron": { diff --git a/misc/hermes-agent/files/patch-hermes__cli_gateway.py b/misc/hermes-agent/files/patch-hermes__cli_gateway.py index d4e6c7ea0332..75ae8f22707c 100644 --- a/misc/hermes-agent/files/patch-hermes__cli_gateway.py +++ b/misc/hermes-agent/files/patch-hermes__cli_gateway.py @@ -1,275 +1,287 @@ ---- 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 +--- hermes_cli/gateway.py.orig 2026-07-08 03:11:08 UTC ++++ hermes_cli/gateway.py +@@ -1638,6 +1638,45 @@ def supports_systemd_services() -> bool: + return True + + ++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 is_freebsd() -> bool: -+ return sys.platform.startswith('freebsd') ++ 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. ++# Privilege escalators, in preference order. sudo(8) is preferred so behavior ++# matches the Linux path used elsewhere in the codebase; doas(1) is the ++# common lightweight alternative on FreeBSD. ++_FREEBSD_PRIV_ESCALATORS = ("sudo", "doas") + -+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 _freebsd_privilege_escalator() -> str | None: ++ """Return the first available escalator command name, or None.""" ++ for name in _FREEBSD_PRIV_ESCALATORS: ++ if shutil.which(name) is not None: ++ return name ++ return None + + +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.""" ++ """Return True only when running on FreeBSD, the port-installed rc.d ++ script exists, AND the caller has a viable path to root (already root, or ++ a privilege escalator like sudo/doas is on PATH). Without one, callers ++ cannot drive service(8) or sysrc(8), so the dispatcher falls through to ++ the generic "not supported" branch and the user can still run ++ `hermes gateway run` in the foreground.""" + if not is_freebsd(): + return False + if shutil.which("service") is None: + return False -+ return FREEBSD_RC_SCRIPT_PATH.exists() ++ if not FREEBSD_RC_SCRIPT_PATH.exists(): ++ return False ++ return _freebsd_is_root() or _freebsd_privilege_escalator() is not None ++ ++ + def is_macos() -> bool: + return sys.platform == "darwin" + +@@ -4405,6 +4444,129 @@ def launchd_status(deep: bool = False): + + + # ============================================================================= ++# FreeBSD rc.d service (port-installed hermes_gateway script) ++# ============================================================================= ++# ++# The rc.d script itself is shipped by misc/hermes-agent; the CLI only flips ++# rcvar via sysrc(8) and drives lifecycle via service(8). ``system=`` is ++# accepted for API parity with systemd_* helpers but is a no-op — rc.d is ++# inherently system-scoped. + + +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').""" ++ """Run *cmd* directly when root; otherwise prepend the first available ++ privilege escalator (sudo, then doas). When none is available, print the ++ command for the user to run manually and return False. Returns True on ++ success.""" + 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}") ++ print(f"✗ Failed to {action} {FREEBSD_RC_SCRIPT_NAME}: exit {e.returncode}") + return False + -+ if shutil.which("sudo") is None: ++ escalator = _freebsd_privilege_escalator() ++ if escalator is None: + print(f" Run as root: {' '.join(cmd)}") + return False + -+ sudo_cmd = ["sudo"] + cmd + try: -+ subprocess.run(sudo_cmd, check=True) - return True ++ subprocess.run([escalator] + cmd, check=True) ++ return True + except subprocess.CalledProcessError as e: -+ print(f"✗ Failed to {action} hermes_gateway: exit {e.returncode}") ++ print(f"✗ Failed to {action} {FREEBSD_RC_SCRIPT_NAME}: 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) ++def freebsd_rc_install( ++ force: bool = False, ++ system: bool = False, ++ run_as_user: str | None = None, ++ enable_on_startup: bool = True, ++ non_interactive: bool = False, ++): ++ """Enable hermes_gateway in /etc/rc.conf. Does NOT start — dispatcher ++ starts via freebsd_rc_start when the user opts in.""" ++ del force, system, enable_on_startup, non_interactive # dispatcher parity + + 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( ++ _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 ++ """Stop the gateway and remove its rcvar. Leaves the rc.d script in place ++ (owned by pkg).""" ++ del system + 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?") ++ 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: ++ escalator = _freebsd_privilege_escalator() or "sudo" ++ print(f" {escalator} service {FREEBSD_RC_SCRIPT_NAME} start") ++ print(f" {escalator} sysrc {FREEBSD_RC_VAR}=YES # start at boot") + + ++# ============================================================================= + # Gateway Runner # ============================================================================= - # Service Configuration - # ============================================================================= -@@ -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) + +@@ -6535,6 +6697,18 @@ def _gateway_command_inner(args): + ) + if start_now: + systemd_start(system=system) + elif supports_freebsd_rc(): -+ freebsd_rc_install(force=force, system=system, run_as_user=run_as_user) ++ non_interactive = not (hasattr(sys.stdin, "isatty") and sys.stdin.isatty()) ++ _sn = getattr(args, "start_now", None) ++ if _sn is not None: ++ start_now = _sn ++ elif not non_interactive: ++ start_now = prompt_yes_no("Start the gateway now after installing the service?", True) ++ else: ++ start_now = True ++ freebsd_rc_install(force=force, run_as_user=run_as_user) ++ if start_now: ++ freebsd_rc_start() elif is_macos(): launchd_install(force) elif is_windows(): -@@ -5096,6 +5266,8 @@ +@@ -6609,6 +6783,8 @@ def _gateway_command_inner(args): sys.exit(1) if supports_systemd_services(): systemd_uninstall(system=system) + elif supports_freebsd_rc(): -+ freebsd_rc_uninstall(system=system) ++ freebsd_rc_uninstall() elif is_macos(): launchd_uninstall() elif is_windows(): -@@ -5129,6 +5301,8 @@ +@@ -6662,6 +6838,8 @@ def _gateway_command_inner(args): sys.exit(1) if supports_systemd_services(): systemd_start(system=system) + elif supports_freebsd_rc(): -+ freebsd_rc_start(system=system) ++ freebsd_rc_start() elif is_macos(): launchd_start() 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) +@@ -6739,6 +6917,12 @@ def _gateway_command_inner(args): + service_available = True + except subprocess.CalledProcessError: + pass ++ elif supports_freebsd_rc(): ++ try: ++ freebsd_rc_stop() + service_available = True + except subprocess.CalledProcessError: + pass -+ elif supports_freebsd_rc(): -+ try: -+ freebsd_rc_stop(system=system) + elif is_macos() and get_launchd_plist_path().exists(): + try: + launchd_stop() +@@ -6772,6 +6956,12 @@ def _gateway_command_inner(args): service_available = True except subprocess.CalledProcessError: pass + elif supports_freebsd_rc(): + try: -+ freebsd_rc_stop(system=system) ++ freebsd_rc_stop() + service_available = True + except subprocess.CalledProcessError: + pass elif is_macos() and get_launchd_plist_path().exists(): try: launchd_stop() -@@ -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 supports_freebsd_rc(): -+ try: -+ freebsd_rc_stop(system=system) +@@ -6836,6 +7026,12 @@ def _gateway_command_inner(args): 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()): ++ elif supports_freebsd_rc(): ++ try: ++ freebsd_rc_stop() ++ service_stopped = True ++ except subprocess.CalledProcessError: ++ pass + elif is_macos() and get_launchd_plist_path().exists(): + try: + launchd_stop() +@@ -6864,6 +7060,8 @@ def _gateway_command_inner(args): + or get_systemd_unit_path(system=True).exists() + ): systemd_start(system=system) + elif supports_freebsd_rc(): -+ freebsd_rc_start(system=system) ++ freebsd_rc_start() elif is_macos() and get_launchd_plist_path().exists(): launchd_start() elif is_windows(): -@@ -5282,6 +5476,13 @@ +@@ -6890,6 +7088,13 @@ def _gateway_command_inner(args): service_available = True except subprocess.CalledProcessError: pass + elif supports_freebsd_rc(): + service_configured = True + try: -+ freebsd_rc_restart(system=system) ++ freebsd_rc_restart() + service_available = True + except subprocess.CalledProcessError: + pass elif is_macos() and get_launchd_plist_path().exists(): service_configured = True try: -@@ -5347,6 +5548,9 @@ - if supports_systemd_services() and (get_systemd_unit_path(system=False).exists() or get_systemd_unit_path(system=True).exists()): +@@ -6972,6 +7177,9 @@ def _gateway_command_inner(args): + or get_systemd_unit_path(system=True).exists() + ): systemd_status(deep, system=system, full=full) - _print_gateway_process_mismatch(snapshot) -+ elif supports_freebsd_rc(): -+ freebsd_rc_status(deep=deep, system=system, full=full) + _print_gateway_process_mismatch(snapshot) ++ elif supports_freebsd_rc(): ++ freebsd_rc_status(deep, system=system, full=full) + _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 e53957e8e42e..727edf8ebd82 100644 --- a/misc/hermes-agent/files/patch-hermes__cli_main.py +++ b/misc/hermes-agent/files/patch-hermes__cli_main.py @@ -1,66 +1,19 @@ ---- 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", -@@ -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", -@@ -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", -@@ -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)", - ) +--- hermes_cli/main.py.orig 2026-07-08 03:11:08 UTC ++++ hermes_cli/main.py +@@ -12664,6 +12664,16 @@ def cmd_skills(args): - # 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 -@@ -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 list + def cmd_skills(args): ++ # Seed ~/.hermes/skills/ from the bundled library before any skills ++ # subcommand runs. Without this, a fresh install's `hermes skills list` ++ # reports zero skills — the sync only fires from cmd_chat / cmd_gateway / ++ # cmd_dashboard, so a user who runs `hermes skills` first sees an empty ++ # catalog and (reasonably) assumes something is broken. On FreeBSD in ++ # particular the port ships bundled skills under DATADIR and exposes the ++ # path via HERMES_BUNDLED_SKILLS; without this sync call, the catalog stays ++ # empty forever until the user happens to run one of the other entrypoints. ++ _sync_bundled_skills_quietly() ++ + # Route 'config' action to skills_config module + if getattr(args, "skills_action", None) == "config": + _require_tty("skills config") diff --git a/misc/hermes-agent/files/patch-hermes__cli_setup.py b/misc/hermes-agent/files/patch-hermes__cli_setup.py index f38c3bd846a5..00dbf99b26d9 100644 --- a/misc/hermes-agent/files/patch-hermes__cli_setup.py +++ b/misc/hermes-agent/files/patch-hermes__cli_setup.py @@ -1,91 +1,76 @@ ---- 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 ( +--- hermes_cli/setup.py.orig 2026-07-08 03:11:08 UTC ++++ hermes_cli/setup.py +@@ -2033,6 +2033,7 @@ def setup_gateway(config: dict): _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, -@@ -2562,6 +2564,9 @@ +@@ -2043,6 +2044,9 @@ def setup_gateway(config: dict): 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 @@ +@@ -2052,7 +2056,8 @@ def setup_gateway(config: dict): service_installed = _is_service_installed() service_running = _is_service_running() supports_systemd = supports_systemd_services() - 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 _is_windows or supports_freebsd ++ supports_rc = supports_freebsd_rc() ++ supports_service_manager = supports_systemd or supports_rc or _is_macos or _is_windows print() if supports_systemd and has_conflicting_systemd_units(): -@@ -2589,6 +2595,8 @@ +@@ -2070,6 +2075,8 @@ def setup_gateway(config: dict): try: if supports_systemd: systemd_restart() -+ elif supports_freebsd: ++ elif supports_rc: + freebsd_rc_restart() elif _is_macos: launchd_restart() elif _is_windows: -@@ -2614,6 +2622,8 @@ +@@ -2095,6 +2102,8 @@ def setup_gateway(config: dict): try: if supports_systemd: systemd_start() -+ elif supports_freebsd: ++ elif supports_rc: + freebsd_rc_start() elif _is_macos: launchd_start() elif _is_windows: -@@ -2631,6 +2641,8 @@ +@@ -2112,6 +2121,8 @@ def setup_gateway(config: dict): elif supports_service_manager: if supports_systemd: svc_name = "systemd" -+ elif supports_freebsd: -+ svc_name = "FreeBSD rc.d" ++ elif supports_rc: ++ svc_name = "rc.d" elif _is_macos: svc_name = "launchd" else: -@@ -2645,6 +2657,9 @@ +@@ -2126,6 +2137,9 @@ def setup_gateway(config: dict): started_inline = False if supports_systemd: installed_scope, did_install = install_linux_gateway_from_setup(force=False) -+ elif supports_freebsd: ++ elif supports_rc: + freebsd_rc_install(force=False) + did_install = True elif _is_macos: launchd_install(force=False) did_install = True -@@ -2662,6 +2677,8 @@ +@@ -2143,6 +2157,8 @@ def setup_gateway(config: dict): try: if supports_systemd: systemd_start(system=installed_scope == "system") -+ elif supports_freebsd: ++ elif supports_rc: + freebsd_rc_start() elif _is_macos: launchd_start() except UserSystemdUnavailableError as e: -@@ -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 e08d46052911..eaebe6198d36 100644 --- a/misc/hermes-agent/files/patch-hermes__cli_uninstall.py +++ b/misc/hermes-agent/files/patch-hermes__cli_uninstall.py @@ -1,35 +1,26 @@ ---- 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}") +--- hermes_cli/uninstall.py.orig 2026-07-08 03:11:08 UTC ++++ hermes_cli/uninstall.py +@@ -261,7 +261,22 @@ def uninstall_gateway_service(): except Exception as 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. + log_warn(f"Could not remove launchd gateway service: {e}") + +- # 4. Windows: uninstall Scheduled Task + Startup-folder entry. The ++ # 4. FreeBSD: rcvar disable via freebsd_rc_uninstall (delegates to ++ # sysrc/service). The rc.d script itself is pkg-owned and is removed ++ # by `pkg delete hermes-agent`, not here. + elif system == "FreeBSD": + try: + from hermes_cli.gateway import ( + supports_freebsd_rc, -+ FREEBSD_RC_SCRIPT_NAME, -+ FREEBSD_RC_VAR, ++ freebsd_rc_uninstall, + ) + 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 ++ freebsd_rc_uninstall() ++ stopped_something = True + except Exception as e: -+ log_warn(f"Could not disable FreeBSD gateway service: {e}") - - return stopped_something - ++ log_warn(f"Could not remove FreeBSD gateway service: {e}") ++ ++ # 5. Windows: uninstall Scheduled Task + Startup-folder entry. The + # gateway_windows module already knows how to locate and remove both + # code paths (schtasks /Delete + .cmd unlink) and how to stop any + # running detached pythonw gateway process. We call into it so the diff --git a/misc/hermes-agent/files/patch-tools_lazy__deps.py b/misc/hermes-agent/files/patch-tools_lazy__deps.py new file mode 100644 index 000000000000..8b26c4100761 --- /dev/null +++ b/misc/hermes-agent/files/patch-tools_lazy__deps.py @@ -0,0 +1,13 @@ +--- tools/lazy_deps.py.orig 2026-07-08 03:11:08 UTC ++++ tools/lazy_deps.py +@@ -443,7 +443,9 @@ def _allow_lazy_installs() -> bool: + cfg = None + if cfg is not None: + sec = cfg.get("security") or {} +- if not bool(sec.get("allow_lazy_installs", True)): ++ # FreeBSD packages own their deps; users can still opt in. ++ default = not sys.platform.startswith("freebsd") ++ if not bool(sec.get("allow_lazy_installs", default)): + return False + + # (2) Sealed-venv env var: blocks ONLY when there is no safe durable diff --git a/misc/hermes-agent/files/wrapper.in b/misc/hermes-agent/files/wrapper.in index 79affb1bd613..59eeff858ef2 100644 --- a/misc/hermes-agent/files/wrapper.in +++ b/misc/hermes-agent/files/wrapper.in @@ -1,13 +1,22 @@ #!%%PYTHON_CMD%% # Wrapper installed by FreeBSD ports — see misc/hermes-agent. # Hermes is installed under %%HERMES_LIBDIR%% (not site-packages) because # upstream ships top-level packages with generic names (tools, agent, # gateway, plugins, ...) that would collide with other Python packages. +import os import sys sys.path.insert(0, "%%HERMES_LIBDIR%%") +# hermes_constants resolves the bundled/optional data dirs via env-var +# override first, then a wheel-installed sysconfig lookup, then a +# caller-supplied default. The port installs those data trees under +# %%DATADIR%% but doesn't ship a Python wheel — so the wrapper injects +# the paths directly, before any Hermes code imports hermes_constants. +os.environ.setdefault("HERMES_BUNDLED_SKILLS", "%%DATADIR%%/skills") +os.environ.setdefault("HERMES_OPTIONAL_SKILLS", "%%DATADIR%%/optional-skills") + from %%ENTRY_MODULE%% import %%ENTRY_FUNC%% as _entry if __name__ == "__main__": sys.exit(_entry())