Skip to content

fix(install): refuse to guess between multiple installs on --update with no --cmd - #659

Open
fujibee wants to merge 3 commits into
mainfrom
fix-599-install
Open

fix(install): refuse to guess between multiple installs on --update with no --cmd#659
fujibee wants to merge 3 commits into
mainfrom
fix-599-install

Conversation

@fujibee

@fujibee fujibee commented Aug 7, 2026

Copy link
Copy Markdown
Owner

Summary

Fixes #599. install.sh --update with no --cmd used to stop at the first .agmsg-marked directory a glob yielded. A glob expands in collation order, not installation order, and nothing records which install came first, so the comment describing this as preserving "the historical first installed agmsg skill" behavior was describing an invariant the code has no way to observe. On a machine with more than one install, --update silently touched whichever one sorted first -- including the shared ~/.agents/bin/codex shim it refreshes -- while the install the caller actually meant, and the caller, saw nothing wrong.

What changed

install.sh's update-mode skill-directory selection now enumerates every .agmsg-marked directory instead of stopping at the first:

  • Zero or one candidate: behaves exactly as before. This is the common single-install case and it is unaffected.
  • Two or more candidates: fails closed. Lists the candidate names and asks for --cmd <name> instead of guessing, with a nonzero exit.
  • Directory names shaped like a backup (*.bak*) are excluded from the candidate set. They carry the same .agmsg marker as a real install (it gets copied along with everything else) but are never the one an unqualified --update means.

--cmd <name> continues to target exactly the named install regardless of how many others exist, unchanged.

Tests

