Kill-proof gates (main's order, 8 October 2026, 12:5x UK): a unique process title and a pid file per gate and merge run; kills by pid file only, enforced by the kill-by-name check

Twice today a merge gate died to signal 15 from another lane's kill pattern (12:4x UK, and the three-node script's self-kill at 10:05 was the same class). The sender cannot be read after the fact (no process accounting on the Mac, nothing in the unified log, a trap sees no sender), so the class is closed by rule: tools/ci/pre-push.sh re-execs under the title igneum-gate:<pid>-<start> and writes "<pid> <start> <mode> <title>" to .git/igneum-gate.pid under the worktree (removed at exit); tools/ci/merge-to-master.sh the same with igneum-merge:<pid>-<start> and .git/igneum-merge.pid; a gate or a merge run is stopped by `kill "$(cut -d' ' -f1 <that file>)"` and never by a name. tools/ci/kill-by-name-check.sh rule 4 (in the gate on every push): killall with any pattern, and pkill/pgrep (with or without -f or -x, bracketed or not) on a generic tool or a gate's name (bash, sh, zsh, node, cargo, python, ssh, git, perl, gate, igneum, pre-push, merge-to-master, build-remote, remote-run, igneum-gate, igneum-merge) are red; pkill -F <pidfile>, pkill -P <pid> and a daemon's own binary under -x stay allowed. Known-failed first: eight new banned shapes and four allowed ones in the self-test; the gate's self-test holds a gate open and reads its title and pid file, then sees the file gone.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-08 11:39:01 +00:00
parent af56609574
commit 2dad9ee52d
3 changed files with 57 additions and 1 deletions

View file

