diff --git a/hosts/mars/hermes-agent.nix b/hosts/mars/hermes-agent.nix index 129b5de..0d84e48 100644 --- a/hosts/mars/hermes-agent.nix +++ b/hosts/mars/hermes-agent.nix @@ -74,11 +74,7 @@ let builtins.readFile ./gitea-pr-review-prompt.md ); - # Mnemosyne memory provider (local SQLite, third-party plugin — not bundled - # with the official image). Fully built as a Nix derivation — see - # pkgs/mnemosyne-env.nix — and mounted READ-ONLY into the container at a - # fixed path. The oneshot near the bottom of this file only writes the - # plugin symlink Docker needs at $HERMES_HOME/plugins/mnemosyne. + # Mnemosyne memory provider: third-party plugin, not in the image. mnemosyneEnv = pkgs.callPackage ../../pkgs/mnemosyne-env.nix { }; # Wire event names (X-GitHub-Event) each route accepts — NOT the @@ -235,15 +231,6 @@ in "${hermesHome}:/opt/data" "${dropboxDir}:/opt/data/dropbox" - # Mnemosyne memory provider — a Nix-built python env, mounted :ro. - # Nothing fetched at boot, nothing writable from inside the container. - # The plugin symlink the oneshot at the bottom of this file writes - # points at the canonical store path (site-packages passthru), which - # is visible inside thanks to the existing /nix/store ro mount, so - # this /opt/data restatement is a readability alias, not a load - # bearing path. - "${mnemosyneEnv}:/opt/data/mnemosyne-env:ro" - # luna's Obsidian vault, synced with CouchDB on jupiter by # livesync-bridge.nix. Under /opt/data so she can write notes, not just # read them; the bridge runs as this same uid/gid so no chown is needed. @@ -435,26 +422,9 @@ in ''; }; - # ---- Mnemosyne memory provider ---------------------------------------- - # The provider's Python closure (mnemosyneEnv, callPackage'd - # pkgs/mnemosyne-env.nix above) is a READ-ONLY nix store path mounted into - # the container — nothing fetched at boot, nothing inside the container can - # write to it, and the mono-repo reproducibility story applies. Hermes only - # needs one mutable pickup point: $HERMES_HOME/plugins/mnemosyne, the - # symlink its discovery scan looks for. Both the plugin wrapper and its - # sibling `mnemosyne` core package live in that env's single site-packages, - # so one link covers them. - # - # Path handling: the symlink target is spelled in the CONTAINER's path - # space (/opt/data/...), because Hermes resolves the plugin from inside the - # container — the same host/container mismatch the webhook prompts already - # navigate via containerHome. Host-side the identical literal resolves - # onto the same store path through hermesHome's bind mount. - # - # Failure posture: after=, not requires= — a failed link write leaves the - # container running with whatever memory.provider falls back to Hermes's - # built-in memory, not a dead bot. The ro mount itself is evaluated at - # build time, so there is nothing provisionable to drift at runtime. + # Hermes discovers memory providers under $HERMES_HOME/plugins; the target + # is a store path, readable in the container via the /nix/store ro mount. + # wantedBy, not requiredBy: a failure leaves Hermes on built-in memory. systemd.services.hermes-agent-mnemosyne-plugin = { description = "Link Mnemosyne provider into the Hermes plugin dir"; before = [ "podman-hermes-agent.service" ]; @@ -468,27 +438,13 @@ in }; script = '' set -euo pipefail - pluginsDir=${hermesHome}/plugins pluginDir=$pluginsDir/mnemosyne - # Target is the STORE path itself, not a /opt/data restatement: the - # container already ro-mounts /nix/store for git/tea (see the volumes - # list), so the identical literal resolves on both sides of the bind - # mount. Using the canonical store path directly — not the - # /opt/data/mnemosyne-env mount — keeps one truth and still works - # whether Hermes resolves the link inside the container or host-side - # during debugging. - target="${mnemosyneEnv.sitePackages}/hermes_memory_provider" - - if [ ! -d "$target" ]; then - echo "hermes_memory_provider not found in the mnemosyne env — unit bug, not transient" >&2 - exit 1 - fi + target=${mnemosyneEnv}/${mnemosyneEnv.sitePackages}/hermes_memory_provider + [ -d "$target" ] || { echo "$target missing" >&2; exit 1; } mkdir -p "$pluginsDir" chown ${hermesUid}:${hermesGid} "$pluginsDir" - # Atomic swap: write to a temp name, rename over the old link. `-T` - # errors loudly if the target turned into a directory for any reason. ln -sfn "$target" "$pluginDir.new" mv -Tf "$pluginDir.new" "$pluginDir" chown -h ${hermesUid}:${hermesGid} "$pluginDir" diff --git a/hosts/mars/mnemosyne/requirements.txt b/hosts/mars/mnemosyne/requirements.txt deleted file mode 100644 index 8d687f0..0000000 --- a/hosts/mars/mnemosyne/requirements.txt +++ /dev/null @@ -1,46 +0,0 @@ -# Pinned requirements for a Mnemosyne side-venv on mars. -# -# Hermes vendors its own Python (the official image's venv) and deliberately -# stays minimal: no pip module inside it, PEP 668 external-management on top. -# Installing provider packages straight into that interpreter would fight the -# image on every rebuild, so Mnemosyne (and its plugin wrapper) live in their -# own venv instead — see the provisioning unit in hosts/mars/hermes-agent.nix. -# -# Freeze captured 2026-09-19 from a verified container-side install of -# `mnemosyne-memory[embeddings]` + `mnemosyne-hermes` — side venv at -# $HERMES_HOME/mnemosyne-venv, activated via $HERMES_HOME/plugins/mnemosyne. -# Versions pinned exactly; transitive deps frozen for reproducibility -# (onnxruntime/numpy drift under a long-lived SQLite state dir is what a -# freeze is here to prevent). -# -anyio==4.15.0 -certifi==2026.7.22 -charset-normalizer==3.5.1 -click==8.5.0 -fastembed==0.8.0 -filelock==3.32.5 -flatbuffers==25.12.19 -fsspec==2026.7.0 -h11==0.16.0 -hf-xet==1.6.0 -httpcore==1.0.9 -httpx==0.28.1 -huggingface-hub==1.32.0 -idna==3.19 -loguru==0.7.3 -mmh3==5.3.0 -mnemosyne-hermes==0.5.0 -mnemosyne-memory==3.15.1 -numpy==2.5.3 -onnxruntime==1.30.0 -packaging==26.3 -pillow==12.3.0 -protobuf==7.36.1 -py-rust-stemmers==0.1.8 -pyyaml==6.0.3 -requests==2.34.2 -sqlite-vec==0.1.9 -tokenizers==0.23.2 -tqdm==4.70.0 -typing-extensions==4.16.0 -urllib3==2.7.0 diff --git a/pkgs/mnemosyne-env.nix b/pkgs/mnemosyne-env.nix index 7b95478..bc57508 100644 --- a/pkgs/mnemosyne-env.nix +++ b/pkgs/mnemosyne-env.nix @@ -1,86 +1,56 @@ -# Mnemosyne memory provider for Hermes on mars — packaged for real (Nix). +# Mnemosyne memory provider for Hermes. Built here rather than pip-installed: +# the image's Python has no pip and is PEP 668 managed. # -# Why derivations instead of a runtime side-venv: the official Hermes image -# vendors its own Python and stays off-limits to pip (no pip module, PEP 668), -# and a runtime venv built host-side breaks twice over inside the container: -# the venv's pyvenv.cfg records a /nix/store python home the container never -# mounts, and a plugins symlink with an absolute host path points nowhere -# from /opt/data. Building here means nothing is fetched at boot, nothing -# under the provider's control is writable from inside the container, and -# the closure is as reproducible as the rest of the host. -# -# Package set (one shared site-packages — the plugin wrapper imports its -# sibling `mnemosyne` core package, so withPackages, not separate envs): -# -# mnemosyne-memory core engine: SQLite/FTS5 storage, recall, tools. -# Base deps only (PyYAML); the optional extras (llm, -# embeddings via fastembed/onnxruntime, mcp, sync) are -# deliberately NOT pulled — recall uses the bundled FTS5 -# lexical path, and the heavyweight ML stack (~hundreds of -# MB, live network on first vector use) buys nothing for -# a first deployment. Adding the embeddings extra later -# is pinning fastembed + sqlite-vec here. -# mnemosyne-hermes the wrapper Hermes discovers under $HERMES_HOME/plugins -# (installs itself as package `hermes_memory_provider`). -# Hard dependency: mnemosyne-memory, PyYAML. -# -# Platform note: both sdists are pure Python (build no C extensions), so -# nothing here constrains the host arch beyond the interpreter itself. +# Core deps only: the embeddings extra (fastembed/onnxruntime) is optional at +# runtime, and recall falls back to FTS5. { python3, fetchPypi, }: let - python = python3; - - mnemosyneMemory = python.pkgs.buildPythonPackage rec { + mnemosyneMemory = python3.pkgs.buildPythonPackage rec { pname = "mnemosyne-memory"; version = "3.15.1"; pyproject = true; src = fetchPypi { - inherit pname version; - sha256 = "sha256-lspUMxc0pUSkhSUrNdiiO5OJ1NMC/S853EYSanXtXKM="; + pname = "mnemosyne_memory"; + inherit version; + hash = "sha256-lspUMxc0pUSkhSUrNdiiO5OJ1NMC/S853EYSanXtXKM="; }; - build-system = with python.pkgs; [ setuptools ]; + build-system = with python3.pkgs; [ setuptools ]; + dependencies = with python3.pkgs; [ pyyaml ]; - # Base dependency set — everything else in the upstream metadata is an - # optional extra (llm / embeddings / mcp / sync / test / dev) and is not - # installed; see the file-level comment. - dependencies = with python.pkgs; [ pyyaml ]; - - doCheck = false; # upstream tests want a live Hermes + LLM key present - pythonImportsCheck = [ "mnemosyne" ]; + doCheck = false; # tests want a live Hermes + LLM key + # Ships the Hermes plugin package too, not just the core. + pythonImportsCheck = [ + "mnemosyne" + "hermes_memory_provider" + ]; }; - mnemosyneHermes = python.pkgs.buildPythonPackage rec { + mnemosyneHermes = python3.pkgs.buildPythonPackage rec { pname = "mnemosyne-hermes"; version = "0.5.0"; pyproject = true; src = fetchPypi { - inherit pname version; - sha256 = "sha256-CzEvnUw5oPFtT5bHQQ/GBdy2C/E7qShQn32irIRYKqw="; + pname = "mnemosyne_hermes"; + inherit version; + hash = "sha256-CzEvnUw5oPFtT5bHQQ/GBdy2C/E7qShQn32irIRYKqw="; }; - build-system = with python.pkgs; [ setuptools ]; - - # The wrapper declares `mnemosyne-memory[embeddings]>=3.11.1` on PyPI, but - # the embeddings extra is only consulted when vector recall is enabled - # (see above) — pass the core dependency explicitly rather than dragging - # in onnxruntime for nothing. - dependencies = [ mnemosyneMemory ] ++ (with python.pkgs; [ pyyaml ]); + build-system = with python3.pkgs; [ setuptools ]; + # Upstream asks for mnemosyne-memory[embeddings]; see the header. + dependencies = [ mnemosyneMemory ] ++ (with python3.pkgs; [ pyyaml ]); doCheck = false; - pythonImportsCheck = [ "hermes_memory_provider" ]; + pythonImportsCheck = [ "mnemosyne_hermes" ]; }; - - # The exposed value is the python env itself (a store path mounted :ro). - # Hermes only needs the site-packages dir inside it; `sitePackages` is a - # passthru the python interpreter derivation (and hence withPackages envs) - # exposes, so the caller uses `${env.sitePackages}` instead of guessing - # the python version in a path literal. in -python.withPackages (_: [ mnemosyneMemory mnemosyneHermes ]) +python3.withPackages (_: [ + mnemosyneMemory + mnemosyneHermes +])