From aeb8f29a6494e9e39ec12e197fedff8d92115e01 Mon Sep 17 00:00:00 2001 From: Paul Bakaus Date: Mon, 31 Aug 2026 16:23:32 -0700 Subject: [PATCH] Launcher: engine-probe PATH validation, working .cmd download path; CI: drop stale path, add oracle job Byte-identical copies of the engine repo's launchers (engine main af7572c): the retired 3.x npm CLI on PATH or in ~/.impeccable/bin is rejected by the engine-probe handshake instead of hijacking every verb; impeccable.cmd's download path is rewritten as straight-line goto flow (the parenthesized blocks expanded %url%/%cached% at parse time, making it dead code) with certutil sha256 verification and a windows-arm64 -> x64 asset fallback; the final error points at the release download instead of npm i -g (npm still serves the 3.x CLI). ci.yml: the generated-output check no longer diffs the deleted cli/engine/detect-antipatterns-browser.js, and a new oracle job fetches the pinned engine (bun run fetch:engine) and replays tests/oracle/ against it. The job is continue-on-error with a loud warning until the first engine release exists; flipping it to required is a release-time toggle, documented in the workflow. Verified here: sh -n on both launcher copies, bun run build green, full oracle replay against the rebuilt engine binary green (770 pass, 0 fail), and a launcher behavior test proving a fake 3.x CLI on PATH is skipped while the download + checksum chain completes against a local file server. Prepared with AI assistance (Claude Code). --- .github/workflows/ci.yml | 48 ++++++++++- skill/scripts/impeccable | 62 +++++++++++--- skill/scripts/impeccable.cmd | 156 ++++++++++++++++++++++++++++------- 3 files changed, 222 insertions(+), 44 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7671947fa..c516ad44f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -111,7 +111,9 @@ jobs: run: npx --yes web-ext@10 lint --source-dir dist/extension-firefox - name: Verify generated tracked outputs - run: git diff --exit-code -- .agents .claude .cursor .gemini .github/skills plugin cli/engine/detect-antipatterns-browser.js extension/detector + # cli/engine/ left the tree with the Rust engine swap; the vendored + # extension/detector/ path stays listed for when its vendoring lands. + run: git diff --exit-code -- .agents .claude .cursor .gemini .github/skills plugin extension/detector - name: Upload build artifacts uses: actions/upload-artifact@v7 @@ -123,6 +125,50 @@ jobs: !dist/extension-firefox/ retention-days: 7 + # Behavior gate: replays the tests/oracle/ goldens against the pinned + # engine binary (ENGINE_VERSION). Without this job the oracle only ever + # runs on developer laptops: tests/oracle.test.mjs skips cleanly when no + # binary is present, so the default suite is silent about it on CI. + # + # continue-on-error is a release-time toggle: until the first engine + # release is published to impeccable-dist, `bun run fetch:engine` 404s and + # the job would block every PR on an asset that cannot exist yet. Once + # v is live, flip `continue-on-error` to false so oracle + # regressions fail CI instead of only annotating it. + oracle: + runs-on: ubuntu-latest + continue-on-error: true + steps: + - name: Checkout repository + uses: actions/checkout@v7 + + - name: Setup Node + uses: actions/setup-node@v7 + with: + node-version: 24 + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Fetch pinned engine binary + id: fetch + continue-on-error: true + run: bun run fetch:engine + + - name: Replay oracle goldens + if: steps.fetch.outcome == 'success' + run: node tests/oracle/run.mjs + + - name: Annotate missing engine release + if: steps.fetch.outcome != 'success' + run: | + echo "::warning title=Oracle not run::bun run fetch:engine could not download engine v$(cat ENGINE_VERSION) from the impeccable-dist release channel. The 762-case oracle behavior gate did NOT run. Expected until the first engine release is published; after that, publish the release assets and flip this job's continue-on-error to false." + test: runs-on: ubuntu-latest needs: test-matrix diff --git a/skill/scripts/impeccable b/skill/scripts/impeccable index 791ac2f2e..108aa9a22 100755 --- a/skill/scripts/impeccable +++ b/skill/scripts/impeccable @@ -2,9 +2,29 @@ # Impeccable launcher. Runs the platform binary shipped next to this script: # /bin/-/impeccable # Order: $IMPECCABLE_BIN, the sibling binary, ~/.impeccable/bin/impeccable, -# then `impeccable` on PATH (the npm shim). Never needs Node. +# the version-pinned cache, then `impeccable` on PATH. Never needs Node. +# The unversioned home binary and the PATH candidate are validated with the +# engine-probe handshake first: the retired 3.x npm CLI also installed a bin +# named `impeccable`, and exec'ing it would fail every verb with +# "Unknown command". Trusted candidates (IMPECCABLE_BIN, the sibling binary, +# the version-pinned cache) are exec'd without a probe: hooks run them on +# every edit and must stay fast. set -eu +# True when the candidate answers the engine handshake (prints +# "impeccable-engine ", exit 0). Quiet and fast (<100ms). +# IMPECCABLE_LAUNCHER_PROBE marks the child as a probe: a copy of this +# launcher reached recursively (e.g. symlinked onto PATH as `impeccable`) +# then skips its own probes and refuses to download, so probing stays cheap +# and can never loop. +probe_ok() { + case "$(IMPECCABLE_LAUNCHER_PROBE=1 "$1" engine-probe 2>/dev/null || true)" in + impeccable-engine*) return 0 ;; + esac + return 1 +} +probing=${IMPECCABLE_LAUNCHER_PROBE:-} + if [ -n "${IMPECCABLE_BIN:-}" ] && [ -x "${IMPECCABLE_BIN}" ]; then exec "${IMPECCABLE_BIN}" "$@" fi @@ -40,24 +60,42 @@ if [ -f "$bin" ]; then # Lost the executable bit in transit (zip extraction, some copiers). chmod +x "$bin" 2>/dev/null && exec "$bin" "$@" fi -if [ -x "${HOME:-/nonexistent}/.impeccable/bin/impeccable" ]; then - exec "${HOME}/.impeccable/bin/impeccable" "$@" +# On Windows (an MSYS/Git Bash shell) the cached names carry .exe so this +# launcher and impeccable.cmd share one cache. +exe="" +[ "$os" = windows ] && exe=".exe" +home_bin="${HOME:-/nonexistent}/.impeccable/bin/impeccable$exe" +if [ -z "$probing" ] && [ -x "$home_bin" ] && probe_ok "$home_bin"; then + exec "$home_bin" "$@" fi # Version-pinned user cache, filled by the download below or by `impeccable update`. version="" [ -f "$dir/VERSION" ] && version=$(tr -d '[:space:]' < "$dir/VERSION") cache_root="${IMPECCABLE_HOME:-${HOME:-/nonexistent}/.impeccable}" -cached="$cache_root/bin/$version/impeccable" +cached="$cache_root/bin/$version/impeccable$exe" if [ -n "$version" ] && [ -x "$cached" ]; then exec "$cached" "$@" fi -if command -v impeccable >/dev/null 2>&1; then +if [ -z "$probing" ] && command -v impeccable >/dev/null 2>&1 && probe_ok impeccable; then exec impeccable "$@" fi # Last resort: fetch this version's binary for the current platform from the # public release channel into the user cache. Needs network; sandboxes without # egress preinstall the binary on PATH instead. +fetch_url() { + if command -v curl >/dev/null 2>&1; then + curl -fsSL --retry 2 -o "$tmp" "$1" 2>/dev/null + elif command -v wget >/dev/null 2>&1; then + wget -q -O "$tmp" "$1" 2>/dev/null + else + return 1 + fi +} +if [ -n "$probing" ]; then + # Inside another launcher's probe: no download, fail fast and quiet. + exit 127 +fi if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then base="${IMPECCABLE_DOWNLOAD_BASE:-https://github.com/renaissance-geek-inc/impeccable-dist/releases/download}" asset="impeccable-$os-$arch" @@ -66,10 +104,12 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then tmp="$cache_root/bin/$version/.impeccable.part.$$" mkdir -p "$cache_root/bin/$version" 2>/dev/null fetched=0 - if command -v curl >/dev/null 2>&1; then - curl -fsSL --retry 2 -o "$tmp" "$url" 2>/dev/null && fetched=1 - elif command -v wget >/dev/null 2>&1; then - wget -q -O "$tmp" "$url" 2>/dev/null && fetched=1 + if fetch_url "$url"; then + fetched=1 + elif [ "$os" = windows ] && [ "$arch" = arm64 ]; then + # Windows on ARM runs x64 binaries; fall back when no arm64 asset exists. + url="$base/v$version/impeccable-windows-x64.exe" + fetch_url "$url" && fetched=1 fi if [ "$fetched" = 1 ]; then if command -v curl >/dev/null 2>&1 && curl -fsSL -o "$tmp.sha256" "$url.sha256" 2>/dev/null; then @@ -90,6 +130,6 @@ if [ -n "$version" ] && [ "$os" != unknown ] && [ "$arch" != unknown ]; then rm -f "$tmp" 2>/dev/null fi -echo "impeccable: no binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2 -echo "Install one: npm i -g impeccable, or download impeccable-$os-$arch from https://github.com/renaissance-geek-inc/impeccable-dist/releases into $cache_root/bin/$version/impeccable" >&2 +echo "impeccable: no engine binary for $os-$arch found (looked in $bin, $cached, PATH)." >&2 +echo "Download impeccable-$os-$arch from https://github.com/renaissance-geek-inc/impeccable-dist/releases into $cache_root/bin/$version/impeccable$exe (then chmod +x), or set IMPECCABLE_BIN to a preinstalled engine binary. Docs: https://impeccable.style" >&2 exit 127 diff --git a/skill/scripts/impeccable.cmd b/skill/scripts/impeccable.cmd index 2028db8ff..bde13d5ea 100644 --- a/skill/scripts/impeccable.cmd +++ b/skill/scripts/impeccable.cmd @@ -1,44 +1,136 @@ @echo off setlocal -rem Impeccable launcher (Windows). Runs bin\windows-x64\impeccable.exe next to this file. -if defined IMPECCABLE_BIN if exist "%IMPECCABLE_BIN%" ( - "%IMPECCABLE_BIN%" %* - exit /b %ERRORLEVEL% -) +rem Impeccable launcher (Windows). Runs bin\windows-\impeccable.exe next +rem to this file, else a cached or freshly downloaded engine binary. +rem +rem Structure notes (this file is exercised by dry parsing and string-level +rem tests, not yet on a real Windows machine): +rem - No multi-line parenthesized blocks: cmd expands %var% at block parse +rem time, which made the old download path read back empty %url%/%cached%. +rem Linear goto flow keeps every expansion on its own line, and avoids +rem delayed expansion eating ! characters in user arguments. +rem - The unversioned user binary and the PATH candidate are validated with +rem the engine-probe handshake (see :probe) so the retired 3.x npm CLI, +rem whose bin is also named impeccable, is never exec'd. IMPECCABLE_BIN, +rem the sibling binary, and the version-pinned cache stay trusted. +rem - Downloads are verified against the .sha256 sidecar via certutil when +rem the sidecar is served; on ARM64 the arm64 asset is tried first and the +rem x64 asset is the fallback (Windows on ARM runs x64 binaries). if not defined IMPECCABLE_SKILL_DIR set "IMPECCABLE_SKILL_DIR=%~dp0.." if not defined IMPECCABLE_SELF set "IMPECCABLE_SELF=%~f0" set "arch=x64" if /I "%PROCESSOR_ARCHITECTURE%"=="ARM64" set "arch=arm64" + +if not defined IMPECCABLE_BIN goto no_env_bin +if not exist "%IMPECCABLE_BIN%" goto no_env_bin +set "run=%IMPECCABLE_BIN%" +goto run +:no_env_bin + set "bin=%~dp0bin\windows-%arch%\impeccable.exe" -if exist "%bin%" ( - "%bin%" %* - exit /b %ERRORLEVEL% -) -if exist "%USERPROFILE%\.impeccable\bin\impeccable.exe" ( - "%USERPROFILE%\.impeccable\bin\impeccable.exe" %* - exit /b %ERRORLEVEL% -) +if not exist "%bin%" goto no_sibling +set "run=%bin%" +goto run +:no_sibling + +set "home_bin=%USERPROFILE%\.impeccable\bin\impeccable.exe" +if not exist "%home_bin%" goto no_home_bin +if defined IMPECCABLE_LAUNCHER_PROBE goto no_home_bin +call :probe "%home_bin%" +if not "%probe_ok%"=="1" goto no_home_bin +set "run=%home_bin%" +goto run +:no_home_bin + set "version=" if exist "%~dp0VERSION" set /p version=<"%~dp0VERSION" if not defined IMPECCABLE_HOME set "IMPECCABLE_HOME=%USERPROFILE%\.impeccable" set "cached=%IMPECCABLE_HOME%\bin\%version%\impeccable.exe" -if defined version if exist "%cached%" ( - "%cached%" %* - exit /b %ERRORLEVEL% -) -where impeccable >nul 2>nul && ( - impeccable %* - exit /b %ERRORLEVEL% -) -if defined version ( - if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/renaissance-geek-inc/impeccable-dist/releases/download" - set "url=%IMPECCABLE_DOWNLOAD_BASE%/v%version%/impeccable-windows-%arch%.exe" - if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul - where curl.exe >nul 2>nul && curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul && move /y "%cached%.part" "%cached%" >nul 2>nul - if exist "%cached%" ( - "%cached%" %* - exit /b %ERRORLEVEL% - ) -) -echo impeccable: no binary found (looked in %bin%, %cached%, PATH). Install one: npm i -g impeccable 1>&2 +if not defined version goto no_cache +if not exist "%cached%" goto no_cache +set "run=%cached%" +goto run +:no_cache + +if defined IMPECCABLE_LAUNCHER_PROBE goto download +where impeccable >nul 2>nul +if errorlevel 1 goto download +call :probe impeccable +if not "%probe_ok%"=="1" goto download +impeccable %* +exit /b + +:download +rem Last resort: fetch this version's binary from the release channel into +rem the version-pinned user cache, verify it, then run it. Never inside +rem another launcher's probe: fail fast and quiet instead. +if defined IMPECCABLE_LAUNCHER_PROBE exit /b 127 +if not defined version goto fail +where curl.exe >nul 2>nul +if errorlevel 1 goto fail +if not defined IMPECCABLE_DOWNLOAD_BASE set "IMPECCABLE_DOWNLOAD_BASE=https://github.com/renaissance-geek-inc/impeccable-dist/releases/download" +if not exist "%IMPECCABLE_HOME%\bin\%version%" mkdir "%IMPECCABLE_HOME%\bin\%version%" >nul 2>nul +set "asset=impeccable-windows-%arch%.exe" +set "url=%IMPECCABLE_DOWNLOAD_BASE%/v%version%/%asset%" +curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul +if not errorlevel 1 goto verify +if not "%arch%"=="arm64" goto fail +set "asset=impeccable-windows-x64.exe" +set "url=%IMPECCABLE_DOWNLOAD_BASE%/v%version%/%asset%" +curl.exe -fsSL -o "%cached%.part" "%url%" >nul 2>nul +if errorlevel 1 goto fail + +:verify +rem Mirrors the sh launcher: a missing sidecar or hash tool skips +rem verification; a mismatch is fatal. +curl.exe -fsSL -o "%cached%.sha256" "%url%.sha256" >nul 2>nul +if not errorlevel 1 goto have_sidecar +del "%cached%.sha256" >nul 2>nul +goto place +:have_sidecar +set "expected=" +set /p expected=<"%cached%.sha256" +for /f "tokens=1" %%h in ("%expected%") do set "expected=%%h" +set "actual=" +for /f "skip=1 delims=" %%h in ('certutil -hashfile "%cached%.part" SHA256 2^>nul') do if not defined actual set "actual=%%h" +del "%cached%.sha256" >nul 2>nul +if not defined expected goto place +if not defined actual goto place +set "actual=%actual: =%" +if /I "%actual%"=="%expected%" goto place +del "%cached%.part" >nul 2>nul +echo impeccable: checksum mismatch downloading %url% 1>&2 +exit /b 127 + +:place +move /y "%cached%.part" "%cached%" >nul 2>nul +if not exist "%cached%" goto fail +set "run=%cached%" +goto run + +:run +"%run%" %* +exit /b + +:probe +rem Sets probe_ok=1 when %1 answers the engine handshake: prints +rem "impeccable-engine " and exits 0. The 3.x npm CLI answers any +rem unknown verb with "Unknown command", exit 1, so it never passes. +set "probe_ok=" +set "probe_tmp=%TEMP%\impeccable-probe-%RANDOM%%RANDOM%.txt" +set "IMPECCABLE_LAUNCHER_PROBE=1" +"%~1" engine-probe >"%probe_tmp%" 2>nul +set "probe_err=%ERRORLEVEL%" +set "IMPECCABLE_LAUNCHER_PROBE=" +if not "%probe_err%"=="0" goto probe_done +findstr /b /c:"impeccable-engine" "%probe_tmp%" >nul 2>nul +if not errorlevel 1 set "probe_ok=1" +:probe_done +del "%probe_tmp%" >nul 2>nul +exit /b 0 + +:fail +del "%cached%.part" >nul 2>nul +echo impeccable: no engine binary found (looked in %bin%, %cached%, PATH). 1>&2 +echo Download impeccable-windows-%arch%.exe from https://github.com/renaissance-geek-inc/impeccable-dist/releases and save it as %cached%, or set IMPECCABLE_BIN to a preinstalled engine binary. Docs: https://impeccable.style 1>&2 exit /b 127