From beb164a9a218ce680737ef46a94e05786fd9e72c Mon Sep 17 00:00:00 2001 From: Spencer Bull Date: Sun, 6 Sep 2026 22:29:49 -0500 Subject: [PATCH] Resolve Hermes profile homes before package setup --- pkgbuilds/hermes-desktop/PKGBUILD | 2 +- pkgbuilds/hermes-desktop/hermes-desktop.sh | 20 ++++--- test/hermes-desktop-test.py | 67 +++++++++++++++++++++- 3 files changed, 80 insertions(+), 9 deletions(-) diff --git a/pkgbuilds/hermes-desktop/PKGBUILD b/pkgbuilds/hermes-desktop/PKGBUILD index 5d32d59..aa4b57d 100644 --- a/pkgbuilds/hermes-desktop/PKGBUILD +++ b/pkgbuilds/hermes-desktop/PKGBUILD @@ -30,7 +30,7 @@ source=("install-${_commit}.sh::https://raw.githubusercontent.com/NousResearch/h 'hermes-desktop.png') sha256sums=('5854b15670b51a8daae8f59ddfa917062de9f74be261eb73b4b8d719710f8968' '821556e6336796450ab852d375117b48a4887e71d255794fd6318d99982a5ab6' - '85834b56348529413bbc248282d3a1769a2276a016affe9eafb7dd8773d9e12f' + '27040084e691421dc1748e7646ae7ee79d5777e8ee1e1ec7581a68432d4a29c4' 'c37d4cfa4801eccbd769fddaebb2115d54faa49a5b7bc0f305f563e3cefd9bd8' 'd60d164e24fdcf6532133b8ea43c77a201e4b9e9dbc396187b58d51d8590ef52') diff --git a/pkgbuilds/hermes-desktop/hermes-desktop.sh b/pkgbuilds/hermes-desktop/hermes-desktop.sh index 77f87ef..fe00939 100644 --- a/pkgbuilds/hermes-desktop/hermes-desktop.sh +++ b/pkgbuilds/hermes-desktop/hermes-desktop.sh @@ -1,18 +1,24 @@ #!/bin/bash set -euo pipefail -export HERMES_HOME="${HERMES_HOME:-$HOME/.hermes}" -HERMES_HOME=$(realpath -ms "$HERMES_HOME") -root="$HERMES_HOME/hermes-agent" -marker="$root/.omarchy-hermes-desktop" -installer=/usr/share/hermes-desktop/install.sh -cli=("$root/venv/bin/python" "$root/hermes") - die() { echo "Hermes: $*" >&2 exit 1 } +HERMES_HOME=$(realpath -ms -- "${HERMES_HOME:-$HOME/.hermes}") +# A profile session shares its parent home's installation and launchers. +home_parent=${HERMES_HOME%/*} +if [[ ${home_parent##*/} == "profiles" ]]; then + HERMES_HOME=${home_parent%/*} +fi +[[ $HERMES_HOME == /* && $HERMES_HOME != "/" ]] || die "Use a Hermes data directory other than /." +export HERMES_HOME +root="$HERMES_HOME/hermes-agent" +marker="$root/.omarchy-hermes-desktop" +installer=/usr/share/hermes-desktop/install.sh +cli=("$root/venv/bin/python" "$root/hermes") + desktop_executable() { local executable for executable in "$root/apps/desktop/release/linux-unpacked/"{Hermes,hermes}; do diff --git a/test/hermes-desktop-test.py b/test/hermes-desktop-test.py index 0395895..d4f60c2 100644 --- a/test/hermes-desktop-test.py +++ b/test/hermes-desktop-test.py @@ -95,7 +95,7 @@ fi ''') self.write(self.base / 'gui', '''#!/usr/bin/env python3 import json,os,sys,time -json.dump({'pid':os.getpid(),'args':sys.argv[1:],'root':os.environ['HERMES_DESKTOP_HERMES_ROOT'],'node':os.environ.get('ELECTRON_RUN_AS_NODE'),'password':os.environ['HERMES_DESKTOP_PASSWORD_STORE']},open(os.environ['TEST_OUTPUT'],'w')) +json.dump({'pid':os.getpid(),'args':sys.argv[1:],'root':os.environ['HERMES_DESKTOP_HERMES_ROOT'],'home':os.environ['HERMES_HOME'],'node':os.environ.get('ELECTRON_RUN_AS_NODE'),'password':os.environ['HERMES_DESKTOP_PASSWORD_STORE']},open(os.environ['TEST_OUTPUT'],'w')) if os.environ.get('TEST_GUI_WAIT'): time.sleep(30) ''') self.write(self.bin / 'unshare', '#!/bin/bash\nexit 0\n') @@ -126,6 +126,71 @@ if os.environ.get('TEST_GUI_WAIT'): time.sleep(30) self.assertEqual((self.root / '.omarchy-hermes-desktop').read_text(), 'ready\n') self.assertEqual(subprocess.check_output(['git','-C',str(self.root),'status','--porcelain'],env=self.env),b'') + def test_profile_setup_installs_in_machine_home(self): + profile = self.home / '.hermes/profiles/coder' + self.write(profile / 'config.yaml', 'profile data\n') + self.env['HERMES_HOME'] = str(profile) + '/./' + self.install() + self.assertEqual((self.root / '.omarchy-hermes-desktop').read_text(), 'ready\n') + self.assertEqual(list(profile.iterdir()), [profile / 'config.yaml']) + self.assertEqual((profile / 'config.yaml').read_text(), 'profile data\n') + + def test_profile_warm_check_install_and_launch_share_machine_runtime(self): + self.install() + before = self.log.read_bytes() + self.env['HERMES_HOME'] = str(self.home / '.hermes/profiles/coder') + self.run_launcher('--check') + self.run_launcher('--install') + self.run_launcher('hermes://profile') + observed = json.loads(self.output.read_text()) + self.assertEqual(observed['root'], str(self.root)) + self.assertEqual(observed['home'], str(self.root.parent)) + self.assertEqual(self.log.read_bytes(), before) + + def test_custom_home_profile_uses_its_own_parent_root(self): + custom_home = self.base / 'custom data' + self.root = custom_home / 'hermes-agent' + self.env['HERMES_HOME'] = str(custom_home / 'profiles/coder') + self.install() + self.run_launcher() + observed = json.loads(self.output.read_text()) + self.assertEqual(observed['root'], str(self.root)) + self.assertEqual(observed['home'], str(custom_home)) + self.assertFalse((self.home / '.hermes').exists()) + self.assertFalse((custom_home / 'profiles').exists()) + + def test_custom_home_with_profiles_ancestor_is_preserved(self): + custom_home = self.base / 'profiles/team/custom data' + self.root = custom_home / 'hermes-agent' + self.env['HERMES_HOME'] = str(custom_home) + self.install() + self.run_launcher() + observed = json.loads(self.output.read_text()) + self.assertEqual(observed['root'], str(self.root)) + self.assertEqual(observed['home'], str(custom_home)) + + def test_profile_resolution_preserves_symlink_home_spelling(self): + target = self.base / 'physical data' + target.mkdir() + alias = self.base / 'data alias' + alias.symlink_to(target, target_is_directory=True) + self.root = alias / 'hermes-agent' + self.env['HERMES_HOME'] = str(alias / 'profiles/coder') + self.install() + self.run_launcher() + observed = json.loads(self.output.read_text()) + self.assertEqual(observed['root'], str(self.root)) + self.assertEqual(observed['home'], str(alias)) + self.assertTrue(alias.is_symlink()) + + def test_filesystem_root_home_is_rejected_before_mutation(self): + self.write(self.bin / 'mkdir', '#!/bin/bash\nprintf called >> "$TEST_LOG"\nexit 42\n') + for home in ['/', '/profiles/coder', '/profiles/coder/./']: + with self.subTest(home=home): + result = self.run_launcher('--install', ok=False, HERMES_HOME=home) + self.assertIn('Use a Hermes data directory other than /.', result.stdout) + self.assertFalse(self.log.exists()) + def test_interrupted_clone_stays_aside_and_retry_succeeds(self): self.run_launcher('--install', ok=False, FAIL_PARTIAL_CLONE='1') self.assertFalse(self.root.exists())