build(snap): package Hermes as a strict-confinement snap - #37711
build(snap): package Hermes as a strict-confinement snap#37711beriberikix wants to merge 7 commits into
Conversation
… installs Hermes previously had a single `is_managed()` gate that blocked ALL interactive config/credential writes for every package-manager-managed install. That is correct for NixOS, which generates the config declaratively and owns a read-only result, but wrong for package managers like Homebrew and Snap: those own only the install tree, while the user still configures Hermes in a writable HERMES_HOME. This adds `is_config_managed()` (true only for NixOS) and recognizes Snap as a managed system: - `_MANAGED_SYSTEM_NAMES` maps `snap`/`snapcraft` -> "Snap" so the HERMES_MANAGED env var set by the snap launcher is understood. - `is_config_managed()` is used by the file-permission and config-write paths in this module instead of `is_managed()`, so Snap/Homebrew users can save settings and credentials to their writable HERMES_HOME while NixOS still refuses mutations. - Update-command and managed-message helpers learn the Snap variants (`snap refresh hermes-agent`). No behavior change for NixOS or unmanaged installs; this only unblocks the package-managed-but-config-writable case that Snap needs. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…stalls Builds on the config-managed vs package-managed split. Applies it across the user-facing commands and routes lifecycle actions to the snapd-native equivalents so a confined Snap behaves sensibly: - gateway.py: `gateway setup` now gates on `is_config_managed()` (so Snap users can configure the gateway), and `gateway install/uninstall/ start/stop/restart/status` print snapd guidance (`snap start hermes-agent.gateway`, `snap services …`, etc.) instead of trying to manage a systemd unit that the snap already declares as a daemon. - main.py: `postinstall` is a no-op with guidance under Snap (deps are baked into the snap, not pip-installed), and `uninstall` points at `snap remove hermes-agent` (`--purge` to drop user data). - setup.py and tui_gateway/server.py: swap `is_managed()` -> `is_config_managed()` so the setup wizard and the dashboard's credential endpoint allow writes to the writable HERMES_HOME on Snap/Homebrew. Tests cover get_managed_system()/is_config_managed(), the update/ postinstall/uninstall rewrites, and the gateway service guidance for Snap. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "Command Installation" section assumes a pip/editable install: it looks for a venv entry point and a ~/.local/bin/hermes symlink, and on a miss it advises `pip install -e '.[all]'`. On a package-managed install (Snap/Homebrew/NixOS) none of that applies — the package manager provides the command, the tree is read-only, and the pip advice is impossible to follow. On Snap this surfaced as two spurious warnings plus a bogus remediation line in the summary. When `get_managed_system()` is set, doctor now reports "Managed by <system>" and skips the venv/symlink checks entirely. Unmanaged installs are unaffected. Also improves Homebrew/NixOS, which had the same false warning. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The "no user allowlists configured" warning hardcoded `~/.hermes/.env`, which is misleading whenever HERMES_HOME is relocated — most visibly under Snap, where the env file lives at `$SNAP_USER_COMMON/hermes/.env`. Use `get_env_path()` (the canonical helper, = `get_hermes_home()/.env`) so the message always points at the file the user should actually edit. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds a core24, strict-confinement snap exposing the `hermes`,
`hermes-agent`, `hermes-acp` CLIs and a `gateway` daemon. Depends on the
managed-install support already merged (the launcher sets
HERMES_MANAGED=snap, which the CLI uses to allow config writes and route
lifecycle commands to snapd).
Key decisions:
- packaging/snap/hermes-snap launcher: redirects all mutable state to
$SNAP_USER_COMMON/hermes (the install tree is read-only), disables lazy
pip/Node bootstrap (HERMES_DISABLE_LAZY_INSTALLS / HERMES_SKIP_NODE_
BOOTSTRAP), and points HERMES_{BUNDLED,OPTIONAL}_SKILLS / WEB_DIST /
TUI_DIR / NODE at the bundled assets under $SNAP.
- TUI (`tui` part): the TUI is a Node bundle whose esbuild target is
node20, but core24's apt nodejs is v18. We fetch the official Node 20
binary (per build arch), build the bundle, stage it at
usr/share/hermes-agent/tui/dist, and stage the `node` binary as the
runtime `hermes --tui` execs. A package.json with "type":"module" is
staged beside dist/ so Node loads the ESM bundle (matches nix/tui.nix).
- Dashboard (`web` part): the Vite/React bundle is a build artifact absent
from a clean checkout, so we build it (`npx vite build`) and stage it at
usr/share/hermes-agent/web_dist. The dashboard is served by the Python
web server, so no Node runtime is staged for it.
- Versioning: `adopt-info` + `craftctl set version` reads the version from
pyproject.toml (avoids the invalid `version: git`, which would ship a
snap literally versioned "git").
- .gitignore: ignore snapcraft build artifacts (parts/ prime/ stage/
*.snap).
Built and smoke-tested locally: CLI/version/doctor, writable config,
update->`snap refresh` rewrite, gateway daemon start/stop, the TUI
rendering via the bundled Node, and the dashboard serving the staged
web_dist.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Pure file/string checks (no snapcraft required, runs in CI without it): - manifest is strict-confined, GUI-free, exposes the four expected apps, and the gateway is a restart-on-failure daemon; every app declares the home/network plugs. - version is adopted (not the literal "git"). - the `tui` part fetches Node 20 and stages the bundle + ESM package.json; the `web` part builds and stages the dashboard via `vite build`. - the launcher exports the snap-safe runtime env (state redirection, HERMES_MANAGED=snap, disabled bootstraps, HERMES_NODE) and routes commands through `hermes`. - the packaging notes document the snapd command alternatives. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`snapcraft pack` emitted ~19 library-linter warnings: caca's GL plugin missing libGLU/libglut, and a long list of "unused" libraries (theora, zvbi, JACK, flite/sphinx, ICU test libs, the FFTW parallel variants, libGLX_mesa, libcaca++, …). These are all optional codec / output-driver dependencies pulled transitively by the `ffmpeg` stage-package; Hermes uses ffmpeg only for audio, so nothing links them. Add a scoped `lint.ignore: library` stanza (globbed by family, so it survives minor package version bumps) to silence the noise. We suppress rather than prime-exclude the files, so a codec/output plugin ffmpeg might dlopen at runtime stays available. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
teknium1
left a comment
There was a problem hiding this comment.
Thanks for the thorough packaging work and local smoke-test notes. Two issues need resolution before this can provide the advertised gateway flow.
Problems
gatewayis a system daemon (snap/snapcraft.yaml:47-52) but uses the launcher’s per-userSNAP_USER_COMMON/HOMEstate selection (packaging/snap/hermes-snap:6-11). The daemon cannot share the interactive user’s configured Hermes state under strict confinement.- Both Node build paths download and execute an archive without checksum or signature verification (
snap/snapcraft.yaml:119-125,156-163). - The stacked managed-install work needs a port to current main’s managed-scope architecture (
hermes_cli/managed_scope.py:1-14), not a mechanical cherry-pick.
Suggested changes
- Establish a single supported daemon/user-state model and add an installed-snap test that configures credentials through the CLI and proves the daemon consumes them.
- Verify the pinned Node archive against official architecture-specific hashes before extraction in both build paths.
Automated hermes-sweeper review.
| - removable-media | ||
| hermes-acp: | ||
| command: bin/hermes-snap hermes-acp | ||
| plugs: |
There was a problem hiding this comment.
This system daemon invokes the same launcher as the user CLI, but that launcher derives HERMES_HOME from SNAP_USER_COMMON/HOME. A daemon has separate system-process state, so it cannot consume the invoking user's configured credentials and gateway state. Please define an explicit shared/system-state or user-session-daemon model and cover it with an installed-snap test.
| *) echo "Unsupported build arch: ${CRAFT_ARCH_BUILD_FOR}" >&2; exit 1 ;; | ||
| esac | ||
| NODE_PKG="node-v${NODE_VERSION}-linux-${NODE_ARCH}" | ||
| curl -fsSL "https://nodejs.org/dist/v${NODE_VERSION}/${NODE_PKG}.tar.xz" \ |
There was a problem hiding this comment.
The pinned URL is still an unverified executable build input: this archive is extracted and its node binary is executed without a checksum or signature check. Verify the architecture-specific official SHA-256 before extraction here and in the duplicate web build path.
What does this PR do?
Adds strict-confinement Snap packaging — a core24 snap exposing the
hermes,hermes-agent,hermes-acpCLIs and agatewaydaemon.Related Issue
Fixes #37709
Type of Change
Changes Made
snap/snapcraft.yaml: core24, strict-confinement manifest.hermespart: Python install with the relevant extras; bundles skills/optional-skills;adopt-info+craftctl set versionreads the version frompyproject.toml.tuipart: fetches the official Node 20 (core24's aptnodejsis v18, too old for the TUI'snode20esbuild target), builds the bundle, stages it plus thenoderuntime and a"type": "module"package.jsonso Node loads the ESM bundle.webpart: builds the Vite dashboard (vite build) and stagesweb_dist(served by the Python web server — no Node runtime needed at runtime).lint.ignore: library: silences advisory "unused library" warnings (all fromffmpeg's transitive codec/GL/ICU deps that Hermes never links).packaging/snap/hermes-snap: launcher — redirects mutable state to$SNAP_USER_COMMON/hermes, disables lazy pip/Node bootstrap, and pointsHERMES_*at the bundled assets + Node.packaging/snap/README.md: build/usage/confinement notes..gitignore: snapcraft build artifacts (parts/,prime/,stage/,*.snap).tests/test_snap_packaging.py: validates the manifest, launcher, and packaging notes (pure file/string checks; runs in CI without snapcraft).How to Test
Notes for reviewers
grade: devel— not yet stable-channel ready (deliberate for an initial packaging PR).lint.ignore: librarydisables the advisory library linter wholesale. Every warning seen across builds was an unusedffmpeg/GL/ICU transitive dep; they surface one-at-a-time as each is silenced. A genuinely missing runtime dep would still be caught by the smoke tests above. Flagged in a manifest comment to revisit (narrow the ignore) beforegrade: stable.20.18.1and branch onCRAFT_ARCH_BUILD_FORfor amd64/arm64.Checklist
Code
build(snap):,test(snap):)pytest tests/test_snap_packaging.py -qpassesDocumentation & Housekeeping
packaging/snap/README.mdaddedcli-config.yaml.example— N/A (no config keys)CONTRIBUTING.md/AGENTS.md— N/A