Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/enforced-assertions-baseline
Original file line number Diff line number Diff line change
@@ -1 +1 @@
638
635
62 changes: 53 additions & 9 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,26 @@ AGENT_TYPE="" # claude-code, codex, gemini, antigravity — passed via --agent-
# the SKILL.md the installer itself had written with the wrong flavor.
AGMSG_SHARED_SKILL_TPL_TYPES="gemini antigravity opencode hermes cursor grok-build"

# Put <src> at <dest>, then remove any leftover <src>. The arm is chosen by
# <dest>, so the fix's scope matches the defect's (#747):
# - regular <dest>: `mv` — an atomic rename, so an interrupted install leaves
# either the whole old config or the whole new one, never a torn file. This
# is the common path and must stay atomic.
# - symlinked <dest>: write THROUGH the link (a redirect follows it) so a
# config.toml managed as a symlink (stow/chezmoi/manual dotfiles) keeps its
# link and its target receives the edit. `mv` would replace the link with a
# plain file and strand the edit on a detached copy — the actual #747 bug.
# This arm is non-atomic (there is no atomic write-through-a-link with plain
# POSIX tools), but the exposure is confined to symlink users, whose target
# is typically a version-controlled dotfile.
move_into_place() {
if [ -L "$2" ]; then
cat "$1" > "$2" && rm -f "$1"
else
mv "$1" "$2"
fi
}

