Skip to content

fix: run the pinned installer instead of the overlay's copy - #154

Open
andrepimenta wants to merge 1 commit into
mainfrom
fix/sync-install-bin-exec-bit
Open

fix: run the pinned installer instead of the overlay's copy#154
andrepimenta wants to merge 1 commit into
mainfrom
fix/sync-install-bin-exec-bit

Conversation

@andrepimenta

Copy link
Copy Markdown
Member

Summary

yarn install installs zero base skills for anyone with CONSENSYS_SKILLS_DIR set, and reports Install complete. while doing it. Found while testing 0.3.0 end to end from the published package in metamask-mobile.

This is a regression from #135 — my own change.

Root cause

tools/sync picked its installer with an executable test:

if [[ -x "$SCRIPT_DIR/install" ]]; then
  INSTALL_BIN="$SCRIPT_DIR/install"
else
  for src in "${SOURCES[@]}"; do ... done   # keeps the LAST match
fi

npm and Yarn do not preserve the executable bit when unpacking a tarball, so every tools/* file arrives 0644 in a consumer install:

node_modules/@metamask/skills/tools/   -rw-r--r--   install
.skills-cache/metamask-skills/tools/   -rwxr-xr-x   install

So the guard is always false in the published package and always true in a git checkout — which is the only place it was ever exercised. The pinned installer was never selected. The fallback loop ran instead, and since it keeps the last match while SOURCES appends CONSENSYS_SKILLS_DIR after METAMASK_SKILLS_DIR, the private overlay's tools/install won every install.

An overlay predating the base: key has no notion of base skills, so it skipped all 16 as excluded by --domain and exited 0.

Deterministic on a real checkout — identical command either side:

chmod +x tools/install → base-included: 16, .claude/skills: 10
chmod -x tools/install → base-included:  0, .claude/skills:  0

Why it survived review

Two conditions had to line up, and #135 supplied both:

  • Preferring the overlay in the fallback was already wrong, but harmless — the overlay's installer understood every flag in use, so output was identical.
  • base: is the first flag it doesn't implement, turning a silent misroute into zero skills.

Before #135, sync preferred $METAMASK_SKILLS_DIR/tools/install, which the CLI defaults to the .skills-cache clone — a git checkout, so the bit survived and the first branch always won. #135 was right to stop executing code from that unpinned clone, but swapped in a condition that cannot hold in the artifact it was protecting.

It cannot reproduce from a source checkout, so neither CI nor local dev would ever see it.

Changes

File Change
tools/sync test -f, exec bash "$INSTALL_BIN"
tools/bootstrap, tools/deploy same treatment — both exec a sibling installer directly
test/cli.test.mjs regression test

The test builds the fixture that reproduces it — package installer at 0644 as npm ships it, overlay at 0755 as git checks it out — and asserts the pinned one runs. Reverting only tools/sync fails it with STALE-OVERLAY.

Testing

  • yarn test — 70/70 (69 + the new one)
  • yarn lint, yarn lint:changelog — clean
  • Verified against the published 0.3.0 in metamask-mobile: chmod +x on the package tools restores all 16 base skills

Follow-up

Needs a 0.3.1 — 0.3.0 cannot deliver base skills to overlay users. metamask-mobile#35263 is pinned at ^0.3.0 and picks the patch up automatically.

Also worth noting: the supply-chain property #135 claimed has never actually held for a consumer. Until this lands, every install executes tools/install from the git cache or the overlay rather than the lockfile-pinned package.

`yarn install` was installing zero base skills for anyone with
CONSENSYS_SKILLS_DIR set, and still exiting 0.

tools/sync chose its installer with `-x "$SCRIPT_DIR/install"`. npm and Yarn do
not preserve the executable bit when unpacking a tarball, so every tools/* file
arrives 0644 in a consumer install and that test is always false there — while
being always true in a git checkout, which is the only place it ever got
exercised. The pinned installer was therefore never selected, and the fallback
loop ran instead. That loop keeps the LAST matching source and SOURCES appends
CONSENSYS_SKILLS_DIR after METAMASK_SKILLS_DIR, so the private overlay's copy of
tools/install won every time. An overlay predating the `base:` frontmatter key
has no notion of base skills, so it skipped all 16 as "excluded by --domain" and
reported "Install complete."

This is a regression from #135. Before that, sync preferred
`$METAMASK_SKILLS_DIR/tools/install`, which the CLI defaults to the .skills-cache
clone — a git checkout, so the bit survived and the first branch always won. #135
correctly stopped executing code from that unpinned clone, but replaced the
condition with one that cannot hold in the artifact it was meant to protect. The
misrouting to the overlay had been latent since the fallback was introduced; it
only became observable once `base:` gave the two installers different behaviour.

Test `-f` and invoke via `bash`, so the bit is irrelevant either way. Applied the
same treatment to tools/bootstrap and tools/deploy, which exec a sibling
installer directly for the same reason.

The regression test builds the fixture that reproduces it — package installer at
0644 as npm ships it, overlay installer at 0755 as git checks it out — and
asserts the pinned one runs. Reverting only tools/sync fails it with
`STALE-OVERLAY`.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant