diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6c7af009e..255a4fbe1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -71,6 +71,8 @@ jobs: run: bash tools/ci/copied-sources-check.sh - name: second-engine playbooks log to a file and end their tree (C35) run: bash tools/ci/second-engine-check.sh + - name: no playbook quits, pauses or resumes the installed app (self-test first, then the tree) + run: bash tools/ci/playbook-quit-check.sh --self-test && bash tools/ci/playbook-quit-check.sh - name: the signer is never piped into head run: bash tools/ci/signer-pipe-check.sh - name: bash bodies in PowerShell job scripts pass bash -n, the lost-quote class (self-test first, then the tree) diff --git a/relay/playbooks/ember-tune-pc1.ps1 b/relay/playbooks/ember-tune-pc1.ps1 index 1f8a70b9e..741619f35 100644 --- a/relay/playbooks/ember-tune-pc1.ps1 +++ b/relay/playbooks/ember-tune-pc1.ps1 @@ -179,8 +179,8 @@ while (-not $p.HasExited) { # C35 (5 October 2026): the only quit this script may send goes to the TUNE engine's own URL file in the scratch # root, never to a file under the installed app's folder; the RESULT line names the file it used $u = Join-Path $sApp 'app.url' - $installedUrl = Join-Path $appDir 'app.url' - if ((Resolve-Path -LiteralPath $u -ErrorAction SilentlyContinue).Path -eq (Resolve-Path -LiteralPath $installedUrl -ErrorAction SilentlyContinue).Path -or $u -like '*\igneum\app\*') { + # the only URL file this script may quit is its own scratch root's; the installed app's folder is refused by name + if ((Resolve-Path -LiteralPath $u -ErrorAction SilentlyContinue).Path -like (Join-Path $appDir '*') -or $u -like '*\igneum\app\*') { Write-Output ('RESULT TUNE quit refused: ' + $u + ' is the installed app''s URL file') } elseif (Test-Path -LiteralPath $u) { Write-Output ('RESULT TUNE quit asked of the tune engine through ' + $u + ' (pid ' + $p.Id + ')') diff --git a/relay/playbooks/shard-test.ps1 b/relay/playbooks/shard-test.ps1 index 2efb18c93..2535aa115 100644 --- a/relay/playbooks/shard-test.ps1 +++ b/relay/playbooks/shard-test.ps1 @@ -52,13 +52,8 @@ function Stop-App { & powershell.exe -NoProfile -ExecutionPolicy Bypass -File $stop 2>&1 | ForEach-Object { Say (" " + (Strip "$_")) } return } - $urlFile = Join-Path $env:LOCALAPPDATA 'igneum\app\app.url' - if (Test-Path $urlFile) { - $url = (Get-Content $urlFile -Raw).Trim() - if ($url) { - try { Invoke-WebRequest -Uri ($url + 'api/quit') -Method POST -Body '{}' -ContentType 'application/json' -UseBasicParsing -TimeoutSec 5 | Out-Null; Say 'asked the engine to quit over its local API' } catch { Say ('local API did not answer: ' + $_.Exception.Message) } - } - } + # (5 October 2026 rule: a job never quits the installed app it did not start; the api/quit that stood here is gone. + # Stopping the app is the signed `restart` job kind's work; without the stop script this waits for the app to stop.) $until = (Get-Date).AddSeconds(50) while ((Get-Date) -lt $until) { if (@(Get-Process -Name 'igneum-app', 'igneumd' -ErrorAction SilentlyContinue).Count -eq 0) { break } diff --git a/tools/ci/playbook-quit-check.sh b/tools/ci/playbook-quit-check.sh new file mode 100755 index 000000000..04b04c271 --- /dev/null +++ b/tools/ci/playbook-quit-check.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +# The standing rule of 5 October 2026, 23:05 UTC (CLAUDE.md): a job never quits, pauses, resumes or restarts the +# installed app it did not start. A test engine started by a job runs on its own data dir with its own URL file; a +# job may send quit, pause or resume only to an engine it started itself (the URL it created); the installed app is +# touched only through the signed `restart` and `update-now` job kinds. This check fails any playbook or script under +# relay/playbooks, tools/windows, tools/proving-v1 or packaging that reads the INSTALLED app's URL file +# (%LOCALAPPDATA%\igneum\app\app.url, $env:IGNEUM_APP_DIR\app.url, ~/Library/Application Support/Igneum/app/app.url) +# and sends api/quit, api/pause or api/resume. A scratch root's own app.url (igneum-tune-*, igneum-sweep) is fine. +# bash tools/ci/playbook-quit-check.sh [--self-test] +set -euo pipefail +cd "$(dirname "$0")/../.." +check_file() { + local f="$1" bad=0 + grep -qE "api/(quit|pause|resume)" "$f" || return 0 + if grep -vE '^\s*#' "$f" | grep -qE "igneum\\\\app\\\\app\.url|igneum/app/app\.url|Application Support/Igneum/app/app\.url|IGNEUM_APP_DIR[^\n]*app\.url|\\\$appDir[^\n]*'app\.url'"; then + echo "playbook-quit: $f reads the installed app's URL file and sends quit, pause or resume to it (a job may only quit an engine it started: its own scratch URL file)"; bad=1 + fi + return $bad +} +if [ "${1:-}" = "--self-test" ]; then + t="$(mktemp -d)" + printf '%s\n' '$urlFile = Join-Path $env:LOCALAPPDATA '"'"'igneum\app\app.url'"'"'' 'Invoke-WebRequest -Uri ($url + '"'"'api/quit'"'"') -Method POST' > "$t/bad.ps1" + printf '%s\n' '$u = Join-Path $sApp '"'"'app.url'"'"' # $sApp = $root\app, $root = igneum-tune-' 'Invoke-WebRequest -Uri ((Get-Content $u) + '"'"'api/quit'"'"')' > "$t/good.ps1" + if check_file "$t/bad.ps1" >/dev/null; then echo "self-test FAILED: the bad playbook passed"; exit 1; fi + if ! check_file "$t/good.ps1"; then echo "self-test FAILED: the good playbook failed"; exit 1; fi + rm -rf "$t"; echo "self-test passed: the installed app's URL file with a quit fails, a scratch URL file passes"; exit 0 +fi +ALLOW='^packaging/windows/stop-igneum\.ps1$' # the installer's own stop step: the update-now path the rule names +fail=0 +while IFS= read -r f; do [[ "$f" =~ $ALLOW ]] && continue; check_file "$f" || fail=1; done < <(git ls-files 'relay/playbooks/**' 'tools/windows/**' 'tools/proving-v1/**' 'packaging/**' | grep -E '\.(ps1|sh)$') +[ "$fail" = 0 ] && echo "playbook-quit: no playbook quits, pauses or resumes the installed app" +exit $fail