Skip to content

build(snap): package Hermes as a strict-confinement snap - #37711

Draft
beriberikix wants to merge 7 commits into
NousResearch:mainfrom
beriberikix:feat/snap-packaging
Draft

build(snap): package Hermes as a strict-confinement snap#37711
beriberikix wants to merge 7 commits into
NousResearch:mainfrom
beriberikix:feat/snap-packaging

Conversation

@beriberikix

Copy link
Copy Markdown

What does this PR do?

Adds strict-confinement Snap packaging — a core24 snap exposing the hermes, hermes-agent, hermes-acp CLIs and a gateway daemon.

⚠️ Stacked on #37710 (feat: support package-managed installs) and opened as a draft because it depends on it. The launcher sets HERMES_MANAGED=snap, which #37710 teaches the CLI to honor (writable config, snapd-native lifecycle commands). GitHub can't base a cross-fork PR on a fork-only branch, so this targets main and its diff currently also includes #37710's commits; once #37710 merges, this diff auto-reduces to just the snap-packaging changes below. Please review/merge #37710 first.

Related Issue

Fixes #37709

Type of Change

  • ✨ New feature (packaging)

Changes Made

  • snap/snapcraft.yaml: core24, strict-confinement manifest.
    • hermes part: Python install with the relevant extras; bundles skills/optional-skills; adopt-info + craftctl set version reads the version from pyproject.toml.
    • tui part: fetches the official Node 20 (core24's apt nodejs is v18, too old for the TUI's node20 esbuild target), builds the bundle, stages it plus the node runtime and a "type": "module" package.json so Node loads the ESM bundle.
    • web part: builds the Vite dashboard (vite build) and stages web_dist (served by the Python web server — no Node runtime needed at runtime).
    • lint.ignore: library: silences advisory "unused library" warnings (all from ffmpeg'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 points HERMES_* 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

snapcraft pack
sudo snap install --dangerous hermes-agent_*.snap

hermes-agent.hermes version          # 0.15.1
hermes-agent.hermes doctor
hermes-agent.hermes --tui            # renders via the bundled Node 20
hermes-agent.hermes dashboard        # serves the staged web_dist
sudo snap start hermes-agent.gateway && snap services hermes-agent

pytest tests/test_snap_packaging.py -q

Notes for reviewers

  • grade: devel — not yet stable-channel ready (deliberate for an initial packaging PR).
  • lint.ignore: library disables the advisory library linter wholesale. Every warning seen across builds was an unused ffmpeg/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) before grade: stable.
  • Both Node-built parts pin Node 20.18.1 and branch on CRAFT_ARCH_BUILD_FOR for amd64/arm64.

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (build(snap):, test(snap):)
  • I searched for existing PRs/issues to make sure this isn't a duplicate
  • My PR contains only snap-packaging changes
  • pytest tests/test_snap_packaging.py -q passes
  • I've added tests for my changes
  • Tested on: Ubuntu 24.04 (amd64), snapcraft 8.14.5 / core24, LXD build

Documentation & Housekeeping

  • Docs — packaging/snap/README.md added
  • cli-config.yaml.example — N/A (no config keys)
  • CONTRIBUTING.md/AGENTS.md — N/A
  • Cross-platform impact — snap is Linux-only by nature; no changes to shared runtime code in this PR
  • Tool descriptions/schemas — N/A

beriberikix and others added 7 commits June 3, 2026 05:43
… 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>
@alt-glitch alt-glitch added type/feature New feature or request area/config Config system, migrations, profiles comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery P3 Low — cosmetic, nice to have labels Jun 2, 2026

@teknium1 teknium1 left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for the thorough packaging work and local smoke-test notes. Two issues need resolution before this can provide the advertised gateway flow.

Problems

  • gateway is a system daemon (snap/snapcraft.yaml:47-52) but uses the launcher’s per-user SNAP_USER_COMMON/HOME state 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.

Comment thread snap/snapcraft.yaml
- removable-media
hermes-acp:
command: bin/hermes-snap hermes-acp
plugs:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread snap/snapcraft.yaml
*) 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" \

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@teknium1 teknium1 added sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform labels Jul 13, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/config Config system, migrations, profiles comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery P3 Low — cosmetic, nice to have sweeper:blast-moderate Sweeper blast radius: moderate — a subsystem or single platform sweeper:risk-compatibility Sweeper risk: may break existing users, config, migrations, defaults, or upgrades sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages sweeper:risk-security-boundary Sweeper risk: may affect sandboxing, auth, credentials, or sensitive data type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Package Hermes as a strict-confinement snap

3 participants