configure_codex_sandbox() {
# --- Configure Codex sandbox (if Codex is installed) ---
# The Codex bridge writes pidfiles/sockets/request files under the
Expand Down Expand Up @@ -158,13 +178,13 @@ configure_codex_sandbox() {
done=1
}
{ print }
' "$code_config" > "$code_config.tmp" && mv "$code_config.tmp" "$code_config"
' "$code_config" > "$code_config.tmp" && move_into_place "$code_config.tmp" "$code_config"
elif grep -q '^\[sandbox_workspace_write\]' "$code_config" 2>/dev/null; then
# Section exists but no writable_roots
awk -v entries="$entries" '
{ print }
/^\[sandbox_workspace_write\]/ { print "writable_roots = [" entries "]" }
' "$code_config" > "$code_config.tmp" && mv "$code_config.tmp" "$code_config"
' "$code_config" > "$code_config.tmp" && move_into_place "$code_config.tmp" "$code_config"
else
# No section at all
printf '\n[sandbox_workspace_write]\nwritable_roots = [%s]\n' "$entries" >> "$code_config"
Expand Down Expand Up @@ -265,22 +285,46 @@ if [ "$UPDATE_ONLY" = true ]; then
# exactly that, not "we ended up with some skill name one way or another").
CMD_WAS_EXPLICIT=false
[ -n "$CMD_NAME" ] && CMD_WAS_EXPLICIT=true
# Find existing install. If --cmd was passed, update exactly that skill;
# otherwise preserve the historical "first installed agmsg skill" behavior.
# Find existing install. If --cmd was passed, update exactly that skill.
# Otherwise, scan for installs and require exactly one: a glob expands in
# collation order, not installation order, and nothing records which
# install came first, so guessing from a list of more than one is a
# silent coin flip on which install (and the shared ~/.agents/bin/codex
# shim it refreshes) gets updated (#599). A single install is unaffected
# -- this is the common case and it still "just works".
#
# No name-based exclusion for backup-shaped directories: --cmd has no
# reserved-name validation, so any pattern that would catch a real backup
# (e.g. "agmsg.bak-20260731") can equally match a legitimately chosen
# install name (e.g. "agmsg.bak-tool") -- there is no substring that is
# guaranteed to mean "not a real install" (co2 review, #659). A leftover
# backup directory that still carries the .agmsg marker is therefore just
# another candidate: it makes the set ambiguous, and ambiguous is exactly
# what this fix already refuses to guess through, below.
if [ -n "$CMD_NAME" ]; then
SKILL_DIR="$AGENTS_DIR/skills/$CMD_NAME"
if [ ! -f "$SKILL_DIR/.agmsg" ]; then
echo " ! Not installed: ~/.agents/skills/$CMD_NAME. Run ./install.sh --cmd $CMD_NAME first." >&2
exit 1
fi
else
SKILL_DIR=""
candidates=()
for d in "$AGENTS_DIR"/skills/*/; do
if [ -f "${d}.agmsg" ]; then
SKILL_DIR="${d%/}"
break
fi
d="${d%/}"
[ -f "$d/.agmsg" ] && candidates+=("$d")
done
case "${#candidates[@]}" in
0) SKILL_DIR="" ;;
1) SKILL_DIR="${candidates[0]}" ;;
*)
echo " ! Several agmsg installs found:" >&2
for d in "${candidates[@]}"; do
echo " $(basename "$d")" >&2
done
echo " ! --update with no --cmd cannot tell which one you mean. Pass --cmd <name> to pick one." >&2
exit 1
;;
esac
fi
if [ -z "$SKILL_DIR" ]; then
echo " ! Not installed. Run ./install.sh first." >&2
Expand Down
99 changes: 87 additions & 12 deletions scripts/remote.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1952,7 +1952,7 @@ _remote_sync_engine_start_locked() {
# Stop only an engine whose argv proves that it owns this team. A stale
# pidfile may point at a recycled, unrelated process and must never authorize
# signalling that process.
IFS=$'\t' read -r old_state old_pid < <(_remote_sync_engine_status "$team")
IFS=$'\t' read -r old_state old_pid < <(AGMSG_SKIP_SYSTEMD_PROBE=1 _remote_sync_engine_status "$team")
if [ "$old_state" = "running" ]; then
kill "$old_pid" 2>/dev/null || true
fi
Expand Down Expand Up @@ -2019,7 +2019,7 @@ _remote_sync_engine_stop() {
local team="$1" pidfile pid state
pidfile="$(_remote_sync_engine_pidfile "$team")"
[ -f "$pidfile" ] || return 0
IFS=$'\t' read -r state pid < <(_remote_sync_engine_status "$team")
IFS=$'\t' read -r state pid < <(AGMSG_SKIP_SYSTEMD_PROBE=1 _remote_sync_engine_status "$team")
if [ "$state" = "running" ]; then
if ! _remote_sync_engine_reap_owned "$team" "$pid"; then
echo "agmsg: sync engine pid $pid did not stop" >&2
Expand All @@ -2041,11 +2041,73 @@ _remote_sync_engine_stop() {
rm -f "$(_remote_sync_engine_cycle_stamp "$team")" 2>/dev/null || true
}

# Return the systemd user-unit state as "state<TAB>pid".
#
# A unit that is not installed is not a systemd-managed team here, so callers
# retain the pidfile behavior. An existing unit is different: an active unit
# whose MainPID cannot be authenticated is UNKNOWN and must not be shadowed by
# a new unmanaged engine. The command can be replaced in tests; no host
# systemd query is needed there.
_remote_systemd_engine_status() {
local team="$1" unit show load active sub pid command systemctl_bin
unit="agmsg-remote-sync-$team.service"
systemctl_bin="${AGMSG_SYSTEMCTL:-systemctl}"
command -v "$systemctl_bin" >/dev/null 2>&1 || { printf 'unavailable\t\n'; return; }
show="$($systemctl_bin --user show "$unit" -p LoadState -p ActiveState -p SubState -p MainPID 2>/dev/null)" || {
printf 'absent\t\n'
return
}
load="$(printf '%s\n' "$show" | sed -n 's/^LoadState=//p')"
active="$(printf '%s\n' "$show" | sed -n 's/^ActiveState=//p')"
sub="$(printf '%s\n' "$show" | sed -n 's/^SubState=//p')"
pid="$(printf '%s\n' "$show" | sed -n 's/^MainPID=//p')"
[ "$load" = "not-found" ] && { printf 'absent\t\n'; return; }
case "$active:$sub" in
active:running)
if _agmsg_pid_valid "$pid" && _agmsg_pid_alive_local "$pid"; then
command="$(compat_get_cmdline "$pid" 2>/dev/null || true)"
if agmsg_cmdline_names_path "$command" "$SCRIPT_DIR/internal/remote-sync.mjs" &&
case "$command" in *" run --team $team") true ;; *) false ;; esac; then
printf 'running\t%s\n' "$pid"
else
printf 'unknown\t%s\n' "$pid"
fi
else
printf 'unknown\t%s\n' "$pid"
fi
;;
active:starting|active:reloading|active:auto-restart|activating:*|deactivating:*)
printf 'starting\t%s\n' "$pid"
;;
inactive:*|failed:*)
printf 'inactive\t%s\n' "$pid"
;;
*)
printf 'unknown\t%s\n' "$pid"
;;
esac
}

# Print "<state>\t<pid>", where pid is empty when no valid pid is available.
# A live PID is not enough: PID reuse can make an unrelated process pass
# kill -0, so running requires the exact engine script/team suffix in argv.
_remote_sync_engine_status() {
local team="$1" pidfile pid command expected
local team="$1" pidfile pid command expected systemd_state systemd_pid
REMOTE_SYNC_ENGINE_SUPERVISOR=""
REMOTE_SYNC_ENGINE_SUPERVISOR_PID=""
if [ "${AGMSG_SKIP_SYSTEMD_PROBE:-0}" != 1 ]; then
IFS=$'\t' read -r systemd_state systemd_pid < <(_remote_systemd_engine_status "$team")
else
systemd_state=absent
fi
case "$systemd_state" in
running|starting|inactive|unknown)
REMOTE_SYNC_ENGINE_SUPERVISOR="systemd"
REMOTE_SYNC_ENGINE_SUPERVISOR_PID="$systemd_pid"
printf '%s\t%s\n' "$systemd_state" "$systemd_pid"
return
;;
esac
pidfile="$(_remote_sync_engine_pidfile "$team")"
if [ ! -f "$pidfile" ]; then
printf 'stopped\t\n'
Expand Down Expand Up @@ -2077,7 +2139,7 @@ _remote_sync_engine_status() {
_remote_sync_engine_reap_owned() {
local team="$1" owned_pid="$2" state pid signal attempts
for signal in TERM KILL; do
IFS=$'\t' read -r state pid < <(_remote_sync_engine_status "$team")
IFS=$'\t' read -r state pid < <(AGMSG_SKIP_SYSTEMD_PROBE=1 _remote_sync_engine_status "$team")
if ! _agmsg_pid_alive_local "$owned_pid"; then return 0; fi
[ "$state" = "running" ] && [ "$pid" = "$owned_pid" ] || return 1
kill "-$signal" "$owned_pid" 2>/dev/null || true
Expand Down Expand Up @@ -2489,6 +2551,12 @@ _remote_status_one() {
case "$engine_state" in
running)
echo "$team connected (engine running, pid $engine_pid) since $connected_at" ;;
starting)
echo "$team connected (engine starting under systemd, pid $engine_pid; do not run sync start) since $connected_at" ;;
inactive)
echo "$team connected (engine inactive under systemd; run: systemctl --user restart agmsg-remote-sync-$team.service) since $connected_at" ;;
unknown)
echo "$team connected (engine state unknown under systemd; do not run sync start; inspect systemctl --user status agmsg-remote-sync-$team.service) since $connected_at" ;;
stopped)
echo "$team connected (engine stopped — run: bash $(agmsg_shq "$SKILL_DIR/scripts/remote.sh") sync start $(agmsg_shq "$team")) since $connected_at" ;;
stale)
Expand Down Expand Up @@ -2818,11 +2886,18 @@ cmd_sync_start() {
fi

IFS=$'\t' read -r engine_state engine_pid < <(_remote_sync_engine_status "$team")
if [ "$engine_state" = "running" ]; then
echo "Sync engine already running (pid $engine_pid)."
agmsg_lock_release
return
fi
case "$engine_state" in
running)
echo "Sync engine already running (pid $engine_pid)."
agmsg_lock_release
return
;;
starting|inactive|unknown)
echo "agmsg: systemd owns team '$team' in state '$engine_state'; inspect or restart the user unit instead of sync start" >&2
agmsg_lock_release
return 1
;;
esac

logfile="$CONNECTION_ROOT/run/remote-sync.$team.log"
[ -f "$logfile" ] && log_offset=$(( $(wc -c < "$logfile" | tr -d ' ') + 1 ))
Expand Down Expand Up @@ -2872,7 +2947,7 @@ cmd_sync_start() {
# not the rest of the machine.
agmsg_lock_release
while [ "$i" -lt 1600 ]; do
IFS=$'\t' read -r engine_state ready_pid < <(_remote_sync_engine_status "$team")
IFS=$'\t' read -r engine_state ready_pid < <(AGMSG_SKIP_SYSTEMD_PROBE=1 _remote_sync_engine_status "$team")
if [ "$engine_state" = "running" ] && [ "$ready_pid" = "$started_pid" ] &&
tail -c "+$log_offset" "$logfile" 2>/dev/null |
awk -v nonce="\"startup_nonce\":\"$startup_nonce\"" '
Expand Down Expand Up @@ -3367,7 +3442,7 @@ cmd_set_endpoint() {
done
fi

IFS=$'\t' read -r engine_state engine_pid < <(_remote_sync_engine_status "$team")
IFS=$'\t' read -r engine_state engine_pid < <(AGMSG_SKIP_SYSTEMD_PROBE=1 _remote_sync_engine_status "$team")
[ "$engine_state" = "running" ] && was_running=1
_remote_sync_engine_stop "$team" || {
echo "agmsg: the sync engine did not stop; refusing to move the endpoint under it" >&2
Expand Down Expand Up @@ -3398,7 +3473,7 @@ cmd_set_endpoint() {
# command ran is restarted too (never silently left stopped, and a restart
# is what hands it the moved address -- a running engine keeps its old
# config in memory). _remote_sync_engine_start kills a live engine first.
IFS=$'\t' read -r end_state end_pid < <(_remote_sync_engine_status "$team")
IFS=$'\t' read -r end_state end_pid < <(AGMSG_SKIP_SYSTEMD_PROBE=1 _remote_sync_engine_status "$team")
if [ "$was_running" -eq 1 ] || [ "$end_state" = "running" ]; then
# Same rule as cmd_pull and cmd_connect: the move is this command's purpose
# and it is done by here, so a start failure reports rather than fails --
Expand Down
Loading
Loading