skills: let the skillset own the skills it owns, and stop a dangling link from killing boot
Baked skill links won over the live skillset clone for all three vendored skills, so a pushed edit to skills/mempalace/SKILL.md was invisible in every container until the next image build -- measured on two hosts (live md5 129bcc4752 vs baked 5236024fef). Cause was ordering, not intent: the baked links are created early with a create-only-when-absent guard to close a smoke readiness race, and the skillset deploy runs last and treats them as foreign. The comment claimed the opposite of the behaviour. The fix is not "skillset always wins". Ownership is per-skill: pi-extensions is owned by its package repo and copied over the snapshot at build time, so the skillset's lagging duplicate must keep losing; pi-devbox-environment is authored here. Only mempalace is skillset-owned. devbox-skill-reconcile therefore runs after the deploy and repoints only the names in skills/skillset-owned.txt, replacing a link solely when it points into the baked tree, so a real directory or a link pointing elsewhere is never disturbed. Precedence is now user override -> live clone (owned names) -> baked snapshot, with the early links intact as the fallback so the readiness race stays closed. Reviewing that turned up a latent boot-abort in the pre-existing baked-link block: `[ ! -e "$link" ]` is TRUE for a dangling symlink, so once a link can point into /workspace/skillset, a vanished mount makes plain `ln -s` fail with "File exists" -- and under `set -euo pipefail` that aborts container start before `exec "$@"`. Reachable on `docker restart` or a host reboot, not on a recreate, since ~/.agents is not a volume on any host. Now `ln -sfn`, which heals the link back to the baked fallback. Smoke additions cover what let this ship: the stale-snapshot canary grepped a phrase present in BOTH the stale and fresh copies, so it passed throughout; it now pins the newest section. Link targets are asserted, not just `test -L`; the owned-list content is asserted both ways; and the reconciler's replace path -- which no CI container exercises, since none mounts a skillset -- is covered by fabricating one. A mutation test showed the obvious three assertions still pass with the "is this link ours?" guard deleted, so a discriminating case was added: an owned name whose link is a user override outside the baked tree. Also refreshes the mempalace snapshot to skillset 670f7f1 (without it the fix helps only hosts that mount skillset) and corrects README, which documented the old, wrong precedence in three places. Verified with 12 fixture cases plus 2 mutants: ownership respected against the real trees, user overrides preserved, relative/trailing-slash/CRLF/space/glob inputs handled, dangling link healed, read-only skills dir exits 0, idempotent.
This commit is contained in:
+28
-5
@@ -58,10 +58,17 @@ fi
|
||||
# the runtime skill-link assertion. Pointing at the image path (/usr/local/...)
|
||||
# keeps the skill fresh from the image and surviving volume recreate (unlike
|
||||
# anything baked under a home dir, which a named volume would shadow). Created
|
||||
# only when absent, so a same-named skillset skill (deployed later, at the end
|
||||
# of this script) or a user override is never clobbered; the skillset deploy
|
||||
# classifies these as foreign-links and its --prune-stale pass leaves them
|
||||
# alone (only dangling symlinks are pruned).
|
||||
# only when absent, so a user override is never clobbered.
|
||||
#
|
||||
# NB: "created only when absent" does NOT hand a same-named skillset skill
|
||||
# priority — the opposite. The skillset deploy runs at the end of this script
|
||||
# and classifies these links as foreign, so through v1.8.4 the BAKED copy
|
||||
# always won and an edit pushed to a skillset-owned skill was invisible until
|
||||
# the next image build. The links below are therefore the FALLBACK only;
|
||||
# devbox-skill-reconcile (invoked right after the skillset deploy) hands the
|
||||
# skillset-OWNED skills back to the live clone. Ownership is per-skill, listed
|
||||
# in skills/skillset-owned.txt — see VENDORED.md for why pi-extensions must
|
||||
# keep losing to the baked copy.
|
||||
DEVBOX_SKILLS_SRC=/usr/local/share/pi-devbox/skills
|
||||
if [ -d "$DEVBOX_SKILLS_SRC" ]; then
|
||||
mkdir -p "$HOME/.agents/skills"
|
||||
@@ -69,7 +76,16 @@ if [ -d "$DEVBOX_SKILLS_SRC" ]; then
|
||||
[ -d "$_sk" ] || continue
|
||||
_skname=$(basename "$_sk")
|
||||
if [ ! -e "$HOME/.agents/skills/$_skname" ]; then
|
||||
ln -s "${_sk%/}" "$HOME/.agents/skills/$_skname"
|
||||
# -sfn, not -s: `[ ! -e ]` is TRUE for a DANGLING symlink (-e follows the
|
||||
# link), and since v1.8.5 these links can point into /workspace/skillset
|
||||
# (see devbox-skill-reconcile, invoked after the skillset deploy). If that
|
||||
# mount vanishes while the writable layer survives — a `docker restart` or
|
||||
# a host reboot under restart: unless-stopped, as opposed to a recreate —
|
||||
# plain `ln -s` fails with "File exists" and, under `set -e`, aborts
|
||||
# container start before `exec "$@"`. With -f the broken link heals back to
|
||||
# the baked fallback, and the reconciler re-points it in the same boot if
|
||||
# the clone is back.
|
||||
ln -sfn "${_sk%/}" "$HOME/.agents/skills/$_skname"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
@@ -385,6 +401,13 @@ elif [ -x /workspace/skillset/deploy-skills.sh ]; then
|
||||
fi
|
||||
if [ -n "$SKILLSET_DEPLOY" ]; then
|
||||
"$SKILLSET_DEPLOY" --bootstrap --prune-stale >/dev/null 2>&1 || true
|
||||
# The deploy leaves the early baked links (above) in place as foreign links,
|
||||
# which silently shadows the live clone for skills the skillset OWNS. Repoint
|
||||
# just those; baked stays the fallback, user overrides still win. `|| true`:
|
||||
# a skill-link refinement must never break container start.
|
||||
if command -v devbox-skill-reconcile >/dev/null 2>&1; then
|
||||
devbox-skill-reconcile "$(dirname "$SKILLSET_DEPLOY")" || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── Execute command ──────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user