diff --git a/misc/hermes-agent/Makefile b/misc/hermes-agent/Makefile index c691cc4a8709..8d130a56b747 100644 --- a/misc/hermes-agent/Makefile +++ b/misc/hermes-agent/Makefile @@ -1,166 +1,166 @@ PORTNAME= hermes-agent -PORTVERSION= 0.15.0 +PORTVERSION= 0.16.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.7.7.2 +GH_TAGNAME= v2026.8.3 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} 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 c8be9a9685e5..5f1ac7a521e0 100644 --- a/misc/hermes-agent/distinfo +++ b/misc/hermes-agent/distinfo @@ -1,5 +1,5 @@ -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 +TIMESTAMP = 1785855049 +SHA256 (hermes-agent-web-dist-0.16.0.tar.gz) = 4141ac39cf4ee44c4bcbc9496f2753c9b53af22bd85bc82816c45dc99ed5e24d +SIZE (hermes-agent-web-dist-0.16.0.tar.gz) = 1486907 +SHA256 (NousResearch-hermes-agent-0.16.0-v2026.8.3_GH0.tar.gz) = 370542c7219faba6300905c3b419e14e6508a31ac698a1a5174e0386990834be +SIZE (NousResearch-hermes-agent-0.16.0-v2026.8.3_GH0.tar.gz) = 63365932 diff --git a/misc/hermes-agent/files/patch-agent_skill__utils.py b/misc/hermes-agent/files/patch-agent_skill__utils.py index 907292438bff..be1fc31615b3 100644 --- a/misc/hermes-agent/files/patch-agent_skill__utils.py +++ b/misc/hermes-agent/files/patch-agent_skill__utils.py @@ -1,12 +1,12 @@ ---- agent/skill_utils.py.orig 2026-07-08 03:11:08 UTC +--- agent/skill_utils.py.orig 2026-08-03 16:57:23 UTC +++ agent/skill_utils.py -@@ -192,6 +192,9 @@ def skill_matches_platform(frontmatter: Dict[str, Any] +@@ -236,6 +236,9 @@ def skill_matches_platform_list(platforms: Any) -> boo 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__defaults.py similarity index 63% rename from misc/hermes-agent/files/patch-hermes__cli_config.py rename to misc/hermes-agent/files/patch-hermes__cli_config__defaults.py index 517d341280d1..504b7aadcd14 100644 --- a/misc/hermes-agent/files/patch-hermes__cli_config.py +++ b/misc/hermes-agent/files/patch-hermes__cli_config__defaults.py @@ -1,15 +1,24 @@ ---- hermes_cli/config.py.orig 2026-07-08 03:11:08 UTC -+++ hermes_cli/config.py -@@ -2554,8 +2554,10 @@ DEFAULT_CONFIG = { +--- hermes_cli/config_defaults.py.orig 2026-08-04 14:52:16 UTC ++++ hermes_cli/config_defaults.py +@@ -4,6 +4,8 @@ verbatim from hermes_cli/config.py. Must not import fr + verbatim from hermes_cli/config.py. Must not import from hermes_cli.config. + """ + ++import sys ++ + DEFAULT_CONFIG = { + "model": "", + "providers": {}, +@@ -2153,8 +2155,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. - "allow_lazy_installs": True, + # 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 75ae8f22707c..694768d825e2 100644 --- a/misc/hermes-agent/files/patch-hermes__cli_gateway.py +++ b/misc/hermes-agent/files/patch-hermes__cli_gateway.py @@ -1,287 +1,280 @@ ---- hermes_cli/gateway.py.orig 2026-07-08 03:11:08 UTC +--- hermes_cli/gateway.py.orig 2026-08-03 16:57:23 UTC +++ hermes_cli/gateway.py -@@ -1638,6 +1638,45 @@ def supports_systemd_services() -> bool: +@@ -1714,6 +1714,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") + + +# 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") + + +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 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 + 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): +@@ -4687,6 +4726,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_run_or_print(cmd: list[str], *, action: str) -> bool: + """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} {FREEBSD_RC_SCRIPT_NAME}: exit {e.returncode}") + return False + + escalator = _freebsd_privilege_escalator() + if escalator is None: + print(f" Run as root: {' '.join(cmd)}") + return False + + try: + subprocess.run([escalator] + cmd, check=True) + return True + except subprocess.CalledProcessError as e: + 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_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...") + _freebsd_run_or_print( + ["sysrc", f"{FREEBSD_RC_VAR}=YES", f"hermes_gateway_user={target_user}"], + action="enable", + ) + + +def freebsd_rc_uninstall(system: bool = False): + """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 + 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 # ============================================================================= -@@ -6535,6 +6697,18 @@ def _gateway_command_inner(args): +@@ -6932,6 +7094,18 @@ def _gateway_command_inner(args): ) if start_now: systemd_start(system=system) + elif supports_freebsd_rc(): + 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(): -@@ -6609,6 +6783,8 @@ def _gateway_command_inner(args): +@@ -7006,6 +7180,8 @@ def _gateway_command_inner(args): sys.exit(1) if supports_systemd_services(): systemd_uninstall(system=system) + elif supports_freebsd_rc(): + freebsd_rc_uninstall() elif is_macos(): launchd_uninstall() elif is_windows(): -@@ -6662,6 +6838,8 @@ def _gateway_command_inner(args): +@@ -7059,6 +7235,8 @@ def _gateway_command_inner(args): sys.exit(1) if supports_systemd_services(): systemd_start(system=system) + elif supports_freebsd_rc(): + freebsd_rc_start() elif is_macos(): launchd_start() elif is_windows(): -@@ -6739,6 +6917,12 @@ def _gateway_command_inner(args): +@@ -7136,6 +7314,18 @@ 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 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() + service_available = True + except subprocess.CalledProcessError: + pass elif is_macos() and get_launchd_plist_path().exists(): try: launchd_stop() -@@ -6836,6 +7026,12 @@ def _gateway_command_inner(args): +@@ -7233,6 +7423,12 @@ def _gateway_command_inner(args): service_stopped = True except subprocess.CalledProcessError: pass + 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): +@@ -7261,6 +7457,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() elif is_macos() and get_launchd_plist_path().exists(): launchd_start() elif is_windows(): -@@ -6890,6 +7088,13 @@ def _gateway_command_inner(args): +@@ -7287,6 +7485,13 @@ def _gateway_command_inner(args): service_available = True except subprocess.CalledProcessError: pass + elif supports_freebsd_rc(): + service_configured = True + try: + freebsd_rc_restart() + service_available = True + except subprocess.CalledProcessError: + pass elif is_macos() and get_launchd_plist_path().exists(): service_configured = True try: -@@ -6972,6 +7177,9 @@ def _gateway_command_inner(args): +@@ -7369,6 +7574,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, system=system, full=full) _print_gateway_process_mismatch(snapshot) elif is_macos() and get_launchd_plist_path().exists(): launchd_status(deep) diff --git a/misc/hermes-agent/files/patch-hermes__cli_main.py b/misc/hermes-agent/files/patch-hermes__cli_main.py index 727edf8ebd82..2cb46e485db8 100644 --- a/misc/hermes-agent/files/patch-hermes__cli_main.py +++ b/misc/hermes-agent/files/patch-hermes__cli_main.py @@ -1,19 +1,19 @@ ---- hermes_cli/main.py.orig 2026-07-08 03:11:08 UTC +--- hermes_cli/main.py.orig 2026-08-03 16:57:23 UTC +++ hermes_cli/main.py -@@ -12664,6 +12664,16 @@ def cmd_skills(args): +@@ -11112,6 +11112,16 @@ def cmd_skills(args): 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 00dbf99b26d9..e3a4aad9cbca 100644 --- a/misc/hermes-agent/files/patch-hermes__cli_setup.py +++ b/misc/hermes-agent/files/patch-hermes__cli_setup.py @@ -1,76 +1,76 @@ ---- hermes_cli/setup.py.orig 2026-07-08 03:11:08 UTC +--- hermes_cli/setup.py.orig 2026-08-03 16:57:23 UTC +++ hermes_cli/setup.py -@@ -2033,6 +2033,7 @@ def setup_gateway(config: dict): +@@ -2241,6 +2241,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, -@@ -2043,6 +2044,9 @@ def setup_gateway(config: dict): +@@ -2251,6 +2252,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, -@@ -2052,7 +2056,8 @@ def setup_gateway(config: dict): +@@ -2260,7 +2264,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_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(): -@@ -2070,6 +2075,8 @@ def setup_gateway(config: dict): +@@ -2278,6 +2283,8 @@ def setup_gateway(config: dict): try: if supports_systemd: systemd_restart() + elif supports_rc: + freebsd_rc_restart() elif _is_macos: launchd_restart() elif _is_windows: -@@ -2095,6 +2102,8 @@ def setup_gateway(config: dict): +@@ -2303,6 +2310,8 @@ def setup_gateway(config: dict): try: if supports_systemd: systemd_start() + elif supports_rc: + freebsd_rc_start() elif _is_macos: launchd_start() elif _is_windows: -@@ -2112,6 +2121,8 @@ def setup_gateway(config: dict): +@@ -2320,6 +2329,8 @@ def setup_gateway(config: dict): elif supports_service_manager: if supports_systemd: svc_name = "systemd" + elif supports_rc: + svc_name = "rc.d" elif _is_macos: svc_name = "launchd" else: -@@ -2126,6 +2137,9 @@ def setup_gateway(config: dict): +@@ -2334,6 +2345,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_rc: + freebsd_rc_install(force=False) + did_install = True elif _is_macos: launchd_install(force=False) did_install = True -@@ -2143,6 +2157,8 @@ def setup_gateway(config: dict): +@@ -2351,6 +2365,8 @@ def setup_gateway(config: dict): try: if supports_systemd: systemd_start(system=installed_scope == "system") + elif supports_rc: + freebsd_rc_start() elif _is_macos: launchd_start() except UserSystemdUnavailableError as e: diff --git a/misc/hermes-agent/files/patch-hermes__cli_uninstall.py b/misc/hermes-agent/files/patch-hermes__cli_uninstall.py index eaebe6198d36..5c7564199600 100644 --- a/misc/hermes-agent/files/patch-hermes__cli_uninstall.py +++ b/misc/hermes-agent/files/patch-hermes__cli_uninstall.py @@ -1,26 +1,26 @@ ---- hermes_cli/uninstall.py.orig 2026-07-08 03:11:08 UTC +--- hermes_cli/uninstall.py.orig 2026-08-03 16:57:23 UTC +++ hermes_cli/uninstall.py -@@ -261,7 +261,22 @@ def uninstall_gateway_service(): +@@ -265,7 +265,22 @@ def uninstall_gateway_service(): except Exception as e: 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_uninstall, + ) + if supports_freebsd_rc(): + freebsd_rc_uninstall() + stopped_something = True + except Exception as e: + 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 index 8b26c4100761..806ce5257141 100644 --- a/misc/hermes-agent/files/patch-tools_lazy__deps.py +++ b/misc/hermes-agent/files/patch-tools_lazy__deps.py @@ -1,13 +1,13 @@ ---- tools/lazy_deps.py.orig 2026-07-08 03:11:08 UTC +--- tools/lazy_deps.py.orig 2026-08-03 16:57:23 UTC +++ tools/lazy_deps.py -@@ -443,7 +443,9 @@ def _allow_lazy_installs() -> bool: +@@ -514,7 +514,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