Compare commits
53
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b71dcad96e | ||
|
|
4d017913d0 | ||
|
|
b686ed892d | ||
|
|
5c03dc8c09 | ||
|
|
3c2a24b248 | ||
|
|
986962bb64 | ||
|
|
a93ee6a433 | ||
|
|
ca4f596a14 | ||
|
|
279f4d6b95 | ||
|
|
243fe1c9d9 | ||
|
|
bae189861f | ||
|
|
981274b20a | ||
|
|
fe1325202b | ||
|
|
432b5e3e24 | ||
|
|
99ec17acfa | ||
|
|
9ca8f90e91 | ||
|
|
d3a5e69162 | ||
|
|
d6130394fa | ||
|
|
7aceb388e7 | ||
|
|
e1fc502286 | ||
|
|
e68994680a | ||
|
|
55a3906f4c | ||
|
|
f8d7fae7a8 | ||
|
|
3def390764 | ||
|
|
a24064c720 | ||
|
|
71d7ac81ae | ||
|
|
4271b880c3 | ||
|
|
884ca49340 | ||
|
|
21b27c5aed | ||
|
|
df819a6f98 | ||
|
|
a041e9a7f3 | ||
|
|
2541eeee3d | ||
|
|
7fec55e0ed | ||
|
|
512960e7da | ||
|
|
e482977f09 | ||
|
|
c64e03d9c5 | ||
|
|
5284be6582 | ||
|
|
64203cc208 | ||
|
|
36353296aa | ||
|
|
288e387a22 | ||
|
|
750dde5ed2 | ||
|
|
b609ae2355 | ||
|
|
fdb3755c7d | ||
|
|
f70c55d813 | ||
|
|
cda02f0a88 | ||
|
|
12646eb5a1 | ||
|
|
ba78e7df09 | ||
|
|
2f918a75ad | ||
|
|
43d2fffaf0 | ||
|
|
7bbb119a68 | ||
|
|
5909210cb3 | ||
|
|
d56c1ba972 | ||
|
|
a12a21c02f |
@@ -0,0 +1,47 @@
|
||||
# Security at Omarchy
|
||||
|
||||
## Report a vulnerability
|
||||
|
||||
If you believe you’ve found a security vulnerability in Omarchy, please tell the [Omarchy Security Team](https://omarchy.org/teams/#security) privately so we have an opportunity to investigate and fix it before it is made public.
|
||||
|
||||
[security@omarchy.org](mailto:security@omarchy.org?subject=Security%20report)
|
||||
|
||||
Please don’t report potential vulnerabilities publicly in GitHub Issues, Discord, or social media before they’ve been resolved.
|
||||
|
||||
## What is a vulnerability?
|
||||
|
||||
We consider a bug a security vulnerability when it can be exploited to cross a meaningful security boundary: an untrusted or lower-privileged party gains access, permissions, or control they didn’t already have.
|
||||
|
||||
Code that could be more robust but does not cross a security boundary is an improvement rather than a security vulnerability. We may still merge a proposed fix and credit the reporter in our release notes.
|
||||
|
||||
Eligibility for our [security credits](https://omarchy.org/security/credits/) page depends on whether a report identifies a confirmed security vulnerability, not on its severity.
|
||||
|
||||
## What to include
|
||||
|
||||
Give us enough information to understand and reproduce the issue:
|
||||
|
||||
- The affected component and Omarchy version.
|
||||
- An explanation of what an attacker can do before and after exploitation.
|
||||
- Steps to reproduce the issue and any proof of concept.
|
||||
- Your preferred contact details for follow-up.
|
||||
|
||||
## Responsible disclosure
|
||||
|
||||
Please act in good faith while investigating and reporting vulnerabilities:
|
||||
|
||||
- Only test systems and accounts you own or have explicit permission to test.
|
||||
- Avoid privacy violations, disruption, data destruction, and service degradation.
|
||||
- Don’t exploit a vulnerability beyond what is needed to demonstrate it.
|
||||
- Give us a reasonable opportunity to investigate and address the issue before publishing details.
|
||||
|
||||
We’ll review your report and keep you informed as we’re able while we work toward a resolution.
|
||||
|
||||
## Credits
|
||||
|
||||
Researchers who privately report a confirmed security vulnerability and give us the chance to ship a fix are thanked on the [security credits](https://omarchy.org/security/credits/) page. Accepted improvements that don’t cross a security boundary may still be credited in our release notes.
|
||||
|
||||
Credits link to each reporter’s X profile and show their avatar. For duplicate reports, only the first reporter is eligible for credit.
|
||||
|
||||
## Regular bugs and support
|
||||
|
||||
For anything that isn’t a security vulnerability, please use the [Omarchy issue tracker](https://github.com/omacom/omarchy/issues).
|
||||
@@ -1,6 +1,6 @@
|
||||
# Omarchy
|
||||
|
||||
Omarchy is a beautiful, modern & opinionated Linux distribution by DHH.
|
||||
Omarchy is a beautiful, fun & agentic Linux distribution by DHH.
|
||||
|
||||
Read more at [omarchy.org](https://omarchy.org).
|
||||
|
||||
|
||||
@@ -86,6 +86,13 @@ codex)
|
||||
command=(codex --approve-for-me)
|
||||
[[ -n ${prompt:-} ]] && command+=(-- "$prompt")
|
||||
;;
|
||||
hermes)
|
||||
if [[ -n ${prompt:-} ]]; then
|
||||
command=(env -u HERMES_SESSION_SOURCE hermes chat --yolo --tui "--query=$prompt")
|
||||
else
|
||||
command=(hermes --yolo)
|
||||
fi
|
||||
;;
|
||||
omp)
|
||||
command=(omp --auto-approve)
|
||||
[[ -n ${prompt:-} ]] && command+=(-- "$prompt")
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Set and launch the default coding agent
|
||||
# omarchy:args=[pi|omp|opencode|ori|claude|codex|grok|agy|copilot|crush]
|
||||
# omarchy:args=[pi|omp|opencode|ori|claude|codex|grok|agy|hermes|copilot|crush]
|
||||
# omarchy:examples=omarchy default agent | omarchy default agent codex | omarchy default agent claude
|
||||
|
||||
installing=false
|
||||
@@ -33,20 +33,36 @@ codex) agent="codex"; name="Codex" ;;
|
||||
crush) agent="crush"; name="Crush" ;;
|
||||
grok) agent="grok"; name="Grok"; agent_package="npm:@xai-official/grok" ;;
|
||||
agy | antigravity | antigravity-cli | gemini | gemini-cli) agent="agy"; name="Antigravity"; agent_package="antigravity-cli" ;;
|
||||
hermes) agent="hermes"; name="Hermes"; agent_installer="omarchy-install-hermes-cli" ;;
|
||||
copilot | github-copilot) agent="copilot"; name="GitHub Copilot" ;;
|
||||
*)
|
||||
echo "Usage: omarchy-default-agent <pi|omp|opencode|ori|claude|codex|grok|agy|copilot|crush>"
|
||||
echo "Usage: omarchy-default-agent <pi|omp|opencode|ori|claude|codex|grok|agy|hermes|copilot|crush>"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
agent_package=${agent_package:-$agent}
|
||||
|
||||
if [[ $installing == "false" ]] && ! mise where "$agent_package" &>/dev/null; then
|
||||
# Hermes reaches mise through its own installer rather than straight from
|
||||
# here: it needs its interpreter pinned, and a bare `mise use` has nowhere to
|
||||
# say so. See omarchy-install-hermes-cli.
|
||||
if [[ -n ${agent_installer:-} ]]; then
|
||||
# Not omarchy-cmd-present: the stub is on PATH from first boot and says
|
||||
# nothing about whether Hermes is installed behind it. Treating a cold stub
|
||||
# as installed skips the floating terminal and runs the minute-long install
|
||||
# inside the menu action instead.
|
||||
agent_present() { "$agent_installer" --check; }
|
||||
agent_install() { "$agent_installer" --now; }
|
||||
else
|
||||
agent_present() { mise where "$agent_package" &>/dev/null; }
|
||||
agent_install() { mise use -g "$agent_package"; }
|
||||
fi
|
||||
|
||||
if [[ $installing == "false" ]] && ! agent_present; then
|
||||
exec omarchy-launch-floating-terminal-with-presentation omarchy-default-agent --install "$agent"
|
||||
fi
|
||||
|
||||
if ! mise use -g "$agent_package"; then
|
||||
if ! agent_install; then
|
||||
if [[ $installing == "true" ]]; then
|
||||
echo "Could not install $name with mise" >&2
|
||||
else
|
||||
|
||||
Executable
+26
@@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Install the Hermes desktop app
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
set -e
|
||||
|
||||
# No CLI is installed here on purpose. Hermes Desktop only runs against a
|
||||
# runtime built from its own commit, so it provisions one itself under
|
||||
# ~/.hermes on first launch, which takes a few minutes and shows its own
|
||||
# progress. Handing it the mise CLI instead fails: PyPI trails the tags, and
|
||||
# the version gap fails the app's readiness probe with a 401.
|
||||
echo "Installing Hermes Desktop..."
|
||||
omarchy-pkg-add hermes-desktop
|
||||
|
||||
# If Hermes was already installed for the terminal, the app supersedes it: one
|
||||
# machine, one Hermes. This drops that copy so the terminal, the default agent
|
||||
# and the app all end up on the app's installation.
|
||||
omarchy-install-hermes-cli || true
|
||||
|
||||
echo "Opening Hermes Desktop..."
|
||||
setsid uwsm-app -- /usr/bin/hermes-desktop >/dev/null 2>&1 &
|
||||
|
||||
echo ""
|
||||
echo "Hermes Desktop has been installed."
|
||||
echo "Its first launch installs the Hermes runtime, which takes a few minutes."
|
||||
Executable
+229
@@ -0,0 +1,229 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Install the Hermes CLI as a mise-backed wrapper in ~/.local/bin
|
||||
# omarchy:args=[--check|--now|--owns]
|
||||
# omarchy:examples=omarchy install hermes cli | omarchy install hermes cli --now
|
||||
|
||||
# Hermes pins every one of its dependencies exactly and declares
|
||||
# Requires-Python >=3.11,<3.14, so it can neither be built against Arch's
|
||||
# Python nor share the python-* packages. mise builds it a private environment
|
||||
# instead.
|
||||
#
|
||||
# It gets its own installer rather than a line in omarchy-mise-install because
|
||||
# of the interpreter pin. Given no compatible interpreter to hand, uv builds
|
||||
# the venv against the system Python in violation of Hermes' own bound,
|
||||
# reports success, and leaves the breakage to surface later inside a
|
||||
# dependency -- and omarchy-mise-install writes a fixed stub with nowhere to
|
||||
# say otherwise.
|
||||
#
|
||||
# There is only ever one Hermes on a machine. hermes-desktop cannot run against
|
||||
# this one -- it needs a runtime built from its own commit, and the version gap
|
||||
# fails its readiness probe -- so it installs its own under ~/.hermes and puts
|
||||
# that on PATH. When the package is present it therefore owns Hermes outright:
|
||||
# this installer stands aside and removes its own copy, so the terminal, the
|
||||
# default agent and the app are all the same installation.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
mode=${1:-}
|
||||
|
||||
tool='pipx:hermes-agent[extras=all]'
|
||||
python='3.13'
|
||||
|
||||
# The line that identifies the stub as this installer's; matched whole, so a
|
||||
# wrapper that merely mentions the command is not mistaken for ours.
|
||||
marker='# Written by omarchy-install-hermes-cli.'
|
||||
|
||||
# The package, not the runtime directory: it is installed before the app has
|
||||
# ever run, and that is exactly when we must not start building a second copy.
|
||||
desktop_owns_hermes() {
|
||||
omarchy-pkg-present hermes-desktop
|
||||
}
|
||||
|
||||
# The venv appears at the python-deps stage, several stages before the one that
|
||||
# installs the command, so its presence says nothing about being usable. The
|
||||
# marker is written last, and the command is what the agent actually runs.
|
||||
desktop_hermes_ready() {
|
||||
[[ -f $HOME/.hermes/hermes-agent/.hermes-bootstrap-complete ]] || return 1
|
||||
|
||||
# An executable of that name proves nothing about whose it is; the app's own
|
||||
# points into ~/.hermes, and anything else is not the install we are asking
|
||||
# about. Matched as a plain string, because the path carries a dot and an
|
||||
# unanchored pattern would also claim a wrapper pointing at ~/xhermes.
|
||||
[[ -f $HOME/.local/bin/hermes ]] || return 1
|
||||
grep -qF "$HOME/.hermes" "$HOME/.local/bin/hermes" || return 1
|
||||
|
||||
# And a marker left behind by an install whose venv has since gone answers
|
||||
# for nothing, so the command has to run, exactly as a foreign one must.
|
||||
hermes_prompt_ready
|
||||
}
|
||||
|
||||
# Whether Hermes is really installed, not merely whether the stub exists. A
|
||||
# stub on its own is cold: running it installs Hermes, which takes minutes.
|
||||
installed() {
|
||||
[[ -d "$(mise where "$tool" 2>/dev/null)/hermes-agent/lib/python$python" ]]
|
||||
}
|
||||
|
||||
# The stub is the only thing this installer owns. Anything else at that path
|
||||
# -- Hermes' official installer, a hand-rolled wrapper, even a dangling link
|
||||
# -- was put there by the user and is never deleted or overwritten here.
|
||||
# Symlinks count as foreign even when they resolve to a marked file: the stub
|
||||
# is written as a regular file, so a link is someone else's arrangement.
|
||||
ours() {
|
||||
[[ -f $HOME/.local/bin/hermes && ! -L $HOME/.local/bin/hermes ]] &&
|
||||
grep -qxF "$marker" "$HOME/.local/bin/hermes"
|
||||
}
|
||||
|
||||
foreign_hermes() {
|
||||
[[ -e $HOME/.local/bin/hermes || -L $HOME/.local/bin/hermes ]] && ! ours
|
||||
}
|
||||
|
||||
# A hermes at that path is usable when it is a command that runs: a regular
|
||||
# executable whose --version answers. The executable bit alone proves little -- a directory
|
||||
# passes -x on search permission, and a wrapper whose interpreter or target is
|
||||
# gone passes it too. The desktop app applies the same probe with the same 15
|
||||
# second budget, so what passes here is what it will use.
|
||||
hermes_runs() {
|
||||
[[ -f $HOME/.local/bin/hermes && -x $HOME/.local/bin/hermes ]] &&
|
||||
timeout 15 "$HOME/.local/bin/hermes" --version >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# The chat subcommand's --oneshot opt-out arrived with native interactive -q,
|
||||
# so its presence is a stable capability check without relying on a version.
|
||||
hermes_prompt_ready() {
|
||||
local help
|
||||
hermes_runs &&
|
||||
help=$(timeout 15 "$HOME/.local/bin/hermes" chat --help 2>/dev/null) &&
|
||||
grep -qF -- '--oneshot' <<<"$help"
|
||||
}
|
||||
|
||||
# --owns answers whether the wrapper on PATH is the one this command wrote, so
|
||||
# the migration and Remove Preinstalls do not each carry their own copy of the
|
||||
# marker and drift from it.
|
||||
if [[ $mode == "--owns" ]]; then
|
||||
if ours; then exit 0; else exit 1; fi
|
||||
fi
|
||||
|
||||
# --check lets callers tell a cold stub from a working one before they commit
|
||||
# to a path that assumes Hermes is ready.
|
||||
if [[ $mode == "--check" ]]; then
|
||||
if desktop_owns_hermes; then
|
||||
if desktop_hermes_ready; then exit 0; else exit 1; fi
|
||||
fi
|
||||
# A foreign command is ready only when it also supports prompted sessions;
|
||||
# since it is not ours to replace, nothing this installer does will update it.
|
||||
if foreign_hermes; then
|
||||
if hermes_prompt_ready; then exit 0; else exit 1; fi
|
||||
fi
|
||||
if installed && hermes_prompt_ready; then exit 0; else exit 1; fi
|
||||
fi
|
||||
|
||||
# Hand Hermes over to the app rather than keeping a second copy beside it.
|
||||
if desktop_owns_hermes; then
|
||||
# Not gated on that copy being healthy: `mise up` can rebuild it against the
|
||||
# wrong interpreter and a half-finished install answers to neither test, and
|
||||
# either way it is still a second Hermes. Removing nothing is harmless.
|
||||
if mise where "$tool" >/dev/null 2>&1; then
|
||||
echo "Hermes Desktop provides Hermes; removing the separate CLI install..." >&2
|
||||
fi
|
||||
|
||||
mise rm -g "$tool" >/dev/null 2>&1 || true
|
||||
mise uninstall --all "$tool" >/dev/null 2>&1 || true
|
||||
|
||||
# Our own stub has to go with it. Left in place it still answers `hermes`
|
||||
# until the app's bootstrap overwrites it, and answering means building the
|
||||
# second Hermes this whole arrangement exists to avoid.
|
||||
if ours; then
|
||||
rm -f "$HOME/.local/bin/hermes"
|
||||
fi
|
||||
|
||||
if desktop_hermes_ready; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "Hermes Desktop is installed but has not set Hermes up yet." >&2
|
||||
echo "Launch Hermes Desktop once to finish installing it." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The user already has a hermes of their own. Leave it be: a working one is
|
||||
# what the default agent will run, and a broken one is theirs to fix.
|
||||
if foreign_hermes; then
|
||||
if hermes_prompt_ready; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if hermes_runs; then
|
||||
echo "~/.local/bin/hermes does not support the interactive seeded sessions Omarchy needs." >&2
|
||||
echo "Update it to a Hermes Agent release with interactive chat queries, then run omarchy-install-hermes-cli again." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "~/.local/bin/hermes exists but is not runnable, and it was not installed by Omarchy." >&2
|
||||
echo "Fix or remove it, then run omarchy-install-hermes-cli again." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Only the marked wrapper proves the matching mise environment is ours to replace.
|
||||
if installed && ! hermes_prompt_ready; then
|
||||
if ours; then
|
||||
echo "Updating Hermes for prompted sessions..." >&2
|
||||
mise rm -g "$tool" >/dev/null 2>&1 || true
|
||||
mise uninstall --all "$tool" >/dev/null 2>&1 || true
|
||||
else
|
||||
echo "A Hermes mise environment exists without an Omarchy-owned wrapper." >&2
|
||||
echo "Update or remove it explicitly, then run omarchy-install-hermes-cli again." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p "$HOME/.local/bin"
|
||||
rm -f "$HOME/.local/bin/hermes"
|
||||
|
||||
cat >"$HOME/.local/bin/hermes" <<EOF
|
||||
#!/bin/bash
|
||||
|
||||
$marker
|
||||
|
||||
export UV_PYTHON="$python"
|
||||
|
||||
# Exported rather than set on the install line alone, so the version resolved
|
||||
# to run agrees with the one just installed. Hermes ships several times a week
|
||||
# and mise's cooldown would otherwise hold a new release back for days.
|
||||
export MISE_MINIMUM_RELEASE_AGE=0
|
||||
|
||||
# mise up -- which omarchy update runs -- reinstalls without that pin, so this
|
||||
# asks which interpreter is actually there rather than whether anything is.
|
||||
if ! [[ -d "\$(mise where '$tool' 2>/dev/null)/hermes-agent/lib/python$python" ]]; then
|
||||
echo "Installing Hermes on Python $python (this takes a minute)..." >&2
|
||||
|
||||
# mise's pipx backend shells out to uv, which a stock Omarchy does not have.
|
||||
# It is fetched here rather than when this stub was written, so setting up a
|
||||
# machine that never runs Hermes costs nothing.
|
||||
if omarchy-cmd-missing uv && ! mise where uv >/dev/null 2>&1; then
|
||||
mise use -g --quiet uv@latest || exit 1
|
||||
fi
|
||||
|
||||
mise use -g --quiet --force '$tool' || exit 1
|
||||
fi
|
||||
|
||||
# The pin belongs to building Hermes, not to everything Hermes then runs.
|
||||
# Exported it would reach the agent and every command it shells out to, so a
|
||||
# uv in the user's own project would resolve 3.13 there too -- uv only warns
|
||||
# when that contradicts the project's requires-python, and builds it anyway.
|
||||
exec env -u UV_PYTHON mise x '$tool' -- hermes "\$@"
|
||||
EOF
|
||||
|
||||
chmod +x "$HOME/.local/bin/hermes"
|
||||
|
||||
# The desktop app resolves a hermes on PATH by running `hermes --version` with
|
||||
# a 15 second budget, then falls back to cloning its own copy when that times
|
||||
# out. A first-run mise install does not fit in 15 seconds, so anything that
|
||||
# hands Hermes to the GUI has to install it here rather than leave it stubbed.
|
||||
if [[ $mode == "--now" ]]; then
|
||||
"$HOME/.local/bin/hermes" --version
|
||||
if ! hermes_prompt_ready; then
|
||||
echo "Hermes installed without the interactive seeded sessions Omarchy needs." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
@@ -449,7 +449,7 @@ greeter_screen() {
|
||||
rows=$(stty size 2>/dev/null </dev/tty | awk '{print $1}')
|
||||
[[ $rows =~ ^[0-9]+$ ]] || rows=${LINES:-24}
|
||||
|
||||
tagline="Beautiful, Modern & Opinionated Linux by DHH"
|
||||
tagline="Beautiful, Fun & Agentic Linux by DHH"
|
||||
hint="Press Return to Start Setup"
|
||||
|
||||
printf '%s%s' "$HIDE_CURSOR" "$CLEAR"
|
||||
@@ -677,11 +677,15 @@ user_groups() {
|
||||
if [[ -f $PROVISIONING_DIR/groups ]]; then
|
||||
while IFS= read -r group; do
|
||||
[[ -n $group ]] || continue
|
||||
# Never grant docker at first boot, even if an older install recorded it
|
||||
# (or a factory snapshot predating the opt-in default carries it): the
|
||||
# docker group is root-equivalent. It is opt-in via
|
||||
# omarchy-setup-security-sudoless-docker.
|
||||
# Never replay old privileged group defaults. Docker is always opt-in.
|
||||
# Input is only retained when the factory image has one of the features
|
||||
# whose installer deliberately grants access to raw input devices.
|
||||
[[ $group == "docker" ]] && continue
|
||||
if [[ $group == "input" ]] &&
|
||||
! pacman -Qq xpadneo-dkms &>/dev/null &&
|
||||
! pacman -Qq ydotool &>/dev/null; then
|
||||
continue
|
||||
fi
|
||||
getent group "$group" >/dev/null || continue
|
||||
[[ ",$groups," == *",$group,"* ]] || groups+=",$group"
|
||||
done <"$PROVISIONING_DIR/groups"
|
||||
|
||||
@@ -84,7 +84,7 @@ fi
|
||||
# Dev-aware skill symlinks. Cannot live in /etc/skel because OMARCHY_PATH may
|
||||
# point at a dev checkout (omarchy dev link) where the target differs.
|
||||
# Loops every skill directory, so shipping a new one needs no edit here.
|
||||
mkdir -p ~/.agents/skills ~/.claude/skills ~/.codex/skills ~/.pi/agent/skills ~/.gemini/config/skills
|
||||
mkdir -p ~/.agents/skills ~/.claude/skills ~/.codex/skills ~/.pi/agent/skills ~/.gemini/config/skills ~/.hermes/skills
|
||||
for skill in "$OMARCHY_PATH"/default/agents/skills/*/; do
|
||||
skill=${skill%/}
|
||||
name=${skill##*/}
|
||||
@@ -93,6 +93,14 @@ for skill in "$OMARCHY_PATH"/default/agents/skills/*/; do
|
||||
ln -sfn "$skill" ~/.codex/skills/"$name"
|
||||
ln -sfn "$skill" ~/.pi/agent/skills/"$name"
|
||||
ln -sfn "$skill" ~/.gemini/config/skills/"$name"
|
||||
ln -sfn "$skill" ~/.hermes/skills/"$name"
|
||||
if [[ -d ~/.hermes/profiles ]]; then
|
||||
for profile in ~/.hermes/profiles/*/; do
|
||||
[[ -d $profile ]] || continue
|
||||
mkdir -p "$profile/skills"
|
||||
ln -sfn "$skill" "$profile/skills/$name"
|
||||
done
|
||||
fi
|
||||
done
|
||||
|
||||
mkdir -p ~/Downloads ~/Pictures ~/Videos ~/.config/gtk-3.0
|
||||
|
||||
Executable
+59
@@ -0,0 +1,59 @@
|
||||
#!/bin/bash
|
||||
|
||||
# omarchy:summary=Remove the Hermes desktop app along with the Hermes runtime it installed.
|
||||
# omarchy:requires-sudo=true
|
||||
|
||||
# -u so an unset HOME is an error rather than a set of rm -rf paths rooted at /.
|
||||
set -euo pipefail
|
||||
|
||||
omarchy-pkg-drop hermes-desktop
|
||||
|
||||
# The app writes this when the runtime it provisions under ~/.hermes has landed,
|
||||
# and it is the only thing that tells that runtime apart from one the user
|
||||
# installed themselves -- the paths are the same either way. Without it the app
|
||||
# never got that far: a machine where it was installed but never launched still
|
||||
# has whatever was there before, and none of it is ours to delete.
|
||||
if [[ -f $HOME/.hermes/hermes-agent/.hermes-bootstrap-complete ]]; then
|
||||
# The checkout and venv, its own uv, its own node. None of it is any use once
|
||||
# the app is gone. Not ~/.config/Hermes, which holds the gateway connections
|
||||
# and their encrypted tokens, the active profile and the update settings. Not
|
||||
# the rest of ~/.hermes either: the chats, memories and the skills Hermes
|
||||
# wrote for itself are the user's, they are small, and finding them still
|
||||
# there after a reinstall is the better surprise.
|
||||
rm -rf \
|
||||
"$HOME/.hermes/hermes-agent" \
|
||||
"$HOME/.hermes/bootstrap-cache" \
|
||||
"$HOME/.hermes/bin" \
|
||||
"$HOME/.hermes/node"
|
||||
|
||||
# Only the wrappers pointing into ~/.hermes, matched as a plain string: the
|
||||
# path carries a dot, so an unanchored pattern would also claim a wrapper
|
||||
# pointing at a sibling like ~/xhermes.
|
||||
for command in hermes hermes-agent hermes-acp; do
|
||||
wrapper="$HOME/.local/bin/$command"
|
||||
|
||||
if [[ -f $wrapper && ! -L $wrapper ]] && grep -qF "$HOME/.hermes" "$wrapper"; then
|
||||
rm -f "$wrapper"
|
||||
fi
|
||||
done
|
||||
|
||||
# When Hermes brought its own Node it symlinked these next to its own commands,
|
||||
# and they point at what we just deleted. Only the links into ~/.hermes: a
|
||||
# system Node, or someone else's, lives somewhere else entirely.
|
||||
for command in node npm npx; do
|
||||
link="$HOME/.local/bin/$command"
|
||||
|
||||
if [[ -L $link && $(readlink "$link") == "$HOME/.hermes"/* ]]; then
|
||||
rm -f "$link"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "Hermes Desktop has been removed."
|
||||
echo "Your chats, memories, and skills are still in ~/.hermes,"
|
||||
echo "and your connections and settings in ~/.config/Hermes."
|
||||
else
|
||||
echo ""
|
||||
echo "Hermes Desktop has been removed."
|
||||
echo "It never finished installing its own Hermes, so nothing in ~/.hermes was touched."
|
||||
fi
|
||||
@@ -17,6 +17,14 @@ if gum confirm "Are you sure you want to remove all preinstalled web apps, TUI w
|
||||
~/.local/bin/gh ~/.local/bin/opencode ~/.local/bin/playwright ~/.local/bin/playwright-cli ~/.local/bin/pi \
|
||||
~/.local/bin/omp ~/.local/bin/ori ~/.local/bin/grok ~/.local/bin/crush ~/.local/bin/ghui ~/.local/bin/hunk
|
||||
|
||||
# Only the wrapper omarchy-install-hermes-cli wrote is a preinstall. Hermes
|
||||
# Desktop's command, an official install, or anything else at that path is
|
||||
# the user's, so it is the installer that decides whether the wrapper is its
|
||||
# own, rather than a copy of its marker kept here.
|
||||
if omarchy-install-hermes-cli --owns; then
|
||||
rm -f ~/.local/bin/hermes
|
||||
fi
|
||||
|
||||
omarchy-pkg-drop \
|
||||
aether \
|
||||
cliamp \
|
||||
|
||||
@@ -143,6 +143,50 @@ authorize_pasted_key() {
|
||||
authorize_key "$key" || exit 1
|
||||
}
|
||||
|
||||
# Only called after a key is authorized. Disabling password authentication
|
||||
# before then could lock the owner out of the machine.
|
||||
disable_password_auth() {
|
||||
local config=/etc/ssh/sshd_config.d/10-omarchy-hardening.conf
|
||||
local effective_config
|
||||
|
||||
if [[ ! -s $AUTHORIZED_KEYS ]]; then
|
||||
echo -e "\e[31mCannot disable SSH password authentication without an authorized key.\e[0m" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "Disabling SSH password authentication, now that a key is authorized..."
|
||||
sudo install -Dm644 /dev/stdin "$config" <<'CONF'
|
||||
# Written by omarchy-setup-security-sshd once an SSH key was authorized.
|
||||
# Delete this file and reload sshd to allow password logins again.
|
||||
PasswordAuthentication no
|
||||
KbdInteractiveAuthentication no
|
||||
CONF
|
||||
|
||||
# Validate before reloading: a config sshd rejects would otherwise take the
|
||||
# service down on its next restart, potentially stranding a remote owner.
|
||||
if ! sudo sshd -t; then
|
||||
echo -e "\e[31msshd rejected the hardening config; removing it and leaving passwords on.\e[0m" >&2
|
||||
sudo rm -f "$config"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Syntax alone is insufficient because sshd uses the first value it reads for
|
||||
# these settings. An earlier administrator rule could leave passwords enabled.
|
||||
# Match keywords case-insensitively: OpenSSH 9.x dumps them lowercase, 10.x
|
||||
# in CamelCase.
|
||||
if ! effective_config=$(sudo sshd -T) ||
|
||||
! grep -qixF "passwordauthentication no" <<<"$effective_config" ||
|
||||
! grep -qixF "kbdinteractiveauthentication no" <<<"$effective_config"; then
|
||||
echo -e "\e[31msshd did not apply the password-authentication restrictions; removing the ineffective config.\e[0m" >&2
|
||||
sudo rm -f "$config"
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Reload rather than restart so an administrator already connected keeps
|
||||
# their session.
|
||||
sudo systemctl reload sshd.service
|
||||
}
|
||||
|
||||
echo -e "\e[32mSetting up SSH server access with key-based authentication.\n\e[0m"
|
||||
|
||||
setup_sshd
|
||||
@@ -161,5 +205,8 @@ else
|
||||
esac
|
||||
fi
|
||||
|
||||
disable_password_auth
|
||||
|
||||
echo -e "\e[32m\nPerfect! The SSH server is running and your key is authorized.\e[0m"
|
||||
echo "Password logins are off; this machine now accepts authorized keys only."
|
||||
echo "You can now connect with: ssh $USER@$(hostname)"
|
||||
|
||||
@@ -5,3 +5,9 @@
|
||||
# omarchy:examples=omarchy toggle bar | omarchy toggle bar off | omarchy toggle bar on
|
||||
|
||||
omarchy-toggle bar-off "${1:-toggle}"
|
||||
|
||||
# The shell's watch on the toggles directory can miss flag changes that land in
|
||||
# quick succession, stranding the bar off screen until the shell restarts.
|
||||
# Nudge the bar to re-read the flag; quiet best-effort so the toggle still
|
||||
# works when the shell is not up.
|
||||
omarchy-shell -q omarchy.bar syncHidden
|
||||
|
||||
@@ -14,6 +14,8 @@ fi
|
||||
|
||||
if grep -q "https://pkgs.omarchy.org/stable/" /etc/pacman.conf; then
|
||||
pkgs="stable"
|
||||
elif grep -q "https://pkgs.omarchy.org/rc/" /etc/pacman.conf; then
|
||||
pkgs="rc"
|
||||
elif grep -q "https://pkgs.omarchy.org/edge/" /etc/pacman.conf; then
|
||||
pkgs="edge"
|
||||
else
|
||||
|
||||
@@ -13,7 +13,7 @@ description: >
|
||||
|
||||
# Omarchy Skill
|
||||
|
||||
Manage [Omarchy](https://omarchy.org/) Linux systems - a beautiful, modern, opinionated Arch Linux distribution with Hyprland.
|
||||
Manage [Omarchy](https://omarchy.org/) Linux systems - a beautiful, fun, agentic Arch Linux distribution with Hyprland.
|
||||
|
||||
This skill is for end-user customization on installed systems.
|
||||
It is not for contributing to Omarchy source code.
|
||||
|
||||
@@ -1,313 +1,6 @@
|
||||
<?xml version="1.0"?>
|
||||
<!DOCTYPE fontconfig SYSTEM "fonts.dtd">
|
||||
<fontconfig>
|
||||
<!-- CJK: give language-tagged text the Noto CJK variant that matches its
|
||||
language. noto-fonts-cjk ships all five, Han glyph shapes differ between
|
||||
them, and the generic assigns below would otherwise hand tagged CJK text
|
||||
to families with no Han coverage, leaving the variant to a charset-scan
|
||||
lottery. Chinese arrives under many tags (W3C recommends the zh-Hans and
|
||||
zh-Hant script forms), so the tags are first folded onto one tag per
|
||||
variant. Order is load-bearing: a bare zh and a bare zh-hant satisfy
|
||||
the contains test of every one of their extensions, so both are pinned
|
||||
by exact matches before the extension folds run. Cantonese under its
|
||||
own primary tag folds to Hong Kong forms, except explicitly
|
||||
Simplified Cantonese, which folds to SC. -->
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="eq">
|
||||
<string>zh</string>
|
||||
</test>
|
||||
<edit name="lang" mode="assign">
|
||||
<string>zh-cn</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="eq">
|
||||
<string>zh-hant</string>
|
||||
</test>
|
||||
<edit name="lang" mode="assign">
|
||||
<string>zh-tw</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>zh-hant-hk</string>
|
||||
</test>
|
||||
<edit name="lang" mode="assign">
|
||||
<string>zh-hk</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>zh-hant-mo</string>
|
||||
</test>
|
||||
<edit name="lang" mode="assign">
|
||||
<string>zh-hk</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>zh-hant</string>
|
||||
</test>
|
||||
<edit name="lang" mode="assign">
|
||||
<string>zh-tw</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>zh-hans</string>
|
||||
</test>
|
||||
<edit name="lang" mode="assign">
|
||||
<string>zh-cn</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>zh-sg</string>
|
||||
</test>
|
||||
<edit name="lang" mode="assign">
|
||||
<string>zh-cn</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>zh-mo</string>
|
||||
</test>
|
||||
<edit name="lang" mode="assign">
|
||||
<string>zh-hk</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="eq">
|
||||
<string>yue</string>
|
||||
</test>
|
||||
<edit name="lang" mode="assign">
|
||||
<string>zh-hk</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>yue-hans</string>
|
||||
</test>
|
||||
<edit name="lang" mode="assign">
|
||||
<string>zh-cn</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>yue-cn</string>
|
||||
</test>
|
||||
<edit name="lang" mode="assign">
|
||||
<string>zh-cn</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>yue</string>
|
||||
</test>
|
||||
<edit name="lang" mode="assign">
|
||||
<string>zh-hk</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
|
||||
<!-- Each variant is then prepended for its tag, ahead of the assigns below
|
||||
because these test the generic names those assigns replace. Unlike the
|
||||
Arabic rule further down, these carry a family test, so the prepend
|
||||
lands just ahead of the matched generic and a concrete family the app
|
||||
asked for stays in front of the variant. -->
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>zh-cn</string>
|
||||
</test>
|
||||
<test name="family">
|
||||
<string>sans-serif</string>
|
||||
</test>
|
||||
<edit name="family" mode="prepend" binding="strong">
|
||||
<string>Noto Sans CJK SC</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>zh-cn</string>
|
||||
</test>
|
||||
<test name="family">
|
||||
<string>serif</string>
|
||||
</test>
|
||||
<edit name="family" mode="prepend" binding="strong">
|
||||
<string>Noto Serif CJK SC</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>zh-cn</string>
|
||||
</test>
|
||||
<test name="family">
|
||||
<string>monospace</string>
|
||||
</test>
|
||||
<edit name="family" mode="prepend" binding="strong">
|
||||
<string>Noto Sans Mono CJK SC</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>zh-hk</string>
|
||||
</test>
|
||||
<test name="family">
|
||||
<string>sans-serif</string>
|
||||
</test>
|
||||
<edit name="family" mode="prepend" binding="strong">
|
||||
<string>Noto Sans CJK HK</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>zh-hk</string>
|
||||
</test>
|
||||
<test name="family">
|
||||
<string>serif</string>
|
||||
</test>
|
||||
<edit name="family" mode="prepend" binding="strong">
|
||||
<string>Noto Serif CJK HK</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>zh-hk</string>
|
||||
</test>
|
||||
<test name="family">
|
||||
<string>monospace</string>
|
||||
</test>
|
||||
<edit name="family" mode="prepend" binding="strong">
|
||||
<string>Noto Sans Mono CJK HK</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>zh-tw</string>
|
||||
</test>
|
||||
<test name="family">
|
||||
<string>sans-serif</string>
|
||||
</test>
|
||||
<edit name="family" mode="prepend" binding="strong">
|
||||
<string>Noto Sans CJK TC</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>zh-tw</string>
|
||||
</test>
|
||||
<test name="family">
|
||||
<string>serif</string>
|
||||
</test>
|
||||
<edit name="family" mode="prepend" binding="strong">
|
||||
<string>Noto Serif CJK TC</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>zh-tw</string>
|
||||
</test>
|
||||
<test name="family">
|
||||
<string>monospace</string>
|
||||
</test>
|
||||
<edit name="family" mode="prepend" binding="strong">
|
||||
<string>Noto Sans Mono CJK TC</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>ja</string>
|
||||
</test>
|
||||
<test name="family">
|
||||
<string>sans-serif</string>
|
||||
</test>
|
||||
<edit name="family" mode="prepend" binding="strong">
|
||||
<string>Noto Sans CJK JP</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>ja</string>
|
||||
</test>
|
||||
<test name="family">
|
||||
<string>serif</string>
|
||||
</test>
|
||||
<edit name="family" mode="prepend" binding="strong">
|
||||
<string>Noto Serif CJK JP</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>ja</string>
|
||||
</test>
|
||||
<test name="family">
|
||||
<string>monospace</string>
|
||||
</test>
|
||||
<edit name="family" mode="prepend" binding="strong">
|
||||
<string>Noto Sans Mono CJK JP</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>ko</string>
|
||||
</test>
|
||||
<test name="family">
|
||||
<string>sans-serif</string>
|
||||
</test>
|
||||
<edit name="family" mode="prepend" binding="strong">
|
||||
<string>Noto Sans CJK KR</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>ko</string>
|
||||
</test>
|
||||
<test name="family">
|
||||
<string>serif</string>
|
||||
</test>
|
||||
<edit name="family" mode="prepend" binding="strong">
|
||||
<string>Noto Serif CJK KR</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="lang" compare="contains">
|
||||
<string>ko</string>
|
||||
</test>
|
||||
<test name="family">
|
||||
<string>monospace</string>
|
||||
</test>
|
||||
<edit name="family" mode="prepend" binding="strong">
|
||||
<string>Noto Sans Mono CJK KR</string>
|
||||
</edit>
|
||||
</match>
|
||||
|
||||
<match target="pattern">
|
||||
<test name="family" qual="any">
|
||||
<string>sans-serif</string>
|
||||
|
||||
@@ -12,6 +12,7 @@ The private-use glyphs in `omarchy.ttf` are:
|
||||
- `U+E907` — Ollama, from <https://simpleicons.org/icons/ollama.svg>
|
||||
- `U+E908` — T3 Code, traced from the app icon in <https://aur.archlinux.org/cgit/aur.git/plain/t3code-icon.png?h=t3code-bin>, since upstream publishes no monochrome SVG
|
||||
- `U+E909` — Ori, from <https://openrouter.ai/brand/v2/openrouter-glyph-dark.svg>, OpenRouter's own mark: Ori ships no separate logo and its product page uses this one
|
||||
- `U+E90A` — Hermes, Font Awesome's staff-snake (CC BY 4.0) from <https://fontawesome.com/icons/staff-snake>, the mark Hermes serves as its favicon: their app icon is a portrait that reads as a smudge at menu size
|
||||
|
||||
The agent marks are monochrome so the menu can render them using the active
|
||||
theme's foreground and selection colors.
|
||||
|
||||
Binary file not shown.
@@ -0,0 +1,7 @@
|
||||
-- Hermes Desktop's frameless HUD manages its own geometry.
|
||||
o.window({ class = "^Hermes$", title = "^Hermes HUD$" }, {
|
||||
tag = "-default-opacity",
|
||||
float = true,
|
||||
border_size = 0,
|
||||
opacity = "1 1",
|
||||
})
|
||||
@@ -141,6 +141,7 @@
|
||||
"setup.default.agent.copilot": {"icon":"","label":"Copilot","checked":"[[ \"$(omarchy-default-agent)\" == \"copilot\" ]]","action":"omarchy-default-agent copilot"},
|
||||
"setup.default.agent.crush": {"icon":"","label":"Crush","checked":"[[ \"$(omarchy-default-agent)\" == \"crush\" ]]","action":"omarchy-default-agent crush"},
|
||||
"setup.default.agent.grok": {"icon":"","iconFont":"omarchy","label":"Grok","checked":"[[ \"$(omarchy-default-agent)\" == \"grok\" ]]","action":"omarchy-default-agent grok"},
|
||||
"setup.default.agent.hermes": {"icon":"","iconFont":"omarchy","label":"Hermes","checked":"[[ \"$(omarchy-default-agent)\" == \"hermes\" ]]","action":"omarchy-default-agent hermes"},
|
||||
"setup.default.agent.omp": {"icon":"","iconFont":"omarchy","label":"omp","checked":"[[ \"$(omarchy-default-agent)\" == \"omp\" ]]","action":"omarchy-default-agent omp"},
|
||||
"setup.default.agent.opencode": {"icon":"","iconFont":"omarchy","label":"OpenCode","checked":"[[ \"$(omarchy-default-agent)\" == \"opencode\" ]]","action":"omarchy-default-agent opencode"},
|
||||
"setup.default.agent.ori": {"icon":"","iconFont":"omarchy","label":"Ori","checked":"[[ \"$(omarchy-default-agent)\" == \"ori\" ]]","action":"omarchy-default-agent ori"},
|
||||
@@ -239,6 +240,7 @@
|
||||
"install.ai.chatgpt": {"icon":"","iconFont":"omarchy","label":"ChatGPT Desktop","disabled":"omarchy-pkg-present openai-codex-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-ai-chatgpt"},
|
||||
"install.ai.dictation": {"icon":"","label":"Dictation","disabled":"omarchy-pkg-present voxtype-bin","action":"omarchy-launch-floating-terminal-with-presentation omarchy-voxtype-install"},
|
||||
"install.ai.grok-bot": {"icon":"","iconFont":"omarchy","label":"Grok Bot","disabled":"omarchy-pkg-present grok-bot","action":"omarchy-install-and-launch 'Grok Bot' grok-bot grok-bot"},
|
||||
"install.ai.hermes": {"icon":"","iconFont":"omarchy","label":"Hermes Desktop","disabled":"omarchy-pkg-present hermes-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-install-ai-hermes"},
|
||||
"install.ai.lm-studio": {"icon":"","iconFont":"omarchy","label":"LM Studio","disabled":"omarchy-pkg-present lmstudio-bin","action":"omarchy-install-app 'LM Studio' lmstudio-bin"},
|
||||
"install.ai.ollama": {"icon":"","iconFont":"omarchy","label":"Ollama","disabled":"omarchy-cmd-present ollama","action":"if omarchy-cmd-present nvidia-smi; then ollama_pkg=ollama-cuda; elif omarchy-cmd-present rocminfo; then ollama_pkg=ollama-rocm; else ollama_pkg=ollama; fi; omarchy-install-app Ollama \"$ollama_pkg\""},
|
||||
"install.ai.t3-code": {"icon":"","iconFont":"omarchy","label":"T3 Code","disabled":"omarchy-pkg-present t3code-bin","action":"omarchy-install-and-launch 'T3 Code' t3code-bin t3code"},
|
||||
@@ -292,6 +294,7 @@
|
||||
"remove.security.fido2": {"icon":"","label":"Fido2","when":"omarchy-pkg-present pam-u2f","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-fido2"},
|
||||
"remove.security.sshd": {"icon":"","label":"SSHD","when":"systemctl is-enabled --quiet sshd","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-sshd"},
|
||||
"remove.security.sudoless-docker": {"icon":"","label":"Sudoless Docker","when":"! omarchy-sudo-docker --configured","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-security-sudoless-docker"},
|
||||
"remove.ai.hermes": {"icon":"","iconFont":"omarchy","label":"Hermes Desktop","when":"omarchy-pkg-present hermes-desktop","action":"omarchy-launch-floating-terminal-with-presentation omarchy-remove-ai-hermes"},
|
||||
"remove.browser.chrome": {"icon":"","label":"Chrome","when":"omarchy-pkg-present google-chrome","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-remove-browser chrome'"},
|
||||
"remove.browser.edge": {"icon":"","label":"Edge","when":"omarchy-pkg-present microsoft-edge-stable-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-remove-browser edge'"},
|
||||
"remove.browser.brave": {"icon":"","label":"Brave","when":"omarchy-pkg-present brave-bin","action":"omarchy-launch-floating-terminal-with-presentation 'omarchy-remove-browser brave'"},
|
||||
|
||||
@@ -26,4 +26,4 @@ Include = /etc/pacman.d/mirrorlist
|
||||
Include = /etc/pacman.d/mirrorlist
|
||||
|
||||
[omarchy]
|
||||
Server = https://pkgs.omarchy.org/edge/$arch
|
||||
Server = https://pkgs.omarchy.org/rc/$arch
|
||||
|
||||
+1
-5
@@ -198,11 +198,7 @@ Runs once per user. It does **not** copy `~/.config/**`, `~/.bashrc`,
|
||||
`flags.lua`, or the nautilus extensions — `/etc/skel` already seeded those.
|
||||
It only does the things `/etc/skel` can't:
|
||||
|
||||
- Skill symlinks `~/.{agents,claude,codex,pi/agent}/skills/<name>` →
|
||||
`$OMARCHY_PATH/default/agents/skills/<name>`, looping over every skill
|
||||
directory there (currently `omarchy` and `diagnose-crash`) so new skills
|
||||
need no edit. Symlinks (not copies) so `omarchy dev link` against a dev
|
||||
checkout repoints them correctly.
|
||||
- Skill symlinks into `~/.agents/skills/<name>`, `~/.claude/skills/<name>`, `~/.codex/skills/<name>`, `~/.pi/agent/skills/<name>`, `~/.gemini/config/skills/<name>` (Antigravity), `~/.hermes/skills/<name>`, and each existing `~/.hermes/profiles/*/skills/<name>` → `$OMARCHY_PATH/default/agents/skills/<name>`, looping over every skill directory there (currently `omarchy` and `diagnose-crash`) so new skills need no edit. Symlinks (not copies) so `omarchy dev link` against a dev checkout repoints them correctly. Hermes profile dirs are only linked when they already exist — provision does not create Hermes profiles.
|
||||
- `xdg-user-dirs-update` (Templates/Public/Desktop folded back into `$HOME`)
|
||||
and `~/.config/gtk-3.0/bookmarks` (needs `$HOME` expansion).
|
||||
- Hyprland's package-owned default input reads `XKBLAYOUT` / `XKBVARIANT`
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
%wheel ALL=(ALL) NOPASSWD: /usr/bin/asdcontrol
|
||||
@@ -4,7 +4,6 @@ run_logged "$OMARCHY_INSTALL/hardware/dell-xps-touchpad-haptics.sh"
|
||||
run_logged "$OMARCHY_INSTALL/hardware/surface.sh"
|
||||
|
||||
run_logged "$OMARCHY_INSTALL/hardware/network.sh"
|
||||
run_logged "$OMARCHY_INSTALL/hardware/input-group.sh"
|
||||
run_logged "$OMARCHY_INSTALL/hardware/set-wireless-regdom.sh"
|
||||
run_logged "$OMARCHY_INSTALL/hardware/fix-fkeys.sh"
|
||||
run_logged "$OMARCHY_INSTALL/hardware/fix-synaptic-touchpad.sh"
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
# Give this user privileged input access for dictation tools + xbox controllers to work.
|
||||
# Recorded for provisioning first-boot user creation and factory reset, granted directly
|
||||
# when the install user already exists (deferred-provisioning installs create the user at
|
||||
# first boot instead).
|
||||
provisioning_dir="${OMARCHY_PROVISIONING_DIR:-/var/lib/omarchy/provisioning}"
|
||||
mkdir -p "$provisioning_dir"
|
||||
grep -qxF input "$provisioning_dir/groups" 2>/dev/null || echo input >>"$provisioning_dir/groups"
|
||||
|
||||
if [[ -n ${OMARCHY_INSTALL_USER:-} ]] && getent passwd "$OMARCHY_INSTALL_USER" >/dev/null; then
|
||||
usermod -aG input "$OMARCHY_INSTALL_USER"
|
||||
fi
|
||||
@@ -13,3 +13,9 @@ omarchy-mise-install npm:@kitlangton/ghui ghui
|
||||
omarchy-mise-install aqua:modem-dev/hunk hunk
|
||||
omarchy-mise-install github:basecamp/hey-cli hey
|
||||
omarchy-mise-install github:OpenRouterLabs/ori-releases ori
|
||||
# Every line above writes a stub and cannot fail. This one can: it exits
|
||||
# non-zero when Hermes Desktop owns Hermes but has not finished setting it up,
|
||||
# and this leaf is sourced under `bash -eE`, so that would abort the rest of
|
||||
# omarchy-provision-user -- the default browser, the mailto handler and the
|
||||
# finalize-user marker all come after it.
|
||||
omarchy-install-hermes-cli || true
|
||||
|
||||
+5
-2
@@ -14,6 +14,7 @@ Omarchy treats AI coding agents as first-class citizens, but it doesn't pick a f
|
||||
| `pi` | [Mario Zechner's Pi](https://github.com/badlogic/pi-mono) |
|
||||
| `omp` | [Oh My Pi](https://github.com/can1357/oh-my-pi) |
|
||||
| `ori` | [Ori](https://openrouter.ai/docs/guides/ori/harness), OpenRouter's harness |
|
||||
| `hermes` | [Hermes](https://hermes-agent.nousresearch.com/), Nous Research's agent |
|
||||
|
||||
`ori` is the odd one out: it runs the other harnesses against OpenRouter's whole model catalog, so `ori claude`, `ori codex`, or `ori opencode` start those agents on whichever model you point them at, and `ori code` is Ori's own agent.
|
||||
|
||||
@@ -43,7 +44,9 @@ Crashes can also be silenced one program at a time, which is what the diagnosis
|
||||
|
||||
### Desktop apps
|
||||
|
||||
The _Install > AI_ menu also carries a couple of graphical AI apps: the ChatGPT desktop app, and Grok Bot for chatting with xAI's models.
|
||||
The _Install > AI_ menu also carries a few graphical AI apps: the ChatGPT desktop app, Grok Bot for chatting with xAI's models, and Hermes Desktop.
|
||||
|
||||
Hermes Desktop is the one to know about, because there is only ever one Hermes on a machine. The app only runs against a runtime built from its own commit, so it installs one of its own under `~/.hermes` on first launch, which takes a few minutes and shows its own progress. From then on that is the Hermes the terminal `hermes` command and the default agent use too, whichever order you installed them in. Removing the app under _Remove > AI_ takes that runtime with it, and keeps your chats, memories, and the skills Hermes wrote for itself.
|
||||
|
||||
### Local LLMs
|
||||
|
||||
@@ -51,6 +54,6 @@ Omarchy recommends two ways of running local LLM models: LM Studio and Ollama. L
|
||||
|
||||
### The Omarchy Skill
|
||||
|
||||
Agent skills help AI use specific tools in a specific way, and Omarchy ships with a default skill for tailoring the system. Like tweaking your Hyprland config, adjusting the bar, or even creating a new theme from scratch. It's symlinked into the skill directories for Claude Code (`~/.claude/skills`), Codex (`~/.codex/skills`), Pi (`~/.pi/agent/skills`), Antigravity (`~/.gemini/config/skills`), and the generic `~/.agents/skills` location, so most harnesses pick it up automatically.
|
||||
Agent skills help AI use specific tools in a specific way, and Omarchy ships with a default skill for tailoring the system. Like tweaking your Hyprland config, adjusting the bar, or even creating a new theme from scratch. It's symlinked into the skill directories for Claude Code (`~/.claude/skills`), Codex (`~/.codex/skills`), Pi (`~/.pi/agent/skills`), Antigravity (`~/.gemini/config/skills`), Hermes (`~/.hermes/skills` and each `~/.hermes/profiles/*/skills`), and the generic `~/.agents/skills` location, so most harnesses pick it up automatically.
|
||||
|
||||
But you should treat this skill as experimental. Different models will use it to different effect. It's best to run in plan mode first, so you have an idea of what the agent would like to change. And then be ready to rollback changes or even invoking `omarchy reinstall configs`, if the agent makes a mess of everything.
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
echo "Install the Hermes CLI wrapper for existing installs"
|
||||
|
||||
# Users who removed the preinstalls opted out of the mise wrappers, and Hermes
|
||||
# is one of them.
|
||||
[[ -f $HOME/.local/state/omarchy/preinstalls-removed ]] && exit 0
|
||||
|
||||
# Hermes Desktop provides its own Hermes. The installer stands aside for it,
|
||||
# removing the mise copy and the Omarchy wrapper an earlier install may have
|
||||
# left beside the app. It also reports when the app has not finished setting
|
||||
# Hermes up, which is the app's to finish, not this migration's to fail on.
|
||||
if omarchy-pkg-present hermes-desktop; then
|
||||
omarchy-install-hermes-cli || true
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Anything already answering to hermes that this installer did not write --
|
||||
# an official install, a hand-rolled wrapper, even a dangling link -- belongs to
|
||||
# the user and stays exactly as it is. The installer is asked rather than
|
||||
# matched against here, so there is one answer to who owns that wrapper.
|
||||
wrapper="$HOME/.local/bin/hermes"
|
||||
if [[ -e $wrapper || -L $wrapper ]] && ! omarchy-install-hermes-cli --owns; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
omarchy-install-hermes-cli
|
||||
@@ -0,0 +1,22 @@
|
||||
echo "Link Omarchy agent skills into Hermes skill directories"
|
||||
|
||||
OMARCHY_PATH="${OMARCHY_PATH:-/usr/share/omarchy}"
|
||||
skills_source="$OMARCHY_PATH/default/agents/skills"
|
||||
|
||||
[[ -d $skills_source ]] || exit 0
|
||||
|
||||
mkdir -p "$HOME/.hermes/skills"
|
||||
|
||||
for skill in "$skills_source"/*/; do
|
||||
[[ -d $skill ]] || continue
|
||||
name=${skill%/}
|
||||
name=${name##*/}
|
||||
ln -sfn "$skills_source/$name" "$HOME/.hermes/skills/$name"
|
||||
if [[ -d $HOME/.hermes/profiles ]]; then
|
||||
for profile in "$HOME"/.hermes/profiles/*/; do
|
||||
[[ -d $profile ]] || continue
|
||||
mkdir -p "$profile/skills"
|
||||
ln -sfn "$skills_source/$name" "$profile/skills/$name"
|
||||
done
|
||||
fi
|
||||
done
|
||||
@@ -0,0 +1,18 @@
|
||||
echo "Drop the default input group grant, which allowed unprivileged keylogging"
|
||||
|
||||
# Membership of `input` gives raw read/write access to /dev/input/event*: any
|
||||
# process running as the user can capture keystrokes and synthesize input. The
|
||||
# blanket grant is unnecessary: the Xbox-controller and ydotool installers add
|
||||
# the group themselves when those features are deliberately installed.
|
||||
#
|
||||
# Preserve membership where one of those opt-in features is present; removing
|
||||
# it there would break the feature the user chose to install.
|
||||
if id -nG "$USER" | grep -qw input; then
|
||||
if pacman -Qq xpadneo-dkms &>/dev/null || pacman -Qq ydotool &>/dev/null; then
|
||||
echo "Keeping $USER in the input group: controller or ydotool support is installed."
|
||||
else
|
||||
sudo gpasswd -d "$USER" input >/dev/null
|
||||
echo "Removed $USER from the input group. Log out and back in to apply."
|
||||
omarchy-state set reboot-required
|
||||
fi
|
||||
fi
|
||||
@@ -0,0 +1,12 @@
|
||||
echo "Point rc-channel installs at the rc package repository"
|
||||
|
||||
# pacman-rc.conf shipped with [omarchy] pointing at the edge repository, a
|
||||
# leftover from when release candidates published there. Candidates now publish
|
||||
# to the dedicated rc channel, so a machine on the rc mirror was taking its
|
||||
# omarchy packages from edge. Repoint only a conf that still carries the
|
||||
# shipped pairing: an administrator who chose another combination keeps it.
|
||||
if grep -q "https://rc-mirror.omarchy.org/" /etc/pacman.d/mirrorlist &&
|
||||
grep -q "^Server = https://pkgs.omarchy.org/edge/" /etc/pacman.conf; then
|
||||
sudo sed -i "s|^Server = https://pkgs.omarchy.org/edge/|Server = https://pkgs.omarchy.org/rc/|" /etc/pacman.conf
|
||||
echo "Switched the [omarchy] repository to the rc channel to match this machine's rc mirror."
|
||||
fi
|
||||
@@ -0,0 +1,133 @@
|
||||
echo "Disable SSH password authentication, or sshd itself when no key is authorized"
|
||||
|
||||
config=/etc/ssh/sshd_config.d/10-omarchy-hardening.conf
|
||||
authorized_keys="$HOME/.ssh/authorized_keys"
|
||||
|
||||
as_root() {
|
||||
if (( EUID == 0 )); then
|
||||
"$@"
|
||||
else
|
||||
sudo "$@"
|
||||
fi
|
||||
}
|
||||
|
||||
# Passwords staying enabled is the state the machine has been living with, so a
|
||||
# condition this migration cannot repair completes with a notice instead of
|
||||
# failing and holding up every migration queued behind it. Only missing
|
||||
# privileges stay pending below, because rerunning from a terminal fixes that.
|
||||
skip() {
|
||||
echo "$1 SSH password authentication remains enabled; run omarchy-setup-security-sshd to harden manually."
|
||||
exit 0
|
||||
}
|
||||
|
||||
# The fixed setup command writes this file itself. Its presence is also the
|
||||
# machine-wide completion state, so migrations run by another account no-op.
|
||||
if [[ -e $config || -L $config ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Earlier versions enabled sshd before importing the key, but did not leave a
|
||||
# marker saying that Omarchy configured it. Limit the repair to a daemon that is
|
||||
# enabled or currently exposed and a user who already has a usable authorized
|
||||
# key. A machine that never set SSH up exits without prompting for privileges.
|
||||
if ! systemctl is-enabled --quiet sshd.service 2>/dev/null &&
|
||||
! systemctl is-active --quiet sshd.service 2>/dev/null; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# sshd reads authorized_keys one entry per line, while ssh-keygen -lf
|
||||
# fingerprints whole files in formats sshd does not accept there — a private
|
||||
# key copied in by mistake passes the file-level check even though sshd finds
|
||||
# no usable entry in it. Ask sshd's question instead: does any single line
|
||||
# parse as a public key?
|
||||
has_usable_key() {
|
||||
local line
|
||||
while IFS= read -r line || [[ -n $line ]]; do
|
||||
if [[ $line =~ ^[[:space:]]*(#|$) ]]; then
|
||||
continue
|
||||
fi
|
||||
if ssh-keygen -lf /dev/stdin <<<"$line" >/dev/null 2>&1; then
|
||||
return 0
|
||||
fi
|
||||
done <"$authorized_keys"
|
||||
return 1
|
||||
}
|
||||
|
||||
# A file that exists but cannot be read leaves the key question unanswered; do
|
||||
# not treat it as proof the machine is password-only. [[ -f ]] and the read
|
||||
# both follow symlinks on purpose: a dotfiles-managed authorized_keys link with
|
||||
# a working key must not count as keyless.
|
||||
if [[ -f $authorized_keys && ! -r $authorized_keys ]]; then
|
||||
skip "Could not read $authorized_keys to check for a usable key."
|
||||
fi
|
||||
|
||||
# The old setup command enabled sshd before importing a key, so an aborted run
|
||||
# left a password-only server exposed. Without a usable key there is nothing to
|
||||
# harden: close the hole Omarchy opened by disabling the server. Omarchy is a
|
||||
# desktop distro, so the console remains; re-enabling password SSH afterwards
|
||||
# is an intentional, informed choice the warning explains how to make.
|
||||
if [[ ! -f $authorized_keys ]] || ! has_usable_key; then
|
||||
if ! as_root systemctl disable --now sshd.service; then
|
||||
echo "Administrator privileges are required to close the password-only SSH server. Run omarchy-migrate again from a terminal." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "No usable SSH key is authorized, so sshd only accepted password logins. The SSH server has been disabled: run omarchy-setup-security-sshd to set it up with key-based authentication, or re-enable sshd to accept password logins anyway."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Under StrictModes, sshd's default, a group- or world-writable home directory,
|
||||
# ~/.ssh, or authorized_keys makes sshd ignore the key that just validated, and
|
||||
# passwords would then be the only way in. Tighten the two paths the setup
|
||||
# command owns, exactly as it does; the home directory is not ours to change.
|
||||
home_mode=$(stat -c '%a' "$HOME" 2>/dev/null) || skip "Could not inspect the permissions on $HOME."
|
||||
if (( 8#$home_mode & 8#022 )); then
|
||||
skip "$HOME is group- or world-writable, so sshd would ignore the authorized key."
|
||||
fi
|
||||
if ! chmod 700 "$HOME/.ssh" || ! chmod 600 "$authorized_keys"; then
|
||||
skip "Could not tighten the permissions on $authorized_keys."
|
||||
fi
|
||||
|
||||
echo "Disabling SSH password authentication on the existing key-based SSH setup..."
|
||||
if ! as_root install -Dm644 /dev/stdin "$config" <<'CONF'
|
||||
# Written by Omarchy once an SSH key was already authorized.
|
||||
# Delete this file and reload sshd to allow password logins again.
|
||||
PasswordAuthentication no
|
||||
KbdInteractiveAuthentication no
|
||||
CONF
|
||||
then
|
||||
echo "Administrator privileges are required to harden the existing SSH setup. Run omarchy-migrate again from a terminal." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# The drop-in itself is always valid, so a rejection means the configuration
|
||||
# was already broken before it arrived — the administrator's to repair.
|
||||
if ! as_root sshd -t; then
|
||||
as_root rm -f -- "$config" || true
|
||||
skip "sshd rejected its configuration."
|
||||
fi
|
||||
|
||||
effective_config=$(as_root sshd -T) || {
|
||||
as_root rm -f -- "$config" || true
|
||||
skip "Could not inspect sshd's effective configuration."
|
||||
}
|
||||
|
||||
# Syntax alone is insufficient because sshd uses the first value it reads. An
|
||||
# sshd_config predating the packaged sshd_config.d Include never reads the
|
||||
# drop-in at all, and an earlier administrator rule overrides it. Either way
|
||||
# the file is ineffective: remove it rather than claiming the machine is
|
||||
# protected.
|
||||
if ! grep -qixF "passwordauthentication no" <<<"$effective_config" ||
|
||||
! grep -qixF "kbdinteractiveauthentication no" <<<"$effective_config"; then
|
||||
as_root rm -f -- "$config" || true
|
||||
skip "sshd does not apply the hardening drop-in, so an earlier rule or a config without the sshd_config.d include wins."
|
||||
fi
|
||||
|
||||
# An enabled but deliberately stopped daemon picks the file up on its next
|
||||
# start. Reload only a daemon that is currently serving connections so existing
|
||||
# sessions survive while new ones get the hardened policy.
|
||||
if systemctl is-active --quiet sshd.service 2>/dev/null; then
|
||||
if ! as_root systemctl reload sshd.service; then
|
||||
echo "The hardening config is installed and valid, but sshd did not reload; it takes effect when sshd next restarts." >&2
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
@@ -948,6 +948,21 @@ Item {
|
||||
onFileChanged: barHiddenProbe.running = true
|
||||
}
|
||||
|
||||
// The directory watch can permanently stop delivering events after flag
|
||||
// changes land in quick succession, stranding the bar off screen until the
|
||||
// shell restarts. `omarchy-toggle-bar` nudges this after flipping the flag
|
||||
// so the probe re-reads it even when the watch has gone quiet.
|
||||
IpcHandler {
|
||||
target: "omarchy.bar"
|
||||
|
||||
// Start rather than restart: a probe already in flight was launched by the
|
||||
// directory watch after the flag flipped, so its answer is current, and
|
||||
// killing it here can swallow the result entirely.
|
||||
function syncHidden(): void {
|
||||
barHiddenProbe.running = true
|
||||
}
|
||||
}
|
||||
|
||||
Variants {
|
||||
model: Quickshell.screens
|
||||
|
||||
|
||||
+7
-1
@@ -19,7 +19,13 @@ mkdir -p "$OMARCHY_ACCEPTANCE_DIR"
|
||||
# the session environment is inherited.
|
||||
export XDG_RUNTIME_DIR="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}"
|
||||
export DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS:-unix:path=$XDG_RUNTIME_DIR/bus}"
|
||||
export OMARCHY_PATH="${OMARCHY_PATH:-$ROOT}"
|
||||
# The suite verifies the installed product the session is running, so default
|
||||
# OMARCHY_PATH to the installed tree — never this checkout, which may hold
|
||||
# only test/ (omarchy-iso-test's --sync-omarchy). qs matches shell instances
|
||||
# by config path, so a suite pointed at any other tree reads the session
|
||||
# shell as "not running". Callers testing a different tree pass it explicitly.
|
||||
export OMARCHY_PATH="${OMARCHY_PATH:-/usr/share/omarchy}"
|
||||
|
||||
export PATH="$OMARCHY_PATH/bin:$PATH"
|
||||
|
||||
if [[ -z ${DISPLAY:-} ]]; then
|
||||
|
||||
@@ -36,7 +36,9 @@ screen_contains() {
|
||||
local text="$1"
|
||||
local snapshot="/tmp/omarchy-acceptance-ocr-$$.png"
|
||||
|
||||
if ! timeout 10 grim "$snapshot" 2>/dev/null; then
|
||||
# Capture at 2x scale: tesseract routinely drops small caption text at
|
||||
# native resolution (the weather panel's detail labels, for one).
|
||||
if ! timeout 10 grim -s 2 "$snapshot" 2>/dev/null; then
|
||||
rm -f "$snapshot"
|
||||
return 1
|
||||
fi
|
||||
|
||||
@@ -75,7 +75,7 @@ wtype -k Return
|
||||
wait_until "style submenu is visible" 15 screen_contains "Theme"
|
||||
screenshot "success-menu-03-style-submenu"
|
||||
|
||||
wtype -k Down -k Down -k Down -k Return
|
||||
wtype -k Down -k Down -k Down -k Down -k Return
|
||||
sleep 1
|
||||
screenshot "success-menu-04-menu-bar-submenu"
|
||||
|
||||
|
||||
Executable
+115
@@ -0,0 +1,115 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# Verifies the security posture of an installed system: the unprivileged
|
||||
# session-to-root paths closed for 4.0.2 (blanket input-group grant, shipped
|
||||
# asdcontrol sudoers authorization) and the SSH hardening flow.
|
||||
#
|
||||
# The sshd section reconfigures the machine (enables sshd, opens the firewall,
|
||||
# disables password logins), so it demands explicit opt-in: it only runs when
|
||||
# OMARCHY_ACCEPTANCE_SUDO_PASSWORD is set, which omarchy-iso-test does for its
|
||||
# throwaway VMs. A cached sudo timestamp alone never triggers it, so running
|
||||
# the suite on a machine you care about cannot reconfigure sshd by accident.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
# Membership of `input` gives raw access to /dev/input/event*: any process
|
||||
# running as the user could log keystrokes. Only the opt-in controller and
|
||||
# ydotool features may grant it.
|
||||
verify_input_group() {
|
||||
if id -nG | grep -qw input; then
|
||||
if pacman -Q xpadneo-dkms &>/dev/null || pacman -Q ydotool &>/dev/null; then
|
||||
pass "input group membership is backed by an opt-in feature"
|
||||
else
|
||||
fail "user is not in the input group" "no controller or ydotool support installed to justify it"
|
||||
fi
|
||||
else
|
||||
pass "user is not in the input group"
|
||||
fi
|
||||
}
|
||||
|
||||
sudo_available() {
|
||||
if sudo -n true 2>/dev/null; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [[ -n ${OMARCHY_ACCEPTANCE_SUDO_PASSWORD:-} ]]; then
|
||||
printf '%s\n' "$OMARCHY_ACCEPTANCE_SUDO_PASSWORD" | sudo -S -v 2>/dev/null
|
||||
return $?
|
||||
fi
|
||||
|
||||
return 1
|
||||
}
|
||||
|
||||
verify_asdcontrol_sudoers() {
|
||||
# Omarchy used to ship a passwordless sudoers grant for asdcontrol; that
|
||||
# authorization now belongs to the package alone.
|
||||
if sudo -n test -e /etc/sudoers.d/omarchy-asdcontrol; then
|
||||
fail "no omarchy asdcontrol sudoers grant is shipped" "/etc/sudoers.d/omarchy-asdcontrol exists"
|
||||
fi
|
||||
pass "no omarchy asdcontrol sudoers grant is shipped"
|
||||
}
|
||||
|
||||
verify_sshd_hardening() {
|
||||
local key_file=/tmp/omarchy-acceptance-sshd-key
|
||||
local effective_config
|
||||
|
||||
rm -f "$key_file" "$key_file.pub"
|
||||
ssh-keygen -t ed25519 -N "" -q -C "omarchy-acceptance" -f "$key_file"
|
||||
|
||||
# sudo keys its cached credential on the calling terminal and, absent one, on
|
||||
# the caller's parent process alone, so a timestamp validated in this shell
|
||||
# never reaches the setup command's own sudo calls when the suite runs
|
||||
# without a terminal (omarchy-iso-test drives it over ssh with no pty). Give
|
||||
# the exercise a pseudo-terminal and validate the password on it first, so
|
||||
# every sudo underneath shares that terminal's credential.
|
||||
if ! OMARCHY_ACCEPTANCE_SUDO_PASSWORD="$OMARCHY_ACCEPTANCE_SUDO_PASSWORD" \
|
||||
OMARCHY_ACCEPTANCE_SSHD_KEY="$(cat "$key_file.pub")" SHELL=/bin/bash \
|
||||
script -qec 'printf "%s\n" "$OMARCHY_ACCEPTANCE_SUDO_PASSWORD" | sudo -S -v 2>/dev/null &&
|
||||
omarchy-setup-security-sshd --key="$OMARCHY_ACCEPTANCE_SSHD_KEY"' /dev/null \
|
||||
</dev/null >"$ARTIFACTS/setup-security-sshd.log" 2>&1; then
|
||||
fail "omarchy-setup-security-sshd completes unattended" "$(tail -5 "$ARTIFACTS/setup-security-sshd.log")"
|
||||
fi
|
||||
pass "omarchy-setup-security-sshd completes unattended"
|
||||
|
||||
systemctl is-active sshd.service >/dev/null || fail "sshd is running after setup"
|
||||
pass "sshd is running after setup"
|
||||
|
||||
grep -qxF "$(cat "$key_file.pub")" "$HOME/.ssh/authorized_keys" || fail "the key is authorized"
|
||||
pass "the key is authorized"
|
||||
|
||||
# The command verifies its own hardening before keeping it, but assert the
|
||||
# effective config independently: sshd honors the first value it reads, and
|
||||
# regressions here reopen password logins. Keywords match case-insensitively
|
||||
# because OpenSSH 9.x dumps them lowercase and 10.x in CamelCase.
|
||||
effective_config=$(sudo -n sshd -T) || fail "sshd reports its effective config"
|
||||
grep -qixF "passwordauthentication no" <<<"$effective_config" || fail "password authentication is off"
|
||||
pass "password authentication is off"
|
||||
grep -qixF "kbdinteractiveauthentication no" <<<"$effective_config" || fail "keyboard-interactive authentication is off"
|
||||
pass "keyboard-interactive authentication is off"
|
||||
|
||||
if omarchy-cmd-present ufw; then
|
||||
sudo -n ufw status | grep -qE '^22/tcp\s+LIMIT' || fail "the SSH port is rate limited in the firewall"
|
||||
pass "the SSH port is rate limited in the firewall"
|
||||
fi
|
||||
|
||||
# Leave the machine as found where cheap: the throwaway key stays useless
|
||||
# once removed, while the hardening itself is the state under test.
|
||||
sed -i "\#$(cat "$key_file.pub" | cut -d' ' -f2)#d" "$HOME/.ssh/authorized_keys"
|
||||
rm -f "$key_file" "$key_file.pub"
|
||||
}
|
||||
|
||||
verify_input_group
|
||||
|
||||
if sudo_available; then
|
||||
verify_asdcontrol_sudoers
|
||||
else
|
||||
pass "asdcontrol sudoers check skipped: sudo needs a password"
|
||||
fi
|
||||
|
||||
if [[ -n ${OMARCHY_ACCEPTANCE_SUDO_PASSWORD:-} ]] && sudo_available; then
|
||||
verify_sshd_hardening
|
||||
else
|
||||
pass "sshd hardening exercise skipped: set OMARCHY_ACCEPTANCE_SUDO_PASSWORD to run it"
|
||||
fi
|
||||
@@ -8,12 +8,17 @@ status=0
|
||||
|
||||
verify_core_packages() {
|
||||
local package
|
||||
local manifest="$OMARCHY_PATH/install/omarchy-base.packages"
|
||||
local -a missing=()
|
||||
|
||||
# Without this, a missing manifest reads as an empty package list and the
|
||||
# audit passes having checked nothing.
|
||||
[[ -f $manifest ]] || fail "all Omarchy core packages are installed" "package manifest not found: $manifest"
|
||||
|
||||
while IFS= read -r package; do
|
||||
[[ -z $package || $package == \#* ]] && continue
|
||||
pacman -Q "$package" >/dev/null 2>&1 || missing+=("$package")
|
||||
done <"$OMARCHY_PATH/install/omarchy-base.packages"
|
||||
done <"$manifest"
|
||||
|
||||
(( ${#missing[@]} == 0 )) || fail "all Omarchy core packages are installed" "missing packages: ${missing[*]}"
|
||||
pass "all Omarchy core packages are installed (${#missing[@]} missing)"
|
||||
|
||||
@@ -431,8 +431,10 @@ assert_launched() {
|
||||
fail "$agent launch $description" "expected: ${expected[*]}\nactual: ${actual[*]}"
|
||||
|
||||
for ((index = 0; index < ${#expected[@]}; index++)); do
|
||||
[[ ${actual[$index]} == ${expected[$index]} ]] ||
|
||||
fail "$agent launch $description" "expected: ${expected[*]}\nactual: ${actual[*]}"
|
||||
case ${actual[$index]} in
|
||||
"${expected[$index]}") ;;
|
||||
*) fail "$agent launch $description" "expected: ${expected[*]}\nactual: ${actual[*]}" ;;
|
||||
esac
|
||||
done
|
||||
}
|
||||
|
||||
@@ -462,10 +464,18 @@ assert_launch claude claude --permission-mode auto -- "Review this project"
|
||||
assert_launch codex codex --approve-for-me -- "Review this project"
|
||||
assert_launch crush crush run "Review this project"
|
||||
assert_launch grok grok --permission-mode bypassPermissions -- "Review this project"
|
||||
assert_launch hermes env -u HERMES_SESSION_SOURCE hermes chat --yolo --tui "--query=Review this project"
|
||||
assert_launch agy agy --dangerously-skip-permissions --prompt-interactive "Review this project"
|
||||
assert_launch copilot copilot --allow-all --interactive "Review this project"
|
||||
pass "agent launcher adapts initial prompts for every supported agent"
|
||||
|
||||
literal_hermes_prompt=$' --help !Crash /quit {$(touch must-not-run)}\ntrailing\\ '
|
||||
printf '%s\n' "hermes" >"$agent_file"
|
||||
omarchy-agent-prompt "$literal_hermes_prompt"
|
||||
assert_launched hermes "binds its literal initial prompt" env -u HERMES_SESSION_SOURCE \
|
||||
hermes chat --yolo --tui "--query=$literal_hermes_prompt"
|
||||
pass "Hermes receives prompted launches as one literal query argument"
|
||||
|
||||
assert_bypass pi pi
|
||||
assert_bypass omp omp --auto-approve
|
||||
assert_bypass opencode opencode --auto
|
||||
@@ -474,6 +484,7 @@ assert_bypass claude claude --permission-mode auto
|
||||
assert_bypass codex codex --approve-for-me
|
||||
assert_bypass crush crush --yolo
|
||||
assert_bypass grok grok --permission-mode bypassPermissions
|
||||
assert_bypass hermes hermes --yolo
|
||||
assert_bypass agy agy --dangerously-skip-permissions
|
||||
assert_bypass copilot copilot --allow-all
|
||||
pass "agent launcher skips permission prompts for every supported agent"
|
||||
|
||||
Executable
+133
@@ -0,0 +1,133 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
migration="$ROOT/migrations/1787760281.sh"
|
||||
test_tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$test_tmp"' EXIT
|
||||
|
||||
mock_bin="$test_tmp/bin"
|
||||
test_home="$test_tmp/home"
|
||||
hermes="$test_home/.local/bin/hermes"
|
||||
marker="# Written by omarchy-install-hermes-cli."
|
||||
mkdir -p "$mock_bin" "$test_home/.local/bin" "$test_home/.local/state/omarchy"
|
||||
|
||||
cat >"$mock_bin/omarchy-pkg-present" <<'SH'
|
||||
#!/bin/bash
|
||||
[[ ${OMARCHY_TEST_DESKTOP_INSTALLED:-0} == 1 ]]
|
||||
SH
|
||||
|
||||
cat >"$mock_bin/omarchy-cmd-missing" <<'SH'
|
||||
#!/bin/bash
|
||||
! command -v "$1" >/dev/null 2>&1
|
||||
SH
|
||||
|
||||
mise_log="$test_tmp/mise-log"
|
||||
cat >"$mock_bin/mise" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '%s\0' "$@" >>"$OMARCHY_TEST_MISE_LOG"
|
||||
[[ $1 != "where" ]]
|
||||
SH
|
||||
|
||||
chmod +x "$mock_bin"/*
|
||||
|
||||
# The real installer is on PATH so the migration writes today's stub, not a
|
||||
# copy of it.
|
||||
run_migration() {
|
||||
OMARCHY_TEST_DESKTOP_INSTALLED="${1:-0}" \
|
||||
OMARCHY_TEST_MISE_LOG="$mise_log" \
|
||||
HOME="$test_home" \
|
||||
PATH="$mock_bin:$ROOT/bin:$PATH" \
|
||||
bash -euo pipefail "$migration" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
run_migration || fail "the migration installs the wrapper on a plain install"
|
||||
[[ -x $hermes ]] && grep -qxF "$marker" "$hermes" || fail "the migration writes the Omarchy wrapper"
|
||||
pass "the migration installs the Hermes wrapper"
|
||||
|
||||
before=$(cat "$hermes")
|
||||
run_migration || fail "rerunning the migration succeeds"
|
||||
[[ $(cat "$hermes") == "$before" ]] || fail "rerunning the migration leaves the same wrapper"
|
||||
pass "the migration is idempotent"
|
||||
|
||||
chmod -x "$hermes"
|
||||
run_migration || fail "the migration repairs a non-executable Omarchy wrapper"
|
||||
[[ -x $hermes ]] && grep -qxF "$marker" "$hermes" ||
|
||||
fail "the migration restores a non-executable Omarchy wrapper"
|
||||
pass "the migration repairs a non-executable Omarchy wrapper"
|
||||
|
||||
rm -f "$hermes"
|
||||
touch "$test_home/.local/state/omarchy/preinstalls-removed"
|
||||
run_migration || fail "the migration succeeds for users who removed the preinstalls"
|
||||
[[ ! -e $hermes ]] || fail "the migration respects the preinstalls opt-out"
|
||||
pass "the migration skips users who removed the preinstalls"
|
||||
rm -f "$test_home/.local/state/omarchy/preinstalls-removed"
|
||||
|
||||
run_migration 1 || fail "the migration succeeds when Hermes Desktop owns Hermes"
|
||||
[[ ! -e $hermes ]] || fail "the migration writes nothing when Hermes Desktop owns Hermes"
|
||||
pass "the migration stands aside for Hermes Desktop"
|
||||
|
||||
# Standing aside is not the same as leaving a second Hermes behind: the wrapper
|
||||
# an earlier install wrote and the mise copy it points at both go when the
|
||||
# desktop app owns Hermes, even though the app has not finished setting up.
|
||||
printf '%s\n' "#!/bin/bash" "$marker" >"$hermes"
|
||||
chmod +x "$hermes"
|
||||
: >"$mise_log"
|
||||
run_migration 1 || fail "the migration succeeds when Hermes Desktop owns Hermes and the old wrapper is present"
|
||||
[[ ! -e $hermes ]] || fail "the migration removes the Omarchy wrapper when Hermes Desktop owns Hermes"
|
||||
mise_calls=$(tr '\0' ' ' <"$mise_log")
|
||||
[[ $mise_calls == *"rm -g "* ]] || fail "the migration removes the global mise Hermes for Hermes Desktop"
|
||||
[[ $mise_calls == *"uninstall --all "* ]] || fail "the migration uninstalls the mise Hermes for Hermes Desktop"
|
||||
pass "the migration clears the old Omarchy Hermes for Hermes Desktop"
|
||||
|
||||
# ...while anyone else's hermes stays exactly where it is, and is not run.
|
||||
foreign_ran="$test_tmp/foreign-ran"
|
||||
foreign_body="#!/bin/bash
|
||||
touch $foreign_ran
|
||||
exec $test_home/.hermes/hermes-agent/venv/bin/hermes \"\$@\""
|
||||
printf '%s\n' "$foreign_body" >"$hermes"
|
||||
chmod +x "$hermes"
|
||||
run_migration 1 || fail "the migration succeeds over a foreign hermes when Hermes Desktop owns Hermes"
|
||||
[[ -x $hermes && $(cat "$hermes") == "$foreign_body" ]] ||
|
||||
fail "the migration leaves a foreign hermes alone when Hermes Desktop owns Hermes"
|
||||
[[ ! -e $foreign_ran ]] || fail "the migration does not run a foreign hermes"
|
||||
pass "the migration preserves a foreign hermes for Hermes Desktop"
|
||||
rm -f "$hermes"
|
||||
|
||||
official_body="#!/bin/bash
|
||||
unset PYTHONPATH
|
||||
unset PYTHONHOME
|
||||
exec $test_home/.hermes/hermes-agent/venv/bin/hermes \"\$@\""
|
||||
printf '%s\n' "$official_body" >"$hermes"
|
||||
chmod +x "$hermes"
|
||||
run_migration || fail "the migration succeeds over a foreign hermes command"
|
||||
[[ $(cat "$hermes") == "$official_body" ]] || fail "the migration leaves a foreign hermes command alone"
|
||||
pass "the migration preserves a foreign hermes command"
|
||||
|
||||
chmod -x "$hermes"
|
||||
run_migration || fail "the migration succeeds over a non-executable foreign hermes"
|
||||
[[ -f $hermes && ! -x $hermes && $(cat "$hermes") == "$official_body" ]] ||
|
||||
fail "the migration leaves a non-executable foreign hermes alone"
|
||||
pass "the migration preserves a non-executable foreign hermes"
|
||||
|
||||
rm -f "$hermes"
|
||||
ln -s "$test_home/nowhere/hermes" "$hermes"
|
||||
run_migration || fail "the migration succeeds over a dangling hermes link"
|
||||
[[ -L $hermes && $(readlink "$hermes") == "$test_home/nowhere/hermes" ]] ||
|
||||
fail "the migration leaves a dangling hermes link alone"
|
||||
pass "the migration preserves a dangling hermes link"
|
||||
|
||||
rm -f "$hermes"
|
||||
mkdir "$hermes"
|
||||
run_migration || fail "the migration succeeds over a directory at the hermes path"
|
||||
[[ -d $hermes ]] || fail "the migration leaves a directory at the hermes path alone"
|
||||
pass "the migration preserves a directory at the hermes path"
|
||||
|
||||
rmdir "$hermes"
|
||||
printf '%s\n' "#!/bin/bash" "# Replaces the stub omarchy-install-hermes-cli used to write." >"$hermes"
|
||||
chmod +x "$hermes"
|
||||
run_migration || fail "the migration succeeds over a wrapper that mentions the installer"
|
||||
grep -qxF "$marker" "$hermes" && fail "the migration does not rewrite a wrapper that merely mentions the installer"
|
||||
pass "the migration preserves a wrapper that merely mentions the installer"
|
||||
Executable
+398
@@ -0,0 +1,398 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
test_tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$test_tmp"' EXIT
|
||||
|
||||
mock_bin="$test_tmp/bin"
|
||||
test_home="$test_tmp/home"
|
||||
mise_log="$test_tmp/mise-log"
|
||||
mkdir -p "$mock_bin" "$test_home/.local/bin"
|
||||
|
||||
cat >"$mock_bin/omarchy-pkg-present" <<'SH'
|
||||
#!/bin/bash
|
||||
[[ ${OMARCHY_TEST_DESKTOP_INSTALLED:-0} == 1 ]]
|
||||
SH
|
||||
|
||||
cat >"$mock_bin/omarchy-cmd-missing" <<'SH'
|
||||
#!/bin/bash
|
||||
! command -v "$1" >/dev/null 2>&1
|
||||
SH
|
||||
|
||||
# `mise where` must fail so the installer sees no Hermes behind the stub.
|
||||
cat >"$mock_bin/mise" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '%s\0' "$@" >>"$OMARCHY_TEST_MISE_LOG"
|
||||
if [[ $1 == "where" && ${OMARCHY_TEST_MISE_WHERE_OK:-0} == 1 ]]; then
|
||||
printf '%s\n' "$OMARCHY_TEST_MISE_ROOT"
|
||||
exit 0
|
||||
fi
|
||||
[[ $1 != "where" ]]
|
||||
SH
|
||||
|
||||
chmod +x "$mock_bin"/*
|
||||
|
||||
run_installer() {
|
||||
OMARCHY_TEST_DESKTOP_INSTALLED="$1" \
|
||||
OMARCHY_TEST_MISE_WHERE_OK="${OMARCHY_TEST_MISE_WHERE_OK:-0}" \
|
||||
OMARCHY_TEST_MISE_ROOT="$test_tmp/mise" \
|
||||
OMARCHY_TEST_MISE_LOG="$mise_log" \
|
||||
HOME="$test_home" \
|
||||
PATH="$mock_bin:$PATH" \
|
||||
bash "$ROOT/bin/omarchy-install-hermes-cli" ${2:+"$2"} >/dev/null 2>&1
|
||||
}
|
||||
|
||||
stub_marker="# Written by omarchy-install-hermes-cli."
|
||||
python_pin="3.13"
|
||||
app_stub_body='#!/bin/bash
|
||||
exec /home/x/.hermes/hermes-agent/venv/bin/hermes "$@"'
|
||||
|
||||
# Writing the stub must not provision anything: user setup calls this on every
|
||||
# machine, including the ones that never run Hermes.
|
||||
: >"$mise_log"
|
||||
rm -f "$test_home/.local/bin/hermes"
|
||||
run_installer 0 || fail "installer failed with no desktop installed"
|
||||
[[ -x $test_home/.local/bin/hermes ]] || fail "installer writes a hermes stub when the desktop is absent"
|
||||
grep -qxF "$stub_marker" "$test_home/.local/bin/hermes" || fail "the stub records which command wrote it"
|
||||
tr '\0' ' ' <"$mise_log" | grep -q "use -g --quiet uv" &&
|
||||
fail "writing the stub does not install uv"
|
||||
pass "writing the Hermes stub provisions nothing"
|
||||
|
||||
# The desktop app owns Hermes, so our own stub must go rather than sit there
|
||||
# answering `hermes` until the app's bootstrap replaces it.
|
||||
printf '%s\n' "#!/bin/bash" "$stub_marker" >"$test_home/.local/bin/hermes"
|
||||
chmod +x "$test_home/.local/bin/hermes"
|
||||
run_installer 1 || true
|
||||
[[ ! -e $test_home/.local/bin/hermes ]] ||
|
||||
fail "the desktop taking over removes the stub this command wrote"
|
||||
pass "installing the desktop app removes the CLI stub"
|
||||
|
||||
# ...but the app's own hermes is not ours to delete.
|
||||
printf '%s\n' "$app_stub_body" >"$test_home/.local/bin/hermes"
|
||||
chmod +x "$test_home/.local/bin/hermes"
|
||||
run_installer 1 || true
|
||||
[[ -x $test_home/.local/bin/hermes ]] ||
|
||||
fail "the desktop app's own hermes command survives"
|
||||
pass "the app's own hermes command is left alone"
|
||||
|
||||
# A copy mise cannot vouch for is still a second Hermes.
|
||||
printf '%s\n' "#!/bin/bash" "$stub_marker" >"$test_home/.local/bin/hermes"
|
||||
chmod +x "$test_home/.local/bin/hermes"
|
||||
: >"$mise_log"
|
||||
OMARCHY_TEST_MISE_WHERE_OK=1 run_installer 1 || true
|
||||
tr '\0' '\n' <"$mise_log" | grep -q "uninstall" ||
|
||||
fail "takeover removes a mise copy even when it is not healthy"
|
||||
pass "takeover removes an unhealthy mise copy"
|
||||
|
||||
# --check answers about Hermes being usable, not about the venv appearing. The
|
||||
# venv exists from the python-deps stage, several stages before the command.
|
||||
rm -rf "$test_home/.hermes"
|
||||
rm -f "$test_home/.local/bin/hermes"
|
||||
run_installer 1 --check && fail "--check reports Hermes missing before the app installs it"
|
||||
# The venv command answers the readiness probes, as the real one does: foreign
|
||||
# wrappers below exec it, and the installer runs both before trusting them.
|
||||
mkdir -p "$test_home/.hermes/hermes-agent/venv/bin"
|
||||
cat >"$test_home/.hermes/hermes-agent/venv/bin/hermes" <<'SH'
|
||||
#!/bin/bash
|
||||
if [[ ${1:-} == "chat" && ${2:-} == "--help" ]]; then
|
||||
[[ ${OMARCHY_TEST_HERMES_CAPABLE:-1} == 1 ]] && echo "--oneshot"
|
||||
else
|
||||
echo "hermes-agent 0.0.0-test"
|
||||
fi
|
||||
SH
|
||||
chmod +x "$test_home/.hermes/hermes-agent/venv/bin/hermes"
|
||||
run_installer 1 --check && fail "--check waits for the install to finish, not just the venv"
|
||||
touch "$test_home/.hermes/hermes-agent/.hermes-bootstrap-complete"
|
||||
printf '%s\n' "#!/bin/bash" "exec $test_home/.hermes/hermes-agent/venv/bin/hermes \"\$@\"" >"$test_home/.local/bin/hermes"
|
||||
chmod +x "$test_home/.local/bin/hermes"
|
||||
run_installer 1 --check || fail "--check reports Hermes present once the app has finished"
|
||||
pass "--check follows the app's completed install"
|
||||
|
||||
# An executable called hermes that belongs to something else is not this
|
||||
# install being ready.
|
||||
printf '%s\n' "#!/bin/bash" "exec /usr/local/bin/somebody-elses-hermes \"\$@\"" >"$test_home/.local/bin/hermes"
|
||||
chmod +x "$test_home/.local/bin/hermes"
|
||||
run_installer 1 --check && fail "--check rejects a hermes command belonging to something else"
|
||||
pass "--check rejects a foreign hermes command"
|
||||
|
||||
# A hermes the user installed themselves -- the official installer, a wrapper of
|
||||
# their own -- is not ours to replace. --check follows whether it runs, and
|
||||
# installing steps aside so the default agent uses it.
|
||||
official_body="#!/bin/bash
|
||||
unset PYTHONPATH
|
||||
unset PYTHONHOME
|
||||
exec $test_home/.hermes/hermes-agent/venv/bin/hermes \"\$@\""
|
||||
printf '%s\n' "$official_body" >"$test_home/.local/bin/hermes"
|
||||
chmod +x "$test_home/.local/bin/hermes"
|
||||
run_installer 0 --check || fail "--check accepts a working foreign hermes command"
|
||||
run_installer 0 || fail "installing over a foreign hermes command returns success"
|
||||
run_installer 0 --now || fail "--now over a foreign hermes command returns success"
|
||||
[[ $(cat "$test_home/.local/bin/hermes") == "$official_body" ]] ||
|
||||
fail "a foreign hermes command is left untouched"
|
||||
pass "a foreign hermes command is preserved and satisfies --check"
|
||||
|
||||
OMARCHY_TEST_HERMES_CAPABLE=0 run_installer 0 --check &&
|
||||
fail "--check rejects a foreign Hermes without native prompted sessions"
|
||||
OMARCHY_TEST_HERMES_CAPABLE=0 run_installer 0 &&
|
||||
fail "installing refuses a foreign Hermes without native prompted sessions"
|
||||
[[ $(cat "$test_home/.local/bin/hermes") == "$official_body" ]] ||
|
||||
fail "an older foreign Hermes command is left untouched"
|
||||
pass "a foreign Hermes must support native prompted sessions"
|
||||
|
||||
# Broken foreign paths are still foreign. They cannot be used, so --check says
|
||||
# so and the installer refuses rather than replacing them.
|
||||
printf '%s\n' "$official_body" >"$test_home/.local/bin/hermes"
|
||||
chmod -x "$test_home/.local/bin/hermes"
|
||||
run_installer 0 --check && fail "--check rejects a non-executable foreign hermes"
|
||||
run_installer 0 && fail "the installer does not succeed over a non-executable foreign hermes"
|
||||
[[ -f $test_home/.local/bin/hermes && ! -x $test_home/.local/bin/hermes ]] ||
|
||||
fail "a non-executable foreign hermes is left untouched"
|
||||
pass "a non-executable foreign hermes is preserved"
|
||||
|
||||
# The executable bit is not enough: a wrapper whose interpreter is gone passes
|
||||
# -x and still cannot run. The probe has to run it to find out, and finding
|
||||
# out never touches the file.
|
||||
broken_interp_body="#!$test_home/nowhere/python3
|
||||
print('hermes')"
|
||||
printf '%s\n' "$broken_interp_body" >"$test_home/.local/bin/hermes"
|
||||
chmod +x "$test_home/.local/bin/hermes"
|
||||
run_installer 0 --check && fail "--check rejects a foreign hermes whose interpreter is missing"
|
||||
run_installer 0 && fail "the installer does not succeed over a foreign hermes whose interpreter is missing"
|
||||
run_installer 0 --now && fail "--now does not succeed over a foreign hermes whose interpreter is missing"
|
||||
[[ -x $test_home/.local/bin/hermes && $(cat "$test_home/.local/bin/hermes") == "$broken_interp_body" ]] ||
|
||||
fail "a foreign hermes whose interpreter is missing is left untouched"
|
||||
pass "a foreign hermes with a missing interpreter is preserved and rejected"
|
||||
|
||||
# Likewise a wrapper that execs a target that is no longer there.
|
||||
broken_target_body="#!/bin/bash
|
||||
exec $test_home/nowhere/hermes \"\$@\""
|
||||
printf '%s\n' "$broken_target_body" >"$test_home/.local/bin/hermes"
|
||||
chmod +x "$test_home/.local/bin/hermes"
|
||||
run_installer 0 --check && fail "--check rejects a foreign hermes whose target is missing"
|
||||
run_installer 0 && fail "the installer does not succeed over a foreign hermes whose target is missing"
|
||||
run_installer 0 --now && fail "--now does not succeed over a foreign hermes whose target is missing"
|
||||
[[ -x $test_home/.local/bin/hermes && $(cat "$test_home/.local/bin/hermes") == "$broken_target_body" ]] ||
|
||||
fail "a foreign hermes whose target is missing is left untouched"
|
||||
pass "a foreign hermes with a missing target is preserved and rejected"
|
||||
|
||||
foreign_target="$test_home/foreign/hermes"
|
||||
mkdir -p "$(dirname "$foreign_target")"
|
||||
printf '%s\n' "$official_body" >"$foreign_target"
|
||||
chmod +x "$foreign_target"
|
||||
rm -f "$test_home/.local/bin/hermes"
|
||||
ln -s "$foreign_target" "$test_home/.local/bin/hermes"
|
||||
run_installer 0 --check || fail "--check accepts a foreign link to a working hermes command"
|
||||
run_installer 0 || fail "the installer succeeds over a foreign link to a working hermes command"
|
||||
run_installer 0 --now || fail "--now succeeds over a foreign link to a working hermes command"
|
||||
[[ -L $test_home/.local/bin/hermes && $(readlink "$test_home/.local/bin/hermes") == "$foreign_target" ]] ||
|
||||
fail "a foreign link to a working hermes command is left untouched"
|
||||
pass "a foreign link to a working hermes command is preserved"
|
||||
|
||||
rm -f "$test_home/.local/bin/hermes"
|
||||
ln -s "$test_home/nowhere/hermes" "$test_home/.local/bin/hermes"
|
||||
run_installer 0 --check && fail "--check rejects a dangling hermes link"
|
||||
run_installer 0 && fail "the installer does not succeed over a dangling hermes link"
|
||||
[[ -L $test_home/.local/bin/hermes && $(readlink "$test_home/.local/bin/hermes") == "$test_home/nowhere/hermes" ]] ||
|
||||
fail "a dangling hermes link is left untouched"
|
||||
pass "a dangling hermes link is preserved"
|
||||
|
||||
# A directory passes -x on search permission alone. It is still not a command.
|
||||
rm -f "$test_home/.local/bin/hermes"
|
||||
mkdir "$test_home/.local/bin/hermes"
|
||||
run_installer 0 --check && fail "--check rejects a directory at the hermes path"
|
||||
run_installer 0 && fail "the installer does not succeed over a directory at the hermes path"
|
||||
[[ -d $test_home/.local/bin/hermes ]] || fail "a directory at the hermes path is left untouched"
|
||||
pass "a directory at the hermes path is preserved and rejected"
|
||||
|
||||
# Mentioning the installer is not the same as being written by it.
|
||||
rmdir "$test_home/.local/bin/hermes"
|
||||
mentions_body="#!/bin/bash
|
||||
# Replaces the stub omarchy-install-hermes-cli used to write.
|
||||
exec $test_home/.hermes/hermes-agent/venv/bin/hermes \"\$@\""
|
||||
printf '%s\n' "$mentions_body" >"$test_home/.local/bin/hermes"
|
||||
chmod +x "$test_home/.local/bin/hermes"
|
||||
run_installer 0 || fail "installing over a wrapper that mentions the installer returns success"
|
||||
[[ $(cat "$test_home/.local/bin/hermes") == "$mentions_body" ]] ||
|
||||
fail "a wrapper that merely mentions the installer is left untouched"
|
||||
pass "ownership needs the exact marker line, not a mention"
|
||||
|
||||
# Our own stub is ours to rewrite, so reinstalling refreshes it to the current
|
||||
# template.
|
||||
rm -f "$test_home/.local/bin/hermes"
|
||||
printf '%s\n' "#!/bin/bash" "$stub_marker" "# stale template" >"$test_home/.local/bin/hermes"
|
||||
chmod +x "$test_home/.local/bin/hermes"
|
||||
run_installer 0 || fail "reinstalling over our own stub succeeds"
|
||||
grep -qxF "$stub_marker" "$test_home/.local/bin/hermes" || fail "the refreshed stub still carries the marker"
|
||||
grep -q "stale template" "$test_home/.local/bin/hermes" && fail "reinstalling rewrites our own stub"
|
||||
grep -q "exec env -u UV_PYTHON mise x" "$test_home/.local/bin/hermes" || fail "the refreshed stub is the current template"
|
||||
pass "reinstalling refreshes the Omarchy stub"
|
||||
|
||||
mkdir -p "$test_tmp/mise/hermes-agent/lib/python$python_pin"
|
||||
: >"$mise_log"
|
||||
OMARCHY_TEST_MISE_WHERE_OK=1 run_installer 0 || fail "reinstalling replaces an older owned Hermes environment"
|
||||
tr '\0' '\n' <"$mise_log" | grep -q '^rm$' || fail "an older owned Hermes environment is removed from mise config"
|
||||
tr '\0' '\n' <"$mise_log" | grep -q '^uninstall$' || fail "an older owned Hermes environment is uninstalled"
|
||||
pass "reinstalling replaces an older owned Hermes environment"
|
||||
|
||||
rm -f "$test_home/.local/bin/hermes"
|
||||
: >"$mise_log"
|
||||
OMARCHY_TEST_MISE_WHERE_OK=1 run_installer 0 &&
|
||||
fail "installing refuses to claim an unmarked Hermes mise environment"
|
||||
tr '\0' '\n' <"$mise_log" | grep -Eq '^(rm|uninstall)$' &&
|
||||
fail "an unmarked Hermes mise environment is never removed"
|
||||
[[ ! -e $test_home/.local/bin/hermes ]] ||
|
||||
fail "an unmarked Hermes mise environment is not given an Omarchy wrapper"
|
||||
pass "a Hermes mise environment needs wrapper ownership before replacement"
|
||||
|
||||
# install/user/mise.sh is sourced by install/user/all.sh through run_logged,
|
||||
# which runs it under `bash -eE` and hands its exit code back to
|
||||
# omarchy-provision-user's `set -euo pipefail`. Everything that finalizes a user
|
||||
# -- the default browser, the mailto handler, the first-install migration
|
||||
# markers, the finalize-user marker -- runs after that source, so this leaf
|
||||
# returning non-zero costs the user all of it. The Hermes installer is the only
|
||||
# line in it that can fail, and it does exactly that whenever hermes-desktop is
|
||||
# installed but the app has not been launched yet: the case a second user on a
|
||||
# shared machine hits on their first login.
|
||||
mise_sh_home="$test_tmp/mise-sh-home"
|
||||
mkdir -p "$mise_sh_home/.local/bin"
|
||||
|
||||
cat >"$mock_bin/omarchy-mise-install" <<'SH'
|
||||
#!/bin/bash
|
||||
exit 0
|
||||
SH
|
||||
chmod +x "$mock_bin/omarchy-mise-install"
|
||||
|
||||
# Desktop installed, nothing bootstrapped: omarchy-install-hermes-cli exits 1.
|
||||
OMARCHY_TEST_DESKTOP_INSTALLED=1 \
|
||||
OMARCHY_TEST_MISE_LOG="$mise_log" \
|
||||
HOME="$mise_sh_home" \
|
||||
PATH="$mock_bin:$ROOT/bin:$PATH" \
|
||||
bash "$ROOT/bin/omarchy-install-hermes-cli" >/dev/null 2>&1 &&
|
||||
fail "the Hermes installer exits non-zero when the desktop app has not set Hermes up"
|
||||
|
||||
# Sourced exactly as run_logged does it.
|
||||
OMARCHY_TEST_DESKTOP_INSTALLED=1 \
|
||||
OMARCHY_TEST_MISE_LOG="$mise_log" \
|
||||
HOME="$mise_sh_home" \
|
||||
PATH="$mock_bin:$ROOT/bin:$PATH" \
|
||||
bash -eE -c 'source "$1"' bash "$ROOT/install/user/mise.sh" >/dev/null 2>&1 ||
|
||||
fail "user setup survives a Hermes install that cannot finish"
|
||||
pass "user setup survives a Hermes install that cannot finish"
|
||||
|
||||
# UV_PYTHON pins the interpreter Hermes is built against. Left in the
|
||||
# environment it reaches Hermes itself and every command the agent shells out
|
||||
# to, so a `uv` run in the user's own project resolves 3.13 there as well --
|
||||
# uv only warns that this contradicts the project's requires-python, then
|
||||
# builds the venv anyway. The stub drops it before handing over.
|
||||
leak_home="$test_tmp/leak-home"
|
||||
leak_bin="$test_tmp/leak-bin"
|
||||
leak_log="$test_tmp/leak-log"
|
||||
leak_prefix="$test_tmp/leak-prefix"
|
||||
mkdir -p "$leak_home/.local/bin" "$leak_bin" "$leak_prefix/hermes-agent/lib/python$python_pin"
|
||||
|
||||
# A mise whose `where` satisfies the stub's probe, so the stub goes straight to
|
||||
# handing over, and whose `x` records the UV_PYTHON it was handed.
|
||||
cat >"$leak_bin/mise" <<SH
|
||||
#!/bin/bash
|
||||
case \$1 in
|
||||
where) echo "$leak_prefix" ;;
|
||||
x) printf '%s' "\${UV_PYTHON-}" >"$leak_log" ;;
|
||||
esac
|
||||
SH
|
||||
chmod +x "$leak_bin/mise"
|
||||
|
||||
OMARCHY_TEST_DESKTOP_INSTALLED=0 \
|
||||
OMARCHY_TEST_MISE_LOG="$mise_log" \
|
||||
HOME="$leak_home" \
|
||||
PATH="$mock_bin:$PATH" \
|
||||
bash "$ROOT/bin/omarchy-install-hermes-cli" >/dev/null 2>&1 ||
|
||||
fail "the installer writes a stub for the leak check"
|
||||
|
||||
HOME="$leak_home" PATH="$leak_bin:$mock_bin:$PATH" \
|
||||
"$leak_home/.local/bin/hermes" --version >/dev/null 2>&1
|
||||
|
||||
[[ -f $leak_log ]] || fail "the stub reaches the command it wraps"
|
||||
[[ -z $(cat "$leak_log") ]] ||
|
||||
fail "the interpreter pin does not follow Hermes into the commands it runs"
|
||||
pass "the interpreter pin does not follow Hermes into the commands it runs"
|
||||
|
||||
# --owns is the one answer to whether the wrapper on PATH is this installer's.
|
||||
# Remove Preinstalls and the migration both ask it rather than carrying their
|
||||
# own copy of the marker, so a change to what ownership means reaches them.
|
||||
owns_home="$test_tmp/owns-home"
|
||||
mkdir -p "$owns_home/.local/bin"
|
||||
|
||||
run_owns() {
|
||||
OMARCHY_TEST_DESKTOP_INSTALLED=0 \
|
||||
OMARCHY_TEST_MISE_LOG="$mise_log" \
|
||||
HOME="$owns_home" \
|
||||
PATH="$mock_bin:$PATH" \
|
||||
bash "$ROOT/bin/omarchy-install-hermes-cli" --owns
|
||||
}
|
||||
|
||||
rm -f "$owns_home/.local/bin/hermes"
|
||||
run_owns && fail "--owns says no when there is no wrapper at all"
|
||||
|
||||
printf '%s\n' "#!/bin/bash" "$stub_marker" >"$owns_home/.local/bin/hermes"
|
||||
chmod +x "$owns_home/.local/bin/hermes"
|
||||
run_owns || fail "--owns recognises the stub this installer wrote"
|
||||
|
||||
printf '%s\n' "#!/bin/bash" "# Replaces the stub omarchy-install-hermes-cli used to write." \
|
||||
>"$owns_home/.local/bin/hermes"
|
||||
run_owns && fail "--owns needs the exact marker line, not a mention"
|
||||
|
||||
# Quoting the marker inside a longer line is not the same as carrying it: the
|
||||
# match is whole-line, so a wrapper describing what it replaced stays the
|
||||
# user's.
|
||||
printf '%s\n' "#!/bin/bash" "# Replaced '$stub_marker' with my own." \
|
||||
>"$owns_home/.local/bin/hermes"
|
||||
run_owns && fail "--owns needs the marker to be the whole line, not part of one"
|
||||
|
||||
rm -f "$owns_home/.local/bin/hermes"
|
||||
ln -s "$test_home/.local/bin/hermes" "$owns_home/.local/bin/hermes"
|
||||
run_owns && fail "--owns disclaims a symlink, whatever it resolves to"
|
||||
rm -f "$owns_home/.local/bin/hermes"
|
||||
pass "--owns answers for the wrapper this installer wrote and nothing else"
|
||||
|
||||
# The marker lives in exactly one place. Every other caller asks --owns, so a
|
||||
# second copy is drift waiting to happen.
|
||||
marker_copies=$(grep -rl "Written by omarchy-install-hermes-cli" \
|
||||
"$ROOT/bin" "$ROOT/install" "$ROOT/migrations" 2>/dev/null | wc -l)
|
||||
(( marker_copies == 1 )) ||
|
||||
fail "only omarchy-install-hermes-cli spells out the ownership marker"
|
||||
pass "the ownership marker is written down once"
|
||||
|
||||
# The app's marker says its install once landed, not that it is still there. A
|
||||
# wrapper whose runtime has since gone answers for nothing, so readiness runs
|
||||
# the command, exactly as it does for a hermes the user installed themselves.
|
||||
ready_home="$test_tmp/ready-home"
|
||||
mkdir -p "$ready_home/.hermes/hermes-agent/venv/bin" "$ready_home/.local/bin"
|
||||
touch "$ready_home/.hermes/hermes-agent/.hermes-bootstrap-complete"
|
||||
printf '%s\n' "#!/bin/bash" "exec $ready_home/.hermes/hermes-agent/venv/bin/hermes \"\$@\"" \
|
||||
>"$ready_home/.local/bin/hermes"
|
||||
chmod +x "$ready_home/.local/bin/hermes"
|
||||
|
||||
run_ready_check() {
|
||||
OMARCHY_TEST_DESKTOP_INSTALLED=1 \
|
||||
OMARCHY_TEST_MISE_LOG="$mise_log" \
|
||||
HOME="$ready_home" \
|
||||
PATH="$mock_bin:$PATH" \
|
||||
bash "$ROOT/bin/omarchy-install-hermes-cli" --check >/dev/null 2>&1
|
||||
}
|
||||
|
||||
run_ready_check && fail "--check rejects the app's wrapper when its runtime is gone"
|
||||
|
||||
cat >"$ready_home/.hermes/hermes-agent/venv/bin/hermes" <<'SH'
|
||||
#!/bin/bash
|
||||
if [[ ${1:-} == "chat" && ${2:-} == "--help" ]]; then
|
||||
echo "--oneshot"
|
||||
else
|
||||
echo "hermes-agent 0.0.0-test"
|
||||
fi
|
||||
SH
|
||||
chmod +x "$ready_home/.hermes/hermes-agent/venv/bin/hermes"
|
||||
run_ready_check || fail "--check accepts the app's wrapper once it runs"
|
||||
pass "readiness runs the app's command rather than trusting its marker"
|
||||
Executable
+112
@@ -0,0 +1,112 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
test_tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$test_tmp"' EXIT
|
||||
|
||||
mock_bin="$test_tmp/bin"
|
||||
test_home="$test_tmp/home"
|
||||
mkdir -p "$mock_bin"
|
||||
|
||||
cat >"$mock_bin/omarchy-pkg-drop" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '%s\0' "$@" >>"$OMARCHY_TEST_DROP_LOG"
|
||||
SH
|
||||
chmod +x "$mock_bin"/*
|
||||
|
||||
seed_install() {
|
||||
rm -rf "$test_home"
|
||||
mkdir -p "$test_home/.hermes/hermes-agent" "$test_home/.hermes/bootstrap-cache" \
|
||||
"$test_home/.hermes/bin" "$test_home/.hermes/node/bin" \
|
||||
"$test_home/.hermes/memories" "$test_home/.hermes/sessions" \
|
||||
"$test_home/.config/Hermes" "$test_home/.local/bin"
|
||||
printf 'chat\n' >"$test_home/.hermes/sessions/one.json"
|
||||
printf 'memory\n' >"$test_home/.hermes/memories/one.md"
|
||||
printf 'soul\n' >"$test_home/.hermes/SOUL.md"
|
||||
printf 'uv\n' >"$test_home/.hermes/bin/uv"
|
||||
ln -sf "$test_home/.hermes/node/bin/node" "$test_home/.local/bin/node"
|
||||
ln -sf "$test_home/.hermes/node/bin/npm" "$test_home/.local/bin/npm"
|
||||
ln -sf /usr/bin/npx "$test_home/.local/bin/npx"
|
||||
printf 'node\n' >"$test_home/.hermes/node/bin/node"
|
||||
touch "$test_home/.hermes/hermes-agent/.hermes-bootstrap-complete"
|
||||
}
|
||||
|
||||
remove() {
|
||||
OMARCHY_TEST_DROP_LOG="$test_tmp/drop-log" HOME="$test_home" PATH="$mock_bin:$PATH" \
|
||||
bash "$ROOT/bin/omarchy-remove-ai-hermes" >/dev/null 2>&1
|
||||
}
|
||||
|
||||
# The app brings its own uv and its own node; both are runtime, not data.
|
||||
seed_install
|
||||
printf '%s\n' "#!/bin/bash" "exec $test_home/.hermes/hermes-agent/venv/bin/hermes \"\$@\"" \
|
||||
>"$test_home/.local/bin/hermes"
|
||||
remove || fail "remove succeeds"
|
||||
[[ ! -d $test_home/.hermes/hermes-agent ]] || fail "the runtime checkout is removed"
|
||||
[[ ! -d $test_home/.hermes/bin ]] || fail "the uv the app installed is removed"
|
||||
[[ ! -d $test_home/.hermes/node ]] || fail "the node the app installed is removed"
|
||||
pass "removal takes the whole runtime the app installed"
|
||||
|
||||
[[ -d $test_home/.config/Hermes ]] ||
|
||||
fail "gateway connections, tokens and settings survive removal"
|
||||
pass "removal keeps the app's connections and settings"
|
||||
|
||||
# -L, not -e: a dangling symlink fails -e while very much still being there.
|
||||
[[ ! -L $test_home/.local/bin/node ]] || fail "a node symlink into ~/.hermes is removed"
|
||||
[[ ! -L $test_home/.local/bin/npm ]] || fail "an npm symlink into ~/.hermes is removed"
|
||||
[[ -L $test_home/.local/bin/npx ]] || fail "an npx symlink pointing elsewhere survives"
|
||||
pass "removal clears only the managed Node links it stranded"
|
||||
|
||||
[[ -f $test_home/.hermes/sessions/one.json ]] || fail "chats survive removal"
|
||||
[[ -f $test_home/.hermes/memories/one.md ]] || fail "memories survive removal"
|
||||
[[ -f $test_home/.hermes/SOUL.md ]] || fail "SOUL.md survives removal"
|
||||
pass "removal keeps what belongs to the user"
|
||||
|
||||
[[ ! -e $test_home/.local/bin/hermes ]] || fail "the app's own hermes command is removed"
|
||||
pass "removal takes the command the app installed"
|
||||
|
||||
# A hermes command the app did not write survives even when the app did install
|
||||
# a runtime of its own.
|
||||
seed_install
|
||||
printf '%s\n' "#!/bin/bash" "exec /usr/local/bin/my-own-hermes \"\$@\"" \
|
||||
>"$test_home/.local/bin/hermes"
|
||||
remove || fail "remove succeeds with a foreign hermes present"
|
||||
[[ -f $test_home/.local/bin/hermes ]] ||
|
||||
fail "a hermes command the app did not write survives removal"
|
||||
pass "removal leaves a hermes it does not own"
|
||||
|
||||
# Installed but never launched. The app provisions its runtime on first launch
|
||||
# and marks it complete when it lands, so without that marker everything under
|
||||
# ~/.hermes predates the app -- an official install, or one built by hand -- and
|
||||
# the paths are identical either way. Dropping the package is the whole job.
|
||||
seed_install
|
||||
rm -f "$test_home/.hermes/hermes-agent/.hermes-bootstrap-complete"
|
||||
printf 'my local edit\n' >"$test_home/.hermes/hermes-agent/PATCH"
|
||||
printf '%s\n' "#!/bin/bash" "exec $test_home/.hermes/hermes-agent/venv/bin/hermes \"\$@\"" \
|
||||
>"$test_home/.local/bin/hermes"
|
||||
remove || fail "remove succeeds when the app never finished installing Hermes"
|
||||
[[ -d $test_home/.hermes/hermes-agent ]] ||
|
||||
fail "a Hermes runtime the app never installed survives removal"
|
||||
[[ -f $test_home/.hermes/hermes-agent/PATCH ]] ||
|
||||
fail "local changes to a runtime the app never installed survive removal"
|
||||
[[ -d $test_home/.hermes/bin && -d $test_home/.hermes/node ]] ||
|
||||
fail "the rest of a runtime the app never installed survives removal"
|
||||
[[ -f $test_home/.local/bin/hermes ]] ||
|
||||
fail "the command a runtime the app never installed put on PATH survives removal"
|
||||
[[ -L $test_home/.local/bin/node ]] ||
|
||||
fail "node links belonging to a runtime the app never installed survive removal"
|
||||
pass "removal leaves a Hermes the app never installed"
|
||||
|
||||
# ~/.hermes carries a dot, so a pattern rather than a plain string would also
|
||||
# claim a wrapper pointing at a sibling directory that merely looks like it.
|
||||
seed_install
|
||||
mkdir -p "$test_home/xhermes/bin"
|
||||
sibling_body="#!/bin/bash
|
||||
exec $test_home/xhermes/bin/hermes \"\$@\""
|
||||
printf '%s\n' "$sibling_body" >"$test_home/.local/bin/hermes"
|
||||
remove || fail "remove succeeds with a wrapper pointing at a sibling directory"
|
||||
[[ -f $test_home/.local/bin/hermes && $(cat "$test_home/.local/bin/hermes") == "$sibling_body" ]] ||
|
||||
fail "a wrapper pointing at ~/xhermes is not mistaken for one pointing into ~/.hermes"
|
||||
pass "removal matches the runtime path as a plain string"
|
||||
Executable
+75
@@ -0,0 +1,75 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(dirname "$0")/base-test.sh"
|
||||
|
||||
migration="$ROOT/migrations/1787843905.sh"
|
||||
[[ -f $migration ]] || fail "Hermes skills migration exists"
|
||||
|
||||
test_dir=$(mktemp -d)
|
||||
trap 'rm -rf "$test_dir"' EXIT
|
||||
home="$test_dir/home"
|
||||
|
||||
run_migration() {
|
||||
HOME="$home" OMARCHY_PATH="$ROOT" bash -euo pipefail "$migration" >/dev/null ||
|
||||
fail "migration exits clean"
|
||||
}
|
||||
|
||||
assert_link() {
|
||||
local link="$1"
|
||||
local skill="$2"
|
||||
local description="$3"
|
||||
|
||||
[[ -L $link && $(readlink "$link") == "$ROOT/default/agents/skills/$skill" ]] ||
|
||||
fail "$description" "$link -> $(readlink "$link" 2>/dev/null || echo missing)"
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------ default home, no profiles
|
||||
|
||||
rm -rf "$home"
|
||||
mkdir -p "$home"
|
||||
run_migration
|
||||
|
||||
for skill in omarchy diagnose-crash; do
|
||||
assert_link "$home/.hermes/skills/$skill" "$skill" "migration links $skill into the default Hermes home"
|
||||
done
|
||||
[[ -e $home/.hermes/profiles ]] && fail "migration does not create Hermes profiles"
|
||||
pass "migration links the default Hermes home and does not create profiles"
|
||||
|
||||
run_migration
|
||||
for skill in omarchy diagnose-crash; do
|
||||
assert_link "$home/.hermes/skills/$skill" "$skill" "migration is idempotent on the default home for $skill"
|
||||
done
|
||||
pass "migration is idempotent on the default home"
|
||||
|
||||
# ------------------------------------------------------------------ pre-existing profile
|
||||
|
||||
rm -rf "$home"
|
||||
mkdir -p "$home/.hermes/profiles/james"
|
||||
run_migration
|
||||
|
||||
for skill in omarchy diagnose-crash; do
|
||||
assert_link "$home/.hermes/skills/$skill" "$skill" "migration links $skill into the default Hermes home when a profile exists"
|
||||
assert_link "$home/.hermes/profiles/james/skills/$skill" "$skill" "migration links $skill into a pre-existing Hermes profile"
|
||||
done
|
||||
[[ -d $home/.hermes/profiles/james ]] || fail "migration leaves the pre-existing profile in place"
|
||||
profile_count=$(find "$home/.hermes/profiles" -mindepth 1 -maxdepth 1 -type d | wc -l)
|
||||
(( profile_count == 1 )) || fail "migration does not create extra profiles" "count=$profile_count"
|
||||
pass "migration links a pre-existing Hermes profile and does not create extras"
|
||||
|
||||
run_migration
|
||||
for skill in omarchy diagnose-crash; do
|
||||
assert_link "$home/.hermes/skills/$skill" "$skill" "migration is idempotent on the default home when a profile exists for $skill"
|
||||
assert_link "$home/.hermes/profiles/james/skills/$skill" "$skill" "migration is idempotent on a pre-existing profile for $skill"
|
||||
done
|
||||
pass "migration is idempotent on a pre-existing profile"
|
||||
|
||||
# ------------------------------------------------------------------ missing skill source
|
||||
|
||||
rm -rf "$home"
|
||||
mkdir -p "$home" "$test_dir/empty-omarchy"
|
||||
HOME="$home" OMARCHY_PATH="$test_dir/empty-omarchy" bash -euo pipefail "$migration" >/dev/null ||
|
||||
fail "migration exits clean when the skill source is missing"
|
||||
[[ -e $home/.hermes ]] && fail "migration no-ops when the skill source is missing"
|
||||
pass "migration no-ops when the skill source is missing"
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(dirname "$0")/base-test.sh"
|
||||
|
||||
migration="$ROOT/migrations/1787865477.sh"
|
||||
test_dir=$(mktemp -d)
|
||||
trap 'rm -rf "$test_dir"' EXIT
|
||||
|
||||
stub_bin="$test_dir/bin"
|
||||
mkdir -p "$stub_bin"
|
||||
|
||||
cat >"$stub_bin/id" <<'STUB'
|
||||
#!/bin/bash
|
||||
printf '%s\n' "${STUB_GROUPS:-wheel}"
|
||||
STUB
|
||||
cat >"$stub_bin/pacman" <<'STUB'
|
||||
#!/bin/bash
|
||||
[[ $1 == "-Qq" ]] || exit 2
|
||||
[[ " ${STUB_PACKAGES:-} " == *" $2 "* ]]
|
||||
STUB
|
||||
cat >"$stub_bin/sudo" <<'STUB'
|
||||
#!/bin/bash
|
||||
exec "$@"
|
||||
STUB
|
||||
cat >"$stub_bin/gpasswd" <<'STUB'
|
||||
#!/bin/bash
|
||||
printf '%s\n' "$*" >>"${GPASSWD_CALLS:?}"
|
||||
STUB
|
||||
cat >"$stub_bin/omarchy-state" <<'STUB'
|
||||
#!/bin/bash
|
||||
printf '%s\n' "$*" >>"${STATE_CALLS:?}"
|
||||
STUB
|
||||
chmod +x "$stub_bin"/*
|
||||
|
||||
gpasswd_calls="$test_dir/gpasswd-calls"
|
||||
state_calls="$test_dir/state-calls"
|
||||
|
||||
run_migration() {
|
||||
rm -f "$gpasswd_calls" "$state_calls"
|
||||
USER=tester STUB_GROUPS="$1" STUB_PACKAGES="${2:-}" \
|
||||
GPASSWD_CALLS="$gpasswd_calls" STATE_CALLS="$state_calls" \
|
||||
PATH="$stub_bin:$PATH" bash -euo pipefail "$migration"
|
||||
}
|
||||
|
||||
run_migration "wheel input" >/dev/null
|
||||
grep -qxF -- "-d tester input" "$gpasswd_calls" || fail "migration removes default input membership"
|
||||
grep -qxF "set reboot-required" "$state_calls" || fail "migration flags the session change for reboot"
|
||||
pass "migration removes the blanket input grant"
|
||||
|
||||
run_migration "wheel" >/dev/null
|
||||
[[ ! -e $gpasswd_calls ]] || fail "migration does not remove an already-absent group"
|
||||
[[ ! -e $state_calls ]] || fail "migration does not flag a reboot when nothing changed"
|
||||
pass "migration is idempotent after input membership is gone"
|
||||
|
||||
run_migration "wheel input" xpadneo-dkms >/dev/null
|
||||
[[ ! -e $gpasswd_calls ]] || fail "migration preserves input for controller support"
|
||||
[[ ! -e $state_calls ]] || fail "preserved controller support does not flag a reboot"
|
||||
|
||||
run_migration "wheel input" ydotool >/dev/null
|
||||
[[ ! -e $gpasswd_calls ]] || fail "migration preserves input for ydotool"
|
||||
[[ ! -e $state_calls ]] || fail "preserved ydotool support does not flag a reboot"
|
||||
pass "migration preserves deliberate input-group opt-ins"
|
||||
@@ -224,6 +224,7 @@ const expectedAgents = {
|
||||
claude: { icon: '', label: 'Claude' },
|
||||
codex: { icon: '\ue905', iconFont: 'omarchy', label: 'Codex' },
|
||||
grok: { icon: '\ue904', iconFont: 'omarchy', label: 'Grok' },
|
||||
hermes: { icon: '\ue90a', iconFont: 'omarchy', label: 'Hermes' },
|
||||
copilot: { icon: '', label: 'Copilot' },
|
||||
crush: { icon: '', label: 'Crush' },
|
||||
}
|
||||
@@ -244,7 +245,7 @@ assertDeepEqual(
|
||||
defaultItems
|
||||
.filter(item => item.parent === 'setup.default.agent')
|
||||
.map(item => item.label),
|
||||
['Antigravity', 'Claude', 'Codex', 'Copilot', 'Crush', 'Grok', 'omp', 'OpenCode', 'Ori', 'Pi'],
|
||||
['Antigravity', 'Claude', 'Codex', 'Copilot', 'Crush', 'Grok', 'Hermes', 'omp', 'OpenCode', 'Ori', 'Pi'],
|
||||
'menu sorts coding agents alphabetically'
|
||||
)
|
||||
const expectedDefaults = {
|
||||
@@ -636,5 +637,5 @@ assert(
|
||||
JS
|
||||
|
||||
font_charset=$(fc-query --format='%{charset}' "$ROOT/default/fonts/omarchy/omarchy.ttf")
|
||||
[[ $font_charset == *"e900-e909"* ]] || fail "Omarchy icon font includes every custom menu glyph"
|
||||
[[ $font_charset == *"e900-e90a"* ]] || fail "Omarchy icon font includes every custom menu glyph"
|
||||
pass "Omarchy icon font includes the official agent marks"
|
||||
|
||||
@@ -36,7 +36,10 @@ SH
|
||||
|
||||
chmod +x "$mock_bin"/*
|
||||
|
||||
export PATH="$mock_bin:$PATH"
|
||||
# $ROOT/bin after the mocks: Remove Preinstalls asks omarchy-install-hermes-cli
|
||||
# whether the wrapper is Omarchy's rather than matching the marker itself, and
|
||||
# that is the real command at runtime. The mocks still shadow what they name.
|
||||
export PATH="$mock_bin:$ROOT/bin:$PATH"
|
||||
export HOME="$test_home"
|
||||
export OMARCHY_TEST_PKG_LOG="$pkg_log"
|
||||
|
||||
@@ -89,3 +92,43 @@ pass "declining Remove Preinstalls changes nothing"
|
||||
"$ROOT/bin/omarchy-remove-preinstalls" >/dev/null
|
||||
[[ -f $marker ]] || fail "Remove Preinstalls records the opt-out"
|
||||
pass "Remove Preinstalls records the opt-out"
|
||||
|
||||
# Hermes' wrapper is only a preinstall when omarchy-install-hermes-cli wrote it.
|
||||
# The desktop app's command and an official install live at the same path and
|
||||
# are the user's, whether or not any package says so.
|
||||
hermes="$test_home/.local/bin/hermes"
|
||||
mkdir -p "$(dirname "$hermes")"
|
||||
|
||||
printf '%s\n' "#!/bin/bash" "# Written by omarchy-install-hermes-cli." >"$hermes"
|
||||
chmod +x "$hermes"
|
||||
"$ROOT/bin/omarchy-remove-preinstalls" >/dev/null
|
||||
[[ ! -e $hermes ]] || fail "Remove Preinstalls deletes the Omarchy Hermes wrapper"
|
||||
pass "Remove Preinstalls deletes the Omarchy Hermes wrapper"
|
||||
|
||||
printf '%s\n' "#!/bin/bash" "exec $test_home/.hermes/hermes-agent/venv/bin/hermes \"\$@\"" >"$hermes"
|
||||
chmod +x "$hermes"
|
||||
"$ROOT/bin/omarchy-remove-preinstalls" >/dev/null
|
||||
[[ -x $hermes ]] || fail "Remove Preinstalls keeps the desktop app's Hermes command"
|
||||
pass "Remove Preinstalls keeps the desktop app's Hermes command"
|
||||
|
||||
official_body="#!/bin/bash
|
||||
unset PYTHONPATH
|
||||
unset PYTHONHOME
|
||||
exec $test_home/.hermes/hermes-agent/venv/bin/hermes \"\$@\""
|
||||
printf '%s\n' "$official_body" >"$hermes"
|
||||
chmod +x "$hermes"
|
||||
"$ROOT/bin/omarchy-remove-preinstalls" >/dev/null
|
||||
[[ -x $hermes && $(cat "$hermes") == "$official_body" ]] || fail "Remove Preinstalls keeps an official Hermes install"
|
||||
pass "Remove Preinstalls keeps an official Hermes install"
|
||||
|
||||
printf '%s\n' "#!/bin/bash" "# Replaces the stub omarchy-install-hermes-cli used to write." >"$hermes"
|
||||
chmod +x "$hermes"
|
||||
"$ROOT/bin/omarchy-remove-preinstalls" >/dev/null
|
||||
[[ -x $hermes ]] || fail "Remove Preinstalls keeps a wrapper that merely mentions the installer"
|
||||
pass "Remove Preinstalls keeps a wrapper that merely mentions the installer"
|
||||
|
||||
rm -f "$hermes"
|
||||
ln -s "$test_home/nowhere/hermes" "$hermes"
|
||||
"$ROOT/bin/omarchy-remove-preinstalls" >/dev/null
|
||||
[[ -L $hermes ]] || fail "Remove Preinstalls keeps a foreign hermes link"
|
||||
pass "Remove Preinstalls keeps a foreign hermes link"
|
||||
|
||||
@@ -8,7 +8,7 @@ test_tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$test_tmp"' EXIT
|
||||
|
||||
mock_bin="$test_tmp/bin"
|
||||
mkdir -p "$mock_bin" "$test_tmp/home"
|
||||
mkdir -p "$mock_bin" "$test_tmp/home" "$test_tmp/home/.hermes/profiles/james"
|
||||
|
||||
for command in xdg-user-dirs-update xdg-settings xdg-mime; do
|
||||
printf '#!/bin/bash\nexit 0\n' >"$mock_bin/$command"
|
||||
@@ -30,6 +30,14 @@ for skill in omarchy diagnose-crash; do
|
||||
link="$test_tmp/home/.gemini/config/skills/$skill"
|
||||
[[ -L $link && $(readlink "$link") == "$ROOT/default/agents/skills/$skill" ]] ||
|
||||
fail "omarchy-provision-user provisions the $skill skill for Antigravity"
|
||||
|
||||
link="$test_tmp/home/.hermes/skills/$skill"
|
||||
[[ -L $link && $(readlink "$link") == "$ROOT/default/agents/skills/$skill" ]] ||
|
||||
fail "omarchy-provision-user provisions the $skill skill for Hermes"
|
||||
|
||||
link="$test_tmp/home/.hermes/profiles/james/skills/$skill"
|
||||
[[ -L $link && $(readlink "$link") == "$ROOT/default/agents/skills/$skill" ]] ||
|
||||
fail "omarchy-provision-user provisions the $skill skill for a Hermes profile"
|
||||
done
|
||||
|
||||
pass "omarchy-provision-user provisions Antigravity skills"
|
||||
pass "omarchy-provision-user provisions Antigravity and Hermes skills"
|
||||
|
||||
@@ -1,11 +1,8 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# The install scripts that grant group memberships must record them in the provisioning
|
||||
# groups file (for first-boot user creation and factory reset) and only call
|
||||
# usermod when the install user actually exists.
|
||||
#
|
||||
# Docker is deliberately excluded: the docker group is root-equivalent, so it is
|
||||
# no longer granted at install time (opt in with omarchy-setup-security-sudoless-docker).
|
||||
# Privileged groups are never granted by the default install. Docker remains an
|
||||
# explicit opt-in, and raw input-device access is granted only by the optional
|
||||
# controller and ydotool installers.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -16,13 +13,7 @@ trap 'rm -rf "$TMPDIR"' EXIT
|
||||
|
||||
export OMARCHY_PROVISIONING_DIR="$TMPDIR/provisioning"
|
||||
|
||||
# Stub getent/usermod: the fake system knows only the user "existing".
|
||||
mkdir -p "$TMPDIR/bin"
|
||||
cat >"$TMPDIR/bin/getent" <<'STUB'
|
||||
#!/bin/bash
|
||||
[[ $1 == passwd && $2 == existing ]] && { echo "existing:x:1000:1000::/home/existing:/bin/bash"; exit 0; }
|
||||
exit 2
|
||||
STUB
|
||||
cat >"$TMPDIR/bin/usermod" <<STUB
|
||||
#!/bin/bash
|
||||
echo "\$@" >>"$TMPDIR/usermod.calls"
|
||||
@@ -44,48 +35,31 @@ cat >"$TMPDIR/bin/sudo" <<STUB
|
||||
echo "\$@" >>"$TMPDIR/sudo.calls"
|
||||
exec "\$@"
|
||||
STUB
|
||||
chmod +x "$TMPDIR/bin"/{getent,usermod,groupadd,install,find,sudo}
|
||||
chmod +x "$TMPDIR/bin"/{usermod,groupadd,install,find,sudo}
|
||||
export PATH="$TMPDIR/bin:$PATH"
|
||||
export OMARCHY_PATH="$ROOT"
|
||||
|
||||
# No install user (deferred-provisioning install): groups recorded, usermod not called.
|
||||
# A deferred-provisioning install records neither privileged group.
|
||||
OMARCHY_INSTALL_USER="" bash -eE "$ROOT/install/config/docker.sh"
|
||||
OMARCHY_INSTALL_USER="" bash -eE "$ROOT/install/hardware/input-group.sh"
|
||||
OMARCHY_INSTALL_USER="" bash -eE "$ROOT/install/config/browser-policy.sh"
|
||||
|
||||
[[ -f $OMARCHY_PROVISIONING_DIR/groups ]] || fail "groups file written without an install user"
|
||||
grep -qxF input "$OMARCHY_PROVISIONING_DIR/groups" || fail "input group recorded"
|
||||
! grep -qxF omarchy-browser-policy "$OMARCHY_PROVISIONING_DIR/groups" ||
|
||||
fail "browser-policy group must not be recorded"
|
||||
[[ ! -f $OMARCHY_PROVISIONING_DIR/groups ]] ||
|
||||
! grep -Eq '^(docker|input)$' "$OMARCHY_PROVISIONING_DIR/groups" ||
|
||||
fail "default install must not record docker or input groups"
|
||||
[[ ! -f $TMPDIR/usermod.calls ]] || fail "usermod not called without an install user"
|
||||
[[ ! -f $TMPDIR/groupadd.calls ]] || ! grep -F omarchy-browser-policy "$TMPDIR/groupadd.calls" >/dev/null ||
|
||||
fail "browser-policy group is not created"
|
||||
grep -F -- '-d -m 0755 -o root -g root /etc/chromium/policies/managed' "$TMPDIR/install.calls" >/dev/null ||
|
||||
fail "browser-policy directory is created root-owned"
|
||||
pass "deferred provisioning records groups without calling usermod"
|
||||
pass "deferred provisioning records no privileged groups"
|
||||
|
||||
# The docker group is root-equivalent and must never be granted automatically.
|
||||
! grep -qxF docker "$OMARCHY_PROVISIONING_DIR/groups" || fail "docker group must not be recorded"
|
||||
pass "docker group is not recorded at install"
|
||||
|
||||
# Missing user (defensive): no usermod either.
|
||||
OMARCHY_INSTALL_USER=ghost bash -eE "$ROOT/install/hardware/input-group.sh"
|
||||
OMARCHY_INSTALL_USER=ghost bash -eE "$ROOT/install/config/browser-policy.sh"
|
||||
[[ ! -f $TMPDIR/usermod.calls ]] || fail "usermod not called for a missing user"
|
||||
pass "missing install user defers group grants"
|
||||
|
||||
# Re-running never duplicates entries.
|
||||
OMARCHY_INSTALL_USER="" bash -eE "$ROOT/install/hardware/input-group.sh"
|
||||
[[ $(grep -cxF input "$OMARCHY_PROVISIONING_DIR/groups") == 1 ]] || fail "input group recorded once"
|
||||
OMARCHY_INSTALL_USER="" bash -eE "$ROOT/install/config/browser-policy.sh"
|
||||
pass "group recording is idempotent"
|
||||
|
||||
# Existing user: usermod applies the recorded groups, and docker is never among them.
|
||||
# The same remains true when an install user already exists.
|
||||
OMARCHY_INSTALL_USER=existing bash -eE "$ROOT/install/config/docker.sh"
|
||||
OMARCHY_INSTALL_USER=existing bash -eE "$ROOT/install/hardware/input-group.sh"
|
||||
OMARCHY_INSTALL_USER=existing bash -eE "$ROOT/install/config/browser-policy.sh"
|
||||
grep -qx -- "-aG input existing" "$TMPDIR/usermod.calls" || fail "usermod grants input to the install user"
|
||||
! grep -q -- "omarchy-browser-policy" "$TMPDIR/usermod.calls" ||
|
||||
fail "usermod must not grant browser-policy to the install user"
|
||||
! grep -q -- "docker" "$TMPDIR/usermod.calls" || fail "usermod must not grant docker to the install user"
|
||||
pass "existing install user gets input but never docker or browser-policy"
|
||||
[[ ! -f $TMPDIR/usermod.calls ]] || fail "default install must not grant privileged groups"
|
||||
pass "existing install user gets neither docker nor input access"
|
||||
|
||||
! grep -q 'hardware/input-group.sh' "$ROOT/install/hardware/all.sh" ||
|
||||
fail "hardware setup must not call the removed input-group grant"
|
||||
[[ ! -e $ROOT/install/hardware/input-group.sh ]] || fail "blanket input-group grant is removed"
|
||||
pass "hardware setup has no blanket input-group grant"
|
||||
|
||||
Executable
+117
@@ -0,0 +1,117 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(dirname "$0")/base-test.sh"
|
||||
|
||||
test_dir=$(mktemp -d)
|
||||
trap 'rm -rf "$test_dir"' EXIT
|
||||
|
||||
stub_bin="$test_dir/bin"
|
||||
mkdir -p "$stub_bin"
|
||||
|
||||
cat >"$stub_bin/omarchy-pkg-add" <<'STUB'
|
||||
#!/bin/bash
|
||||
printf 'pkg %s\n' "$*" >>"${CALL_LOG:?}"
|
||||
STUB
|
||||
cat >"$stub_bin/omarchy-cmd-missing" <<'STUB'
|
||||
#!/bin/bash
|
||||
exit 0
|
||||
STUB
|
||||
cat >"$stub_bin/systemctl" <<'STUB'
|
||||
#!/bin/bash
|
||||
printf 'systemctl %s\n' "$*" >>"${CALL_LOG:?}"
|
||||
STUB
|
||||
cat >"$stub_bin/sshd" <<'STUB'
|
||||
#!/bin/bash
|
||||
case $1 in
|
||||
-t)
|
||||
[[ ${SSHD_SYNTAX_VALID:-1} == 1 ]]
|
||||
;;
|
||||
-T)
|
||||
# OpenSSH 10.x dumps keywords in CamelCase; 9.x dumped them lowercase.
|
||||
if [[ ${SSHD_DUMP_LOWERCASE:-0} == 1 ]]; then
|
||||
printf 'passwordauthentication %s\n' "${SSHD_PASSWORD_AUTH:-no}"
|
||||
printf 'kbdinteractiveauthentication %s\n' "${SSHD_KBD_AUTH:-no}"
|
||||
else
|
||||
printf 'PasswordAuthentication %s\n' "${SSHD_PASSWORD_AUTH:-no}"
|
||||
printf 'KbdInteractiveAuthentication %s\n' "${SSHD_KBD_AUTH:-no}"
|
||||
fi
|
||||
;;
|
||||
*)
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
STUB
|
||||
cat >"$stub_bin/sudo" <<'STUB'
|
||||
#!/bin/bash
|
||||
case $1 in
|
||||
install)
|
||||
destination="${TEST_ROOT:?}${4:?}"
|
||||
/usr/bin/mkdir -p "${destination%/*}"
|
||||
/usr/bin/install -Dm644 /dev/stdin "$destination"
|
||||
;;
|
||||
rm)
|
||||
/usr/bin/rm -f "${TEST_ROOT:?}${3:?}"
|
||||
;;
|
||||
*)
|
||||
exec "$@"
|
||||
;;
|
||||
esac
|
||||
STUB
|
||||
chmod +x "$stub_bin"/*
|
||||
|
||||
ssh-keygen -q -t ed25519 -N "" -f "$test_dir/key"
|
||||
public_key=$(<"$test_dir/key.pub")
|
||||
|
||||
run_setup() {
|
||||
local scenario="$1"
|
||||
local home="$test_dir/$scenario/home"
|
||||
local root="$test_dir/$scenario/root"
|
||||
|
||||
mkdir -p "$home" "$root"
|
||||
: >"$test_dir/$scenario.calls"
|
||||
|
||||
HOME="$home" TEST_ROOT="$root" CALL_LOG="$test_dir/$scenario.calls" \
|
||||
SSHD_SYNTAX_VALID="${SSHD_SYNTAX_VALID:-1}" \
|
||||
SSHD_PASSWORD_AUTH="${SSHD_PASSWORD_AUTH:-no}" \
|
||||
SSHD_KBD_AUTH="${SSHD_KBD_AUTH:-no}" \
|
||||
PATH="$stub_bin:$PATH" \
|
||||
bash "$ROOT/bin/omarchy-setup-security-sshd" --key="$public_key"
|
||||
}
|
||||
|
||||
output=$(run_setup success)
|
||||
config="$test_dir/success/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf"
|
||||
grep -qxF "PasswordAuthentication no" "$config" || fail "SSH setup disables password authentication"
|
||||
grep -qxF "KbdInteractiveAuthentication no" "$config" || fail "SSH setup disables keyboard-interactive authentication"
|
||||
grep -qxF "systemctl reload sshd.service" "$test_dir/success.calls" || fail "SSH setup reloads the validated config"
|
||||
grep -q "Password logins are off" <<<"$output" || fail "SSH setup reports hardening after it succeeds"
|
||||
pass "SSH setup authorizes a key and disables password logins"
|
||||
|
||||
output=$(SSHD_DUMP_LOWERCASE=1 run_setup success-legacy)
|
||||
config="$test_dir/success-legacy/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf"
|
||||
[[ -e $config ]] || fail "SSH setup accepts the lowercase sshd -T dump of OpenSSH 9.x"
|
||||
grep -q "Password logins are off" <<<"$output" || fail "SSH setup reports hardening on OpenSSH 9.x"
|
||||
pass "SSH setup verifies settings across sshd -T keyword casings"
|
||||
|
||||
if SSHD_PASSWORD_AUTH=yes run_setup ineffective >"$test_dir/ineffective.output" 2>&1; then
|
||||
fail "SSH setup must fail when password authentication remains effective"
|
||||
fi
|
||||
[[ ! -e $test_dir/ineffective/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] ||
|
||||
fail "SSH setup removes an ineffective hardening config"
|
||||
! grep -qF "systemctl reload sshd.service" "$test_dir/ineffective.calls" ||
|
||||
fail "SSH setup must not reload ineffective hardening"
|
||||
! grep -q "Password logins are off" "$test_dir/ineffective.output" ||
|
||||
fail "SSH setup must not claim ineffective hardening succeeded"
|
||||
pass "SSH setup verifies the effective daemon settings"
|
||||
|
||||
if SSHD_SYNTAX_VALID=0 run_setup invalid >"$test_dir/invalid.output" 2>&1; then
|
||||
fail "SSH setup must fail when sshd rejects its config"
|
||||
fi
|
||||
[[ ! -e $test_dir/invalid/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] ||
|
||||
fail "SSH setup removes a rejected hardening config"
|
||||
! grep -qF "systemctl reload sshd.service" "$test_dir/invalid.calls" ||
|
||||
fail "SSH setup must not reload a rejected config"
|
||||
! grep -q "Password logins are off" "$test_dir/invalid.output" ||
|
||||
fail "SSH setup must not claim rejected hardening succeeded"
|
||||
pass "SSH setup fails safely when sshd rejects the config"
|
||||
+213
@@ -0,0 +1,213 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(dirname "$0")/base-test.sh"
|
||||
|
||||
test_dir=$(mktemp -d)
|
||||
trap 'rm -rf "$test_dir"' EXIT
|
||||
|
||||
migration="$ROOT/migrations/1788124236.sh"
|
||||
stub_bin="$test_dir/bin"
|
||||
mkdir -p "$stub_bin"
|
||||
|
||||
cat >"$stub_bin/systemctl" <<'STUB'
|
||||
#!/bin/bash
|
||||
printf 'systemctl %s\n' "$*" >>"${CALL_LOG:?}"
|
||||
case "$1 $2" in
|
||||
"is-enabled --quiet") [[ ${SSHD_ENABLED:-0} == 1 ]] ;;
|
||||
"is-active --quiet") [[ ${SSHD_ACTIVE:-0} == 1 ]] ;;
|
||||
"reload sshd.service") [[ ${SSHD_RELOAD_VALID:-1} == 1 ]] ;;
|
||||
"disable --now") ;;
|
||||
*) exit 2 ;;
|
||||
esac
|
||||
STUB
|
||||
|
||||
cat >"$stub_bin/sshd" <<'STUB'
|
||||
#!/bin/bash
|
||||
printf 'sshd %s\n' "$*" >>"${CALL_LOG:?}"
|
||||
case $1 in
|
||||
-t) [[ ${SSHD_SYNTAX_VALID:-1} == 1 ]] ;;
|
||||
-T)
|
||||
printf 'PasswordAuthentication %s\n' "${SSHD_PASSWORD_AUTH:-no}"
|
||||
printf 'KbdInteractiveAuthentication %s\n' "${SSHD_KBD_AUTH:-no}"
|
||||
;;
|
||||
*) exit 2 ;;
|
||||
esac
|
||||
STUB
|
||||
|
||||
cat >"$stub_bin/sudo" <<'STUB'
|
||||
#!/bin/bash
|
||||
printf 'sudo %s\n' "$*" >>"${CALL_LOG:?}"
|
||||
if [[ ${SUDO_ALLOWED:-1} != 1 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
exec "$@"
|
||||
STUB
|
||||
|
||||
chmod +x "$stub_bin"/*
|
||||
|
||||
ssh-keygen -q -t ed25519 -N "" -f "$test_dir/key"
|
||||
public_key=$(<"$test_dir/key.pub")
|
||||
|
||||
run_migration() {
|
||||
local scenario=$1
|
||||
local home="$test_dir/$scenario/home"
|
||||
local root="$test_dir/$scenario/root"
|
||||
local config="$root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf"
|
||||
|
||||
mkdir -p "$home/.ssh" "${config%/*}"
|
||||
chmod "${HOME_MODE:-755}" "$home"
|
||||
: >"$test_dir/$scenario.calls"
|
||||
case "${AUTHORIZED_KEY_STATE:-valid}" in
|
||||
valid) printf '%s\n' "$public_key" >"$home/.ssh/authorized_keys" ;;
|
||||
invalid) printf 'not a public key\n' >"$home/.ssh/authorized_keys" ;;
|
||||
private) cat "$test_dir/key" >"$home/.ssh/authorized_keys" ;;
|
||||
symlink)
|
||||
printf '%s\n' "$public_key" >"$home/.ssh/imported_key"
|
||||
ln -s imported_key "$home/.ssh/authorized_keys"
|
||||
;;
|
||||
unreadable)
|
||||
printf '%s\n' "$public_key" >"$home/.ssh/authorized_keys"
|
||||
chmod 000 "$home/.ssh/authorized_keys"
|
||||
;;
|
||||
esac
|
||||
if [[ ${LOOSE_SSH_PERMS:-0} == 1 ]]; then
|
||||
chmod 755 "$home/.ssh"
|
||||
chmod 644 "$home/.ssh/authorized_keys"
|
||||
fi
|
||||
if [[ ${ALREADY_HARDENED:-0} == 1 ]]; then
|
||||
printf 'PasswordAuthentication no\n' >"$config"
|
||||
fi
|
||||
|
||||
# Keep the privileged production destination fixed in the shipped migration.
|
||||
# For this isolated test only, rewrite that one assignment in the input fed to
|
||||
# bash so no scenario can touch the host's /etc.
|
||||
sed "s|^config=/etc/ssh/sshd_config.d/10-omarchy-hardening.conf$|config=$config|" "$migration" |
|
||||
HOME="$home" CALL_LOG="$test_dir/$scenario.calls" PATH="$stub_bin:$PATH" \
|
||||
SSHD_ENABLED="${SSHD_ENABLED:-0}" SSHD_ACTIVE="${SSHD_ACTIVE:-0}" \
|
||||
SSHD_SYNTAX_VALID="${SSHD_SYNTAX_VALID:-1}" \
|
||||
SSHD_PASSWORD_AUTH="${SSHD_PASSWORD_AUTH:-no}" \
|
||||
SSHD_KBD_AUTH="${SSHD_KBD_AUTH:-no}" \
|
||||
SSHD_RELOAD_VALID="${SSHD_RELOAD_VALID:-1}" \
|
||||
SUDO_ALLOWED="${SUDO_ALLOWED:-1}" \
|
||||
bash -euo pipefail
|
||||
}
|
||||
|
||||
sshd_disabled() {
|
||||
grep -qxF "sudo systemctl disable --now sshd.service" "$test_dir/$1.calls"
|
||||
}
|
||||
|
||||
SSHD_ENABLED=0 SSHD_ACTIVE=0 run_migration disabled
|
||||
[[ ! -e $test_dir/disabled/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] ||
|
||||
fail "SSH migration leaves a disabled daemon alone"
|
||||
! grep -q '^sudo ' "$test_dir/disabled.calls" || fail "disabled SSH does not prompt for privileges"
|
||||
pass "SSH migration no-ops when sshd is not enabled or active"
|
||||
|
||||
ALREADY_HARDENED=1 SSHD_ENABLED=1 SSHD_ACTIVE=1 run_migration hardened >/dev/null
|
||||
[[ ! -s $test_dir/hardened.calls ]] || fail "an already-hardened machine must not touch sshd or prompt"
|
||||
grep -qxF "PasswordAuthentication no" "$test_dir/hardened/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf" ||
|
||||
fail "the existing hardening config is left alone"
|
||||
pass "SSH migration no-ops when the hardening config already exists"
|
||||
|
||||
# Without a usable key, sshd only accepts password logins — the hole the old
|
||||
# setup command could leave open. The migration closes it by disabling sshd.
|
||||
AUTHORIZED_KEY_STATE=missing SSHD_ENABLED=1 run_migration no-key >/dev/null
|
||||
[[ ! -e $test_dir/no-key/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] ||
|
||||
fail "SSH migration must not write the hardening config without an authorized key"
|
||||
sshd_disabled no-key || fail "SSH migration disables a password-only sshd"
|
||||
pass "SSH migration disables sshd when no key is authorized"
|
||||
|
||||
AUTHORIZED_KEY_STATE=invalid SSHD_ENABLED=1 run_migration invalid-key >/dev/null
|
||||
sshd_disabled invalid-key || fail "a malformed authorized_keys leaves sshd password-only"
|
||||
pass "SSH migration disables sshd when authorized_keys holds no valid key"
|
||||
|
||||
# ssh-keygen -lf accepts a whole private-key file, so only a per-line check
|
||||
# catches the classic `cp id_ed25519 authorized_keys` slip that sshd cannot use.
|
||||
AUTHORIZED_KEY_STATE=private SSHD_ENABLED=1 run_migration private-key >/dev/null
|
||||
[[ ! -e $test_dir/private-key/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] ||
|
||||
fail "SSH migration must not treat a private key as an authorized key"
|
||||
sshd_disabled private-key || fail "a private-key authorized_keys leaves sshd password-only"
|
||||
pass "SSH migration disables sshd when authorized_keys holds a private key"
|
||||
|
||||
# A dotfiles-managed symlink with a working key is a key-based setup, not a
|
||||
# keyless one; it must be hardened, never disabled.
|
||||
AUTHORIZED_KEY_STATE=symlink SSHD_ENABLED=1 SSHD_ACTIVE=1 run_migration symlink-key >/dev/null
|
||||
grep -qxF "PasswordAuthentication no" "$test_dir/symlink-key/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf" ||
|
||||
fail "SSH migration hardens a symlinked authorized_keys with a valid key"
|
||||
! sshd_disabled symlink-key || fail "SSH migration must not disable sshd when the symlinked key is usable"
|
||||
pass "SSH migration follows an authorized_keys symlink to its key"
|
||||
|
||||
# An unreadable file answers neither "keyless" nor "key-based": touch nothing.
|
||||
if (( EUID != 0 )); then
|
||||
AUTHORIZED_KEY_STATE=unreadable SSHD_ENABLED=1 run_migration unreadable >/dev/null
|
||||
[[ ! -e $test_dir/unreadable/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] ||
|
||||
fail "SSH migration must not harden against an unverifiable authorized_keys"
|
||||
! grep -q '^sudo ' "$test_dir/unreadable.calls" || fail "an unreadable authorized_keys does not prompt or disable"
|
||||
pass "SSH migration leaves an unreadable authorized_keys alone"
|
||||
fi
|
||||
|
||||
# StrictModes makes sshd ignore authorized_keys under a group-writable home,
|
||||
# so the key that validated would be unusable and passwords the only way in.
|
||||
HOME_MODE=775 SSHD_ENABLED=1 run_migration loose-home >/dev/null
|
||||
[[ ! -e $test_dir/loose-home/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] ||
|
||||
fail "SSH migration must not disable passwords when sshd would ignore the key"
|
||||
! grep -q '^sudo ' "$test_dir/loose-home.calls" || fail "a group-writable home does not prompt for privileges"
|
||||
pass "SSH migration leaves a group-writable home directory alone"
|
||||
|
||||
LOOSE_SSH_PERMS=1 SSHD_ENABLED=1 SSHD_ACTIVE=1 run_migration active >/dev/null
|
||||
config="$test_dir/active/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf"
|
||||
grep -qxF "PasswordAuthentication no" "$config" || fail "SSH migration disables password authentication"
|
||||
grep -qxF "KbdInteractiveAuthentication no" "$config" || fail "SSH migration disables keyboard-interactive authentication"
|
||||
[[ $(stat -c '%a' "$test_dir/active/home/.ssh") == "700" ]] ||
|
||||
fail "SSH migration tightens ~/.ssh so StrictModes accepts the key"
|
||||
[[ $(stat -c '%a' "$test_dir/active/home/.ssh/authorized_keys") == "600" ]] ||
|
||||
fail "SSH migration tightens authorized_keys so StrictModes accepts the key"
|
||||
grep -qxF "sudo sshd -t" "$test_dir/active.calls" || fail "SSH migration validates sshd syntax"
|
||||
grep -qxF "sudo sshd -T" "$test_dir/active.calls" || fail "SSH migration validates effective sshd settings"
|
||||
grep -qxF "sudo systemctl reload sshd.service" "$test_dir/active.calls" || fail "SSH migration reloads an active daemon"
|
||||
pass "SSH migration hardens and reloads an existing key-based SSH setup"
|
||||
|
||||
SSHD_ENABLED=1 SSHD_ACTIVE=0 run_migration stopped >/dev/null
|
||||
[[ -e $test_dir/stopped/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] ||
|
||||
fail "SSH migration hardens an enabled but stopped daemon"
|
||||
! grep -qF 'reload sshd.service' "$test_dir/stopped.calls" || fail "SSH migration must not start or reload a stopped daemon"
|
||||
pass "SSH migration hardens an enabled daemon without starting it"
|
||||
|
||||
# Conditions the migration cannot repair complete with a notice — leaving the
|
||||
# machine as it was — so they never block the migrations queued behind this one.
|
||||
SSHD_ENABLED=1 SSHD_ACTIVE=1 SSHD_PASSWORD_AUTH=yes run_migration ineffective >"$test_dir/ineffective.output" 2>&1 ||
|
||||
fail "an ineffective drop-in must complete without blocking later migrations"
|
||||
[[ ! -e $test_dir/ineffective/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] ||
|
||||
fail "SSH migration removes an ineffective config"
|
||||
! grep -qF 'reload sshd.service' "$test_dir/ineffective.calls" || fail "SSH migration must not reload ineffective hardening"
|
||||
pass "SSH migration backs off when another rule keeps password authentication enabled"
|
||||
|
||||
SSHD_ENABLED=1 SSHD_ACTIVE=1 SSHD_SYNTAX_VALID=0 run_migration invalid-config >"$test_dir/invalid-config.output" 2>&1 ||
|
||||
fail "a rejected config must complete without blocking later migrations"
|
||||
[[ ! -e $test_dir/invalid-config/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] ||
|
||||
fail "SSH migration removes a rejected config"
|
||||
! grep -qF 'reload sshd.service' "$test_dir/invalid-config.calls" || fail "SSH migration must not reload rejected hardening"
|
||||
pass "SSH migration backs off when sshd rejects the config"
|
||||
|
||||
# The installed config is valid, so a failed reload only delays it until the
|
||||
# next sshd restart; keep it staged rather than failing or removing it.
|
||||
SSHD_ENABLED=1 SSHD_ACTIVE=1 SSHD_RELOAD_VALID=0 run_migration reload-fail >"$test_dir/reload-fail.output" 2>&1 ||
|
||||
fail "a failed reload must complete without blocking later migrations"
|
||||
[[ -e $test_dir/reload-fail/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] ||
|
||||
fail "a failed reload keeps the valid hardening config staged"
|
||||
pass "SSH migration keeps the hardening staged when sshd cannot reload"
|
||||
|
||||
# Privileges are the one genuinely retryable failure: stay pending so the
|
||||
# login notifier prompts for a terminal run.
|
||||
if SUDO_ALLOWED=0 SSHD_ENABLED=1 SSHD_ACTIVE=1 run_migration no-sudo >"$test_dir/no-sudo.output" 2>&1; then
|
||||
fail "SSH migration must stay pending when privileges are unavailable"
|
||||
fi
|
||||
[[ ! -e $test_dir/no-sudo/root/etc/ssh/sshd_config.d/10-omarchy-hardening.conf ]] ||
|
||||
fail "no hardening config is left behind without privileges"
|
||||
pass "SSH migration stays pending until privileges are granted"
|
||||
|
||||
if SUDO_ALLOWED=0 AUTHORIZED_KEY_STATE=missing SSHD_ENABLED=1 run_migration no-sudo-keyless >"$test_dir/no-sudo-keyless.output" 2>&1; then
|
||||
fail "SSH migration must stay pending when it cannot disable a password-only sshd"
|
||||
fi
|
||||
pass "SSH migration stays pending when disabling sshd needs privileges"
|
||||
@@ -1,10 +1,7 @@
|
||||
#!/bin/bash
|
||||
#
|
||||
# The docker group is root-equivalent, so no automatic path may grant it. These
|
||||
# tests guard the paths that are not exercised by a fresh-install run: first-boot
|
||||
# provisioning replaying a recorded (or factory-snapshot) group list, and the
|
||||
# Quattro upgrade. Opting in stays a deliberate, warned step
|
||||
# (omarchy-setup-security-sudoless-docker).
|
||||
# Docker is root-equivalent, so no automatic path may grant it. Raw input access
|
||||
# is likewise excluded unless a feature that explicitly needs it is installed.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
@@ -13,11 +10,15 @@ source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
TMPDIR=$(mktemp -d)
|
||||
trap 'rm -rf "$TMPDIR"' EXIT
|
||||
|
||||
# First-boot provisioning must never grant docker even when it is recorded (an
|
||||
# older install, or a factory snapshot predating the opt-in default).
|
||||
# First-boot provisioning must not replay old privileged defaults.
|
||||
mkdir -p "$TMPDIR/bin"
|
||||
printf '#!/bin/bash\nexit 0\n' >"$TMPDIR/bin/getent" # every group "exists"
|
||||
chmod +x "$TMPDIR/bin/getent"
|
||||
cat >"$TMPDIR/bin/pacman" <<'STUB'
|
||||
#!/bin/bash
|
||||
[[ $1 == "-Qq" ]] || exit 2
|
||||
[[ " ${STUB_PACKAGES:-} " == *" $2 "* ]]
|
||||
STUB
|
||||
chmod +x "$TMPDIR/bin/getent" "$TMPDIR/bin/pacman"
|
||||
export PATH="$TMPDIR/bin:$PATH"
|
||||
|
||||
PROVISIONING_DIR="$TMPDIR/prov"
|
||||
@@ -29,9 +30,15 @@ eval "$(sed -n '/^user_groups() {/,/^}/p' "$ROOT/bin/omarchy-provision-owner")"
|
||||
groups=$(user_groups)
|
||||
|
||||
[[ ",$groups," == *",wheel,"* ]] || fail "user_groups always includes wheel"
|
||||
[[ ",$groups," == *",input,"* ]] || fail "user_groups includes recorded non-docker groups"
|
||||
[[ ",$groups," != *",input,"* ]] || fail "user_groups must not replay the blanket input grant"
|
||||
[[ ",$groups," == *",docker,"* ]] && fail "user_groups must never grant the docker group"
|
||||
pass "first-boot user_groups includes recorded groups but never docker"
|
||||
pass "first-boot user_groups replays neither privileged default"
|
||||
|
||||
groups=$(STUB_PACKAGES=xpadneo-dkms user_groups)
|
||||
[[ ",$groups," == *",input,"* ]] || fail "user_groups keeps input for installed controller support"
|
||||
groups=$(STUB_PACKAGES=ydotool user_groups)
|
||||
[[ ",$groups," == *",input,"* ]] || fail "user_groups keeps input for installed ydotool support"
|
||||
pass "first-boot user_groups keeps deliberate input-group opt-ins"
|
||||
|
||||
# The Quattro upgrade must not re-add the user to docker.
|
||||
if rg -q 'usermod -aG docker' "$ROOT/bin/omarchy-upgrade-to-quattro"; then
|
||||
|
||||
@@ -71,8 +71,15 @@ done
|
||||
pass "a URL naming a transport git does not implement never reaches git"
|
||||
|
||||
# The checker is a separate command, so its absence has to refuse the URL rather
|
||||
# than wave it through to git.
|
||||
if install_theme "https://github.com/example/omarchy-cool-theme.git" "$mock_bin:$PATH"; then
|
||||
# than wave it through to git. Installed machines carry the packaged checker in
|
||||
# /usr/bin, so absence is simulated by shadowing it with a stub that reports
|
||||
# command-not-found instead of thinning the PATH.
|
||||
missing_checker_bin="$test_tmp/missing-checker-bin"
|
||||
mkdir -p "$missing_checker_bin"
|
||||
printf '#!/bin/bash\nexit 127\n' >"$missing_checker_bin/omarchy-git-url-check"
|
||||
chmod +x "$missing_checker_bin/omarchy-git-url-check"
|
||||
|
||||
if install_theme "https://github.com/example/omarchy-cool-theme.git" "$missing_checker_bin:$mock_bin:$ROOT/bin:$PATH"; then
|
||||
fail "omarchy-theme-install refuses a URL it cannot check"
|
||||
fi
|
||||
|
||||
|
||||
@@ -15,6 +15,10 @@ fi
|
||||
test_tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$test_tmp"' EXIT
|
||||
|
||||
# The checkout may live under /home, which the tmpfs below hides, so take a
|
||||
# mount-safe copy of the helper before the mounts land.
|
||||
cp "$ROOT/bin/omarchy-windows-vm" "$test_tmp/omarchy-windows-vm"
|
||||
|
||||
# Hide host state before creating the production paths used by the root helper.
|
||||
mount -t tmpfs -o mode=0755,size=8m run-test /run
|
||||
mkdir -p /run/lock
|
||||
@@ -27,7 +31,7 @@ mount -t tmpfs -o uid=0,gid=0,mode=0710,size=1g home-alice /home/alice
|
||||
export HOME=/home/alice
|
||||
unset OMARCHY_WINDOWS_DIR
|
||||
set -- help
|
||||
source "$ROOT/bin/omarchy-windows-vm" >/dev/null 2>&1
|
||||
source "$test_tmp/omarchy-windows-vm" >/dev/null 2>&1
|
||||
|
||||
# The namespace maps the host filesystem's uid 0 to nobody. Only / remains on
|
||||
# that filesystem; all paths the helper mutates are isolated tmpfs mounts.
|
||||
|
||||
Reference in New Issue
Block a user