Author SHA1 Message Date
darman 25e17f163e Merge pull request 'mars: provision the Mnemosyne memory provider for Hermes' (#6) from feat/mars-hermes-mnemosyne into master
Reviewed-on: #6
2026-09-19 02:16:35 +02:00
darmanandClaude Opus 5 9b96d3c6f8 mars(mnemosyne): fix env build and plugin symlink target
- fetchPypi: sdists are published underscore-named, so the hyphenated
  pname 404'd.
- sitePackages is relative; prefix the env path so the unit's -d check
  and the symlink point at the store.
- hermes_memory_provider ships in mnemosyne-memory, not mnemosyne-hermes;
  move the import check accordingly.
- Drop the unused /opt/data/mnemosyne-env mount, the orphaned
  requirements.txt, and trim comments.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 02:09:14 +02:00
3 changed files with 34 additions and 154 deletions
+6 -50
View File
@@ -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"
-46
View File
@@ -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
+28 -58
View File
@@ -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
])