@ -14,6 +14,11 @@
# .json, .jsonl, .out, .err, .pid, .csv, .md, .toml, .yml) is flagged: a file name is a redirect or an argument, and a
# redirect is never on a command line.
# 3. `ps ... | grep <literal>` without the bracket form is flagged (the same self-match).
# 4. (8 October 2026, 12:4x UK: twice in one day a merge gate died to signal 15 from another lane's kill pattern) `killall` with any
# pattern, and `pkill`/`pgrep` (with or without -f, -x) whose pattern is a generic tool or a gate's name (bash, sh, node, cargo,
# python, ssh, git, pre-push, merge-to-master, build-remote, remote-run, gate, igneum-gate, igneum-merge) are flagged: such a
# pattern can match a gate or a merge run of another lane. A gate is killed by its pid file only
# (.git/igneum-gate.pid, .git/igneum-merge.pid under the worktree: `kill "$(cut -d' ' -f1 <pidfile>)"`).
#
# tools/ci/kill-by-name-check.sh # exit 1 with file:line and the reason
# tools/ci/kill-by-name-check.sh --self-test # fires on each banned shape, passes each allowed one
@ -24,6 +29,20 @@ check_line() { # <line> -> prints the reason, returns 1, when the line carrie
code="${line%%#*}" # a comment is not code (a line that starts with // or * is a comment too)
[[ "$code" =~ ^[[:space:]]*(//|\*|/\*) ]] && return 0
[[ "$line" =~ ^[[:space:]]*(//|\*|/\*) ]] && return 0
# rule 4: killall, and any pkill/pgrep whose pattern could match a gate or a merge run of another lane (kills are by pid file only)
if [[ "$code" =~ (^|[^A-Za-z0-9_./-])killall[[:space:]]+(-[A-Za-z0-9]+[[:space:]]+)*(\"[^\"]*\"|\'[^\']*\'|[^[:space:]|;\)]+) ]]; then
echo "killall ${BASH_REMATCH[3]}: killall matches by name across every lane's processes; a gate or a merge run dies with it; kill by pid file only"; return 1
fi
local gp="$code"
while [[ "$gp" =~ (^|[^A-Za-z0-9_./-])(pgrep|pkill)([[:space:]]+-[A-Za-z0-9]+)*[[:space:]]+(\"[^\"]*\"|\'[^\']*\'|[^[:space:]|;\)-][^[:space:]|;\)]*) ]]; do
local gpat="${BASH_REMATCH[4]}"; gpat="${gpat#\"}"; gpat="${gpat%\"}"; gpat="${gpat#\'}"; gpat="${gpat%\'}"
local core="${gpat#^}"; core="$(printf '%s' "$core" | sed -E 's/^\[([A-Za-z])\]/\1/')" # the bracket form [p]re-push reads as pre-push
case "$core" in
bash|sh|zsh|node|cargo|python|python3|ssh|git|perl|gate|igneum|*pre-push*|*merge-to-master*|*build-remote*|*remote-run*|*igneum-gate*|*igneum-merge*)
echo "pkill/pgrep on a pattern that can match a gate or a merge run of another lane ($gpat): kill by pid file only (.git/igneum-gate.pid, .git/igneum-merge.pid)"; return 1 ;;
esac
gp="${gp#*${BASH_REMATCH[2]}}"
done
# every pgrep/pkill -f on the line, not only the first (`pkill -f "[i]gneum-prove-host"; pkill -f prove-shard.sh` hid its second)
local rest="$code" m
while [[ "$rest" =~ (^|[^A-Za-z0-9_./-])(pgrep|pkill)([[:space:]]+-[A-Za-z0-9]+)*[[:space:]]+-[A-Za-z]*f[A-Za-z]*[[:space:]]+(\"[^\"]*\"|\'[^\']*\'|[^[:space:]|;\)]+) ]]; do
@ -52,6 +71,14 @@ check_line() { # <line> -> prints the reason, returns 1, when the line carrie
if [ "${1:-}" = "--self-test" ]; then
fails=0
bad=(
'killall node'
'killall -9 igneum-miner'
'pkill -f bash'
'pkill -x node'
'pkill -f "[p]re-push.sh"'
'pgrep -f merge-to-master'
'pkill -f "^igneum-gate:"'
'pkill cargo'
'pkill -f igneum-roll.log'
'pkill -f "fleet-wave-3.log" || true'
'pgrep -f igneumd >/dev/null && exit 0'
@ -65,6 +92,10 @@ if [ "${1:-}" = "--self-test" ]; then
"pgrep -fl 'igneumd --' | grep -v Wallet"
)
good=(
'kill "$(cut -d" " -f1 .git/igneum-gate.pid)"'
'pkill -F /srv/x/run.pid'
'pkill -P "$keeper"'
'pkill -x igneumd'
'pgrep -f "[i]gneumd" >/dev/null'
"pkill -f '[n]ode tools/fleet/wave.mjs'"
'pgrep -x igneumd'

View file

@ -19,6 +19,12 @@
# unless --fixes-master, none pushes the branch, pending waits then goes
set -euo pipefail
ROOT=$(git rev-parse --show-toplevel); cd "$ROOT"
# kill-proof by rule (8 October 2026): a unique title per run and a pid file under the worktree's git directory, removed at exit; a merge
# run is stopped by `kill "$(cut -d' ' -f1 .git/igneum-merge.pid)"` and never by a name pattern (tools/ci/kill-by-name-check.sh)
if [ -z "${IGNEUM_MERGE_TITLE:-}" ] && [ "${IGNEUM_GATE_NO_TITLE:-0}" != 1 ]; then export IGNEUM_MERGE_TITLE="igneum-merge:$$-$(date +%s)"; exec -a "$IGNEUM_MERGE_TITLE" bash "$0" "$@"; fi
MERGE_PID_FILE="$(git rev-parse --git-dir)/igneum-merge.pid"
printf '%s %s %s\n' "$$" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "${IGNEUM_MERGE_TITLE:-untitled}" > "$MERGE_PID_FILE" 2>/dev/null || true
trap 'rm -f "$MERGE_PID_FILE"' EXIT
. tools/ci/gh-env.sh # every gh call here reads Igneum's own gh directory, never the founder's (8 October 2026)
BRANCH="$(git rev-parse --abbrev-ref HEAD)"; TRIES=6; CI_WAIT_MIN="${CI_WAIT_MIN:-25}"; FIXES_MASTER=0; SELF_TEST=0; REMOTE="${MERGE_REMOTE:-origin}"
while [ $# -gt 0 ]; do case "$1" in --tries) TRIES="$2"; shift 2 ;; --ci-wait) CI_WAIT_MIN="$2"; shift 2 ;; --fixes-master) FIXES_MASTER=1; shift ;; --remote) REMOTE="$2"; shift 2 ;; --self-test) SELF_TEST=1; shift ;; -*) echo "unknown option $1" >&2; exit 2 ;; *) BRANCH="$1"; shift ;; esac; done

View file

@ -27,8 +27,19 @@ cd "$(git rev-parse --show-toplevel)" || exit 1
unset GIT_DIR GIT_WORK_TREE GIT_INDEX_FILE GIT_PREFIX GIT_COMMON_DIR GIT_OBJECT_DIRECTORY GIT_ALTERNATE_OBJECT_DIRECTORIES GIT_QUARANTINE_PATH GIT_PUSH_OPTION_COUNT
MODE="${1:-local}"; MODE="${MODE#--}"
GATE_ROOT="$(pwd -P)"
# Kill-proof by rule (8 October 2026, 12:4x UK: twice in a day a gate died to signal 15 from another lane's kill pattern): every gate run
# carries a unique process title (igneum-gate:<pid>-<start>, set by re-exec through `exec -a`) and writes its pid, start and mode to
# .git/igneum-gate.pid under the worktree (removed at exit); a gate is stopped by that file only (`kill "$(cut -d' ' -f1 <file>)"`), and
# tools/ci/kill-by-name-check.sh refuses any pkill/pgrep/killall pattern that could match a gate. IGNEUM_GATE_NO_TITLE=1 skips the re-exec.
if [ -z "${IGNEUM_GATE_TITLE:-}" ] && [ "${IGNEUM_GATE_NO_TITLE:-0}" != 1 ] && [ "$MODE" != self-test ]; then
export IGNEUM_GATE_TITLE="igneum-gate:$$-$(date +%s)"
exec -a "$IGNEUM_GATE_TITLE" bash "$0" "$@"
fi
GATE_PID_FILE="$(git rev-parse --git-dir 2>/dev/null || echo .git)/igneum-gate.pid"
printf '%s %s %s %s\n' "$$" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" "$MODE" "${IGNEUM_GATE_TITLE:-untitled}" > "$GATE_PID_FILE" 2>/dev/null || true
[ "${IGNEUM_GATE_HOLD:-0}" = 1 ] && sleep 4 # the self-test's window to read the file and the title
. "$GATE_ROOT/tools/ci/gh-env.sh" # every gh call under the gate reads Igneum's own gh directory, never the founder's (8 October 2026) # the readers below are called from fixture repositories in the self-test, so by absolute path
RED=0; N=0; LOG="$(mktemp)"; trap 'rm -rf "$LOG" "${SITE_TMP:-}"' EXIT
RED=0; N=0; LOG="$(mktemp)"; trap 'rm -rf "$LOG" "${SITE_TMP:-}"; rm -f "$GATE_PID_FILE"' EXIT
T0=$(date +%s)
run() {
@ -271,6 +282,14 @@ case "$MODE" in
MODE=ci GITHUB_ACTIONS= site_in_place && { echo "self-test failed: --ci outside GitHub Actions chose the in-place build"; fails=1; }
MODE=ci GITHUB_ACTIONS=true site_in_place || { echo "self-test failed: --ci inside GitHub Actions did not choose the in-place build"; fails=1; }
declare -f tree_checks | grep -q 'never_push_checks' || { echo "self-test failed: the full gate does not run the never-push checks"; fails=1; }
# kill-proof: a running gate writes its pid, start and mode to .git/igneum-gate.pid and removes it at exit; the title re-exec is in the script
pf="$(mktemp -d)"; ( cd "$pf" && git init -q -b master . )
# a gate that is held open (IGNEUM_GATE_HOLD=1 sleeps 3 s after writing the file) names its own pid and mode in the file; the file is gone after it
( cd "$pf"; IGNEUM_GATE_HOLD=1 bash "$GATE_ROOT/tools/ci/pre-push.sh" --list >/dev/null 2>&1 & p=$!; sleep 2; f="$pf/.git/igneum-gate.pid"
if [ ! -f "$f" ]; then echo "self-test failed: no pid file at $f while the gate runs"; else read -r pid start mode title < "$f"; [ "$pid" = "$p" ] || echo "self-test failed: the pid file names $pid, the gate is $p"; [ "$mode" = list ] || echo "self-test failed: the pid file's mode is $mode"; case "$title" in igneum-gate:*) ;; *) echo "self-test failed: the pid file carries no title: $title" ;; esac; [ "$(ps -o command= -p "$p" | cut -d' ' -f1)" = "$title" ] || echo "self-test failed: the process title is $(ps -o command= -p "$p" | cut -d' ' -f1), the file says $title"; fi
wait $p 2>/dev/null; [ -f "$f" ] && echo "self-test failed: the pid file outlived the gate"; true ) | grep 'self-test failed' && fails=1
rm -rf "$pf"
grep -q 'exec -a "\$IGNEUM_GATE_TITLE" bash "\$0" "\$@"' "$0" || { echo "self-test failed: the gate does not re-exec under its unique title"; fails=1; }
# the overlap sweep's wall clock runs the command with GNU timeout where it exists and plainly where it does not (bash 3.2 under set -u included)
[ "$(wall_clock 5 /bin/echo clocked 2>&1)" = clocked ] || { echo "self-test failed: wall_clock did not run its command"; fails=1; }
[ "$(PATH=/nonexistent wall_clock 5 /bin/echo plain 2>&1)" = plain ] || { echo "self-test failed: wall_clock without a timeout binary did not run its command plainly"; fails=1; }