Two new cases in tests/test_install.bats, alongside the existing --update --cmd coverage:

  • install: --update with no --cmd refuses to guess between two real installs (#599) -- two real installs present, bare --update exits nonzero, lists both names, and neither install's VERSION changes.
  • install: --update with no --cmd still finds the one real install past a bak-named decoy (#599) -- one real install plus a .bak--named directory carrying the same marker; bare --update still finds and updates the real one, and the decoy is untouched.

Full tests/test_install.bats run: 49/49 ok, exit 0 (includes the two new tests and the pre-existing --update --cmd + backup-skill case, unaffected since it always specifies --cmd). bash -n install.sh and git diff --check both pass. Manually smoke-tested end to end against an isolated temp HOME with two real installs (fails closed as expected) and a single install (updates normally).

fujibee added 3 commits August 7, 2026 14:39
--update with no --cmd

The skill-directory selection loop broke on the first .agmsg-
marked directory a glob yielded. A glob expands in collation
order, not installation order, and nothing records which install
came first, so the "historical first installed" comment described
an invariant the code could not observe. On a machine with more
than one install, --update silently touched whichever one sorted
first -- including the shared ~/.agents/bin/codex shim it
refreshes -- while the intended install, and the caller, saw
nothing wrong.

Enumerate all .agmsg-marked directories instead of stopping at the
first. Zero or one candidate behaves exactly as before -- the
common single-install case is unaffected. Two or more now fails
closed: list the candidates and ask for --cmd, rather than
guessing. Backup-shaped directory names (*.bak*) are excluded from
the candidate set, since they carry the same .agmsg marker as a
real install but are never the one a bare --update means.

Fixes #599.
missed-owner test

co2 review on #659: the "*.bak*" exclusion pattern also matched a
real install whose --cmd name merely contains "bak" (e.g.
"agmsg.bakery") -- --cmd has no reserved-name validation, so that
install silently dropped out of the candidate set, reproducing the
same silent-wrong-pick failure #599 exists to close. Narrow the
pattern to the literal ".bak-" shape actually reported
(agmsg.bak-20260731), which does not collide with a plausible
command name.

Also tighten the two-installs regression test: it was comparing
VERSION strings, which are the same source-derived value for both
installs and would not distinguish "one got silently updated" from
"neither did". Compare distinct per-install sentinels instead.
closed

Two rounds of narrowing ("*.bak*", then "*.bak-*") hit the same
collision: --cmd has no reserved-name validation, so any pattern
that catches a real backup name (e.g. "agmsg.bak-20260731") can
equally match a legitimately chosen install name (e.g.
"agmsg.bak-tool" or "agmsg.bakery"), silently dropping a real
install from the candidate set -- the exact failure #599 exists to
close, just moved into the exclusion instead of the glob order.

No code in this repo generates a ".bak-"-named directory; the
reported name is a human backup convention, not something the
installer produces, so no name-based pattern can be made safe here.

Stop special-casing names. A directory that still carries the
.agmsg marker is just another candidate; more than one candidate is
exactly the ambiguity this fix already refuses to guess through.
The regression test now shows a leftover backup-shaped directory
landing in the same "Several agmsg installs found" refusal as any
other second candidate, rather than being silently excluded or
silently chosen.
fujibee added a commit that referenced this pull request Aug 10, 2026
…cing (review)

Two issues raised in review, both against the ownership guard added earlier:

1. An existing agmsg shim written before this PR shipped has no
   `# agmsg-shim-owner:` line at all. shim_owner_script_dir returns empty for
   it, and the install guard required owner to be non-empty before refusing
   -- so a shim with unknown, unrecorded ownership was treated as "unowned,
   safe to take" and silently overwritten by any new, differently-named
   install. That is #553's own bug, recurring at exactly the migration
   moment it matters most: the first time a second install runs an
   installer carrying this fix against a production shim that predates it.

   Fixed by treating "is an agmsg shim (marker matches) but records no
   owner" as "legacy owner, unknown" rather than "unowned" -- it now fails
   closed the same as a foreign-owned shim, distinguishing the two only in
   the message shown (named owner vs. "predates ownership tracking").
   AGMSG_CODEX_SHIM_FORCE=1 still claims it either way.

   Also, non-blocking but folded in: the owner comment now stores the
   %q-quoted path (matching the executable export line's own quoting)
   rather than the raw path, and every reader compares/displays the quoted
   form -- a path containing a literal newline could otherwise have forged
   a second, fake comment line of its own.

2. install.sh's --update block forced the shim reclaim unconditionally,
   including for a bare `--update` with no `--cmd`. That resolves its
   target by scanning for a single existing install rather than the caller
   naming one, and on this base (integration/remote) that scan does not
   fail closed when more than one install is present -- the fail-closed fix
   for that (#599) is PR #659, still pending against main and not merged
   here. Forcing unconditionally meant whichever install a glob happened to
   resolve to could steal the shim from another install the caller never
   named at all, compounding #599 with a #553-shaped consequence.

   Fixed by capturing whether --cmd was passed explicitly (before CMD_NAME
   gets defaulted or resolved to anything else) and forcing only then. Bare
   `--update` still refreshes a shim this same install already owns (the
   common single-install case, unaffected), but no longer reaches past one
   it doesn't.

Forced reproduction for both: reverted each fix in turn, confirmed the
matching test fails (RED) against the reverted code -- a crafted legacy
shim gets silently claimed in the first case, an unrelated install's shim
gets silently stolen by an ambiguously-resolved bare --update in the second
-- then restored the fix and confirmed both pass (GREEN).
fujibee added a commit that referenced this pull request Aug 10, 2026
… (review)

The previous two fixes -- failing closed on an owner-unknown (legacy, pre-
this-feature) shim, and no longer forcing bare `--update` -- are each
correct alone but combined to block the single-install upgrade path they
were never meant to touch. Nearly every real machine's existing shim
predates ownership tracking and has no owner comment; a routine
`install.sh --update` with no `--cmd` (how a single-install user normally
upgrades) could no longer refresh it at all, leaving a stale shim (possibly
still pointing at a pre-move path) in place indefinitely.

Adds agmsg_only_one_install: true iff exactly one `.agmsg`-marked directory
exists under ~/.agents/skills, derived directly from this machine's actual
state rather than assumed from whether #599's fail-closed multi-install
handling (PR #659) is merged wherever this runs -- which it may not be.
An owner-unknown shim is now claimable without --cmd/--force specifically
when this holds: with only one agmsg install anywhere, nothing else could
have written the shim, so there is no one to take it from. With two or
more installs present the check returns false and the existing fail-closed
behavior is unchanged.

Forced reproduction: reverted the allowance, confirmed the new "bare
--update migrates this machine's own pre-#553 shim" test fails (RED) --
the legacy shim is left stale, still pointing at its old path -- then
restored the fix and confirmed it migrates to the new owner-tracked format
(GREEN).
fujibee added a commit that referenced this pull request Aug 10, 2026
…all (review)

Non-blocking review note: agmsg_only_one_install's ~/.agents/skills scan
answers the same question #659 (still open against main, unmerged) will
independently need for its own bare-`--update` candidate enumeration --
the third time tonight two implementations have answered the same
question separately (after #722 -> #733, #741's config validation).

Splits the scan into agmsg_install_candidates (lists candidate install
dirs, one per line) with agmsg_only_one_install now just counting its
output. This doesn't unify the two implementations -- #659 doesn't exist
on this base to point at yet -- but gives it something to call instead of
re-scanning ~/.agents/skills a second time once it lands on a shared base
with this file. Behavior unchanged; tests unchanged and still 73/73.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

install.sh --update silently updates whichever install sorts first, and reports success

1 participant