Refuse before touching anything, and move an old gateway cleanly

The first E2E run of the migration found that the new runtime cannot install its gateway service while the old package's gateway, still running from deleted files, holds the state directory, so that service is stopped first. Removal also takes the unit backups `openclaw update` leaves behind.

Codex's review found the rest: upstream's installer takes over any loaded gateway service, so a gateway running another OpenClaw, a foreign command on PATH or an openclaw shadowing it are refused before anything is set up; root is refused before --check runs the user's wrapper; --check needs the package, since --now would otherwise run pacman where no terminal can ask for a password; and moving a service clears the selectors that would aim the install at another unit.

Co-Authored-By: Codex XHigh <noreply@openai.com>
This commit is contained in:
Spencer BullandCodex XHigh committed 2026-09-26 00:18:45 -05:00
1 parent be18b324f8
commit 24d591da14
4 files changed
+98 -22

No files matched your search

+57 -15
View File
@@ -20,6 +20,12 @@ set -euo pipefail
mode=${1:-}
# Everything here runs the user's own files, --check included.
if (( EUID == 0 )); then
echo "Run this command as your desktop user, without sudo." >&2
exit 1
fi
prefix="$HOME/.openclaw"
runtime_command="$prefix/bin/openclaw"
command_path="$HOME/.local/bin/openclaw"
@@ -48,22 +54,47 @@ on_path() {
[[ $(realpath -m -- "$found") == "$(realpath -m -- "$runtime_command")" ]]
}
# Nothing yet, or the runtime's command.
path_clear() {
! type -P openclaw >/dev/null || on_path
}
# The package counts too: without it --now has a pacman step to take, and
# answering yes here would run that where no terminal can ask for a password.
installed() {
runtime_runs && on_path
omarchy-pkg-present openclaw && runtime_runs && on_path
}
# The service a unit runs, read from its ExecStart alone: other lines can name
# ~/.openclaw paths whichever OpenClaw the unit starts.
unit_runs() {
grep -E '^ExecStart=' "$1" | grep -qF -- "$2"
}
# A gateway service already running some other OpenClaw. Upstream's installer
# rewrites a loaded gateway service to the copy it has just installed, so
# seeding beside one would take it over.
foreign_gateway() {
local unit="$HOME/.config/systemd/user/openclaw-gateway.service"
[[ -f $unit ]] && ! unit_runs "$unit" "$prefix/" && ! unit_runs "$unit" "/usr/lib/node_modules/openclaw/"
}
# A service the openclaw package's own copy installed runs from
# /usr/lib/node_modules/openclaw, which that package no longer ships. Installed
# again from the runtime, it runs code `openclaw update` can replace; upstream
# rewrites the unit and restarts it. A service running any other OpenClaw is
# the user's arrangement and stays.
# rewrites the unit and starts it. It is stopped first: still running the old
# code from deleted files, it holds the state directory the newer runtime has
# to migrate, and could not restart from those files anyway. A service running
# any other OpenClaw is the user's arrangement and stays.
rehome_services() {
local role unit
for role in gateway node; do
unit="$HOME/.config/systemd/user/openclaw-$role.service"
if [[ -f $unit ]] && grep -qF "/usr/lib/node_modules/openclaw/" "$unit"; then
if [[ -f $unit ]] && unit_runs "$unit" "/usr/lib/node_modules/openclaw/"; then
echo "Moving the OpenClaw $role service to $prefix..."
if ! "$runtime_command" "$role" install --force; then
systemctl --user stop "openclaw-$role.service" 2>/dev/null || true
# The selectors would point the install at another unit and command.
if ! env -u OPENCLAW_PROFILE -u OPENCLAW_SYSTEMD_UNIT -u OPENCLAW_WRAPPER "$runtime_command" "$role" install --force; then
echo "Could not move the OpenClaw $role service. Finish with: openclaw $role install --force" >&2
return 1
fi
@@ -82,8 +113,8 @@ case "$mode" in
;;
esac
if (( EUID == 0 )); then
echo "Run this command as your desktop user, without sudo." >&2
if ! command_ours; then
echo "$command_path is not the OpenClaw Omarchy set up. Move it aside, then run omarchy-install-openclaw-cli --now again." >&2
exit 1
fi
@@ -94,12 +125,28 @@ fi
# A runtime that already answers is never reinstalled: it may be past the
# packaged release by its own updates, and seeding would take it back.
seeding=false
if ! runtime_runs; then
seeding=true
if [[ ! -r $seed/install-cli.sh || ! -r $seed/openclaw.tgz ]]; then
echo "The installed OpenClaw package cannot set up a self-updating OpenClaw. Run 'omarchy update', then try again." >&2
exit 1
fi
fi
# Refused before anything in the home is touched.
if ! path_clear; then
echo "'openclaw' on PATH is $(type -P openclaw), which is what Omarchy would run instead of $runtime_command." >&2
echo "Remove it or reorder PATH, then run omarchy-install-openclaw-cli --now again." >&2
exit 1
fi
if [[ $seeding == "true" ]] && foreign_gateway; then
echo "The OpenClaw gateway service runs another OpenClaw, which setting one up in $prefix would take over." >&2
echo "Remove that gateway with 'openclaw gateway uninstall', then run omarchy-install-openclaw-cli --now again." >&2
exit 1
fi
if [[ $seeding == "true" ]]; then
# Every choice the installer reads from the environment is named, so an
# OPENCLAW_INSTALL_METHOD or OPENCLAW_PREFIX left in a shell cannot move it.
echo "Setting up OpenClaw in $prefix..."
@@ -110,18 +157,13 @@ if ! runtime_runs; then
fi
fi
if command_ours; then
mkdir -p "${command_path%/*}"
ln -sfn "$runtime_command" "$command_path"
else
echo "$command_path is not the OpenClaw Omarchy set up. Move it aside, then run omarchy-install-openclaw-cli --now again." >&2
exit 1
fi
mkdir -p "${command_path%/*}"
ln -sfn "$runtime_command" "$command_path"
rehome_services
if ! on_path; then
echo "$runtime_command is ready, but 'openclaw' on PATH is $(type -P openclaw || echo missing), which is what Omarchy runs." >&2
echo "Remove it or reorder PATH, then run omarchy-install-openclaw-cli --now again." >&2
echo "Put ~/.local/bin on PATH, then run omarchy-install-openclaw-cli --now again." >&2
exit 1
fi
+3 -2
View File
@@ -35,8 +35,9 @@ for unit_file in "$unit_dir"/openclaw-gateway.service "$unit_dir"/openclaw-node.
systemctl --user disable --now "$unit" 2>/dev/null ||
unit_stopped "$unit"; then
# .bak is what `gateway install --force` leaves behind when it rewrites a
# unit, so it goes with the unit.
rm -f "$unit_file" "$unit_file.bak" "$unit_dir/default.target.wants/$unit"
# unit, and .reconcile-*.bak what `openclaw update` leaves when it
# refreshes one, so they go with the unit.
rm -f "$unit_file" "$unit_file.bak" "$unit_file".reconcile-*.bak "$unit_dir/default.target.wants/$unit"
systemctl --user daemon-reload 2>/dev/null || true
# A unit that had been failing stays listed as "not-found failed" after
# its file is gone until its failed state is reset.
+34 -5
View File
@@ -21,6 +21,10 @@ cat >"$mock_bin/omarchy-pkg-add" <<'SH'
printf 'pkg-add %s\n' "$*" >>"$OMARCHY_TEST_ROOT/events"
touch "$OMARCHY_TEST_ROOT/package-installed"
SH
cat >"$mock_bin/systemctl" <<'SH'
#!/bin/bash
printf 'systemctl %s\n' "$*" >>"$OMARCHY_TEST_ROOT/events"
SH
chmod +x "$mock_bin/"*
# Stands in for upstream's install-cli.sh: it writes the command the way the
@@ -34,7 +38,7 @@ cat >"$prefix/bin/openclaw" <<EOF
#!/usr/bin/env bash
set -euo pipefail
[[ -z "${OMARCHY_TEST_INSTALL_BROKEN:-}" ]] || exit 1
printf 'runtime %s\n' "\$*" >>"$OMARCHY_TEST_ROOT/events"
printf 'runtime %s%s\n' "\$*" "\${OPENCLAW_PROFILE:+ profile=\$OPENCLAW_PROFILE}" >>"$OMARCHY_TEST_ROOT/events"
exec true "$prefix/tools/node-v24.19.0/lib/node_modules/openclaw/dist/entry.js" "\$@"
EOF
chmod 755 "$prefix/bin/openclaw"
@@ -122,10 +126,33 @@ printf '#!/bin/bash\n' >"$test_tmp/usr-bin/openclaw"
chmod +x "$test_tmp/usr-bin/openclaw"
run omarchy-install-openclaw-cli --now && fail "an openclaw earlier on PATH fails the install"
grep -q "on PATH is $test_tmp/usr-bin/openclaw" "$test_tmp/output" || fail "an openclaw earlier on PATH is named" "$(cat "$test_tmp/output")"
[[ ! -e $test_home/.openclaw && ! -e $command ]] || fail "an openclaw earlier on PATH is refused before anything is touched"
rm "$test_tmp/usr-bin/openclaw"
run omarchy-install-openclaw-cli --now || fail "--now follows once nothing shadows the runtime" "$(cat "$test_tmp/output")"
printf '#!/bin/bash\n' >"$test_tmp/usr-bin/openclaw"
chmod +x "$test_tmp/usr-bin/openclaw"
run omarchy-install-openclaw-cli --check && fail "--check calls a shadowed runtime installed"
rm "$test_tmp/usr-bin/openclaw"
run omarchy-install-openclaw-cli --check || fail "--check follows once nothing shadows the runtime"
pass "the runtime has to be the openclaw PATH finds"
pass "the runtime has to be the openclaw PATH finds, and anything else is refused before it is set up"
# A runtime that answers without the package still leaves --now a pacman step,
# which the default agent must not run outside a terminal.
rm "$test_tmp/package-installed"
run omarchy-install-openclaw-cli --check && fail "--check calls a runtime without its package installed"
pass "--check needs the package too, so --now never has a password to ask for unseen"
# Upstream's installer rewrites a loaded gateway service to the copy it has
# just made, so a gateway running another OpenClaw stops the seeding first.
new_home foreign-gateway
mkdir -p "$test_home/.config/systemd/user"
printf 'Environment=OPENCLAW_CONFIG_PATH=%s/.openclaw/openclaw.json\nExecStart=/opt/node %s/openclaw/dist/index.js gateway\n' "$test_home" "$test_home" \
>"$test_home/.config/systemd/user/openclaw-gateway.service"
run omarchy-install-openclaw-cli --now && fail "a gateway running another OpenClaw stops the install"
grep -q "runs another OpenClaw" "$test_tmp/output" || fail "a gateway running another OpenClaw is named" "$(cat "$test_tmp/output")"
! grep -q '^install-cli' "$events" && [[ ! -e $test_home/.openclaw ]] ||
fail "a gateway running another OpenClaw is refused before anything is set up" "$(cat "$events")"
pass "a gateway running another OpenClaw is refused before upstream's installer can take it over"
# A gateway the old package installed runs from /usr/lib/node_modules, which
# the seed package no longer ships; one running any other OpenClaw stays.
@@ -134,9 +161,11 @@ units="$test_home/.config/systemd/user"
mkdir -p "$units"
printf 'ExecStart=/usr/bin/node /usr/lib/node_modules/openclaw/dist/index.js gateway --port 18789\n' >"$units/openclaw-gateway.service"
printf 'ExecStart=/opt/node /home/someone/openclaw/dist/index.js node run\n' >"$units/openclaw-node.service"
run omarchy-install-openclaw-cli --now || fail "--now moves the old package's services" "$(cat "$test_tmp/output")"
grep -Fxq "runtime gateway install --force" "$events" || fail "--now moves a gateway the old package installed" "$(cat "$events")"
! grep -q "runtime node install" "$events" || fail "--now leaves a service running another OpenClaw alone" "$(cat "$events")"
OPENCLAW_PROFILE=work run omarchy-install-openclaw-cli --now || fail "--now moves the old package's services" "$(cat "$test_tmp/output")"
grep -A1 -Fx "systemctl --user stop openclaw-gateway.service" "$events" | grep -Fxq "runtime gateway install --force" ||
fail "--now stops a gateway the old package installed, then moves it" "$(cat "$events")"
! grep -q "runtime node install\|openclaw-node" "$events" || fail "--now leaves a service running another OpenClaw alone" "$(cat "$events")"
! grep -q "install --force profile=work" "$events" || fail "--now moves the default unit whatever profile the shell selects" "$(cat "$events")"
pass "a service the old package installed moves to the runtime, and only that one"
# The migration moves only machines that have the package, and waits for the
+4
View File
@@ -245,6 +245,8 @@ fresh_openclaw_home() {
"$HOME/.local/share/applications" "$HOME/.local/share/icons/hicolor/256x256/apps"
touch "$HOME/.config/systemd/user/openclaw-gateway.service" \
"$HOME/.config/systemd/user/openclaw-gateway.service.bak" \
"$HOME/.config/systemd/user/openclaw-gateway.service.reconcile-0f1e.bak" \
"$HOME/.config/systemd/user/openclaw-gateway.service.reconcile-0f1e.receipt.bak" \
"$HOME/.config/systemd/user/openclaw-node.service" \
"$HOME/.openclaw/openclaw.json" \
"$HOME/.local/share/applications/OpenClaw.desktop" \
@@ -269,6 +271,8 @@ fresh_openclaw_home
for gone in .config/systemd/user/openclaw-gateway.service \
.config/systemd/user/openclaw-gateway.service.bak \
.config/systemd/user/openclaw-gateway.service.reconcile-0f1e.bak \
.config/systemd/user/openclaw-gateway.service.reconcile-0f1e.receipt.bak \
.config/systemd/user/default.target.wants/openclaw-gateway.service \
.config/systemd/user/openclaw-node.service \
.config/systemd/user/default.target.wants/openclaw-node.service \