From dd954178b8ac346ec49c60b7a4215fb72e56769c Mon Sep 17 00:00:00 2001 From: igneum-labs <337424239+igneum-labs@users.noreply.github.com> Date: Thu, 8 Oct 2026 08:51:55 +0000 Subject: [PATCH] Every gate gh call reads Igneum's own gh directory, never the founder's (main's rule, 8 October 2026, 10:0x UK): tools/ci/gh-env.sh sets GH_CONFIG_DIR=~/.config/gh-igneum for the gate, the hook, the merge tool and the CI-state reader At 09:46 UK the founder's gh had his other account active (his own work) and tools/ci/gh-account-check.sh refused every Igneum push from the Mac (the v5 lane's 56a50160 held local). The check now reads Igneum's directory: empty, it refuses naming the one step (the founder or main stores the Igneum token there with `GH_CONFIG_DIR=~/.config/gh-igneum gh auth login --with-token`; no lane does); another login, refused and named; the stored entry, passes; while tools/ci/github-suspended stands it skips with a line, because no gh call can succeed and the hook already refuses GitHub pushes, so the lanes land on the mirror meanwhile. Known-failed first: an empty directory, another login, the founder's directory never read (the fake gh records the directory it was given), the marker's skip, no gh. IGNEUM_GH_CONFIG_DIR overrides the path for the self-tests. Co-Authored-By: Claude Fable 5.1 --- tools/ci/README.md | 2 +- tools/ci/ci-state.mjs | 4 +++- tools/ci/gh-account-check.sh | 34 +++++++++++++++++++++++++++------- tools/ci/gh-env.sh | 8 ++++++++ tools/ci/merge-to-master.sh | 1 + tools/ci/pre-push.sh | 3 ++- 6 files changed, 42 insertions(+), 10 deletions(-) create mode 100755 tools/ci/gh-env.sh diff --git a/tools/ci/README.md b/tools/ci/README.md index cdef5e19f..2e15aace5 100644 --- a/tools/ci/README.md +++ b/tools/ci/README.md @@ -9,7 +9,7 @@ | a box or network check gets one retry (`retry-once.sh`) | Nothing by itself: wraps the box-locks check, the scene parity check and the live public API check so a first failure is printed and retried once; the second is the verdict. The checks keep their own skip line on a runner without the resource. | 7 October 2026 | | the red watcher fires on cancelled and timed-out runs too (`ci-red.yml`, `red-watch.mjs`) | The watcher's `if` missing any of failure, cancelled, timed_out, or the conclusion not handed to the record step (the self-test reads the workflow file); the line names the kind: CI red, CI cancelled, CI timed out. | 7 October 2026 | -| gh's active account is the stored Igneum entry (`gh-account-check.sh`) | A push or a landing from this Mac while `gh auth status` names any other account as active (or none). RULE: no lane switches gh accounts on this Mac, ever; the second owner's login belongs to other projects and must never touch Igneum; the stored entry's name is in ~/.config/igneum/gh-user, never in the repository. | 7 October 2026, 21:41 UK: a lane switched gh to the other login during the suspension; nobody could say which | +| gh's active account is the stored Igneum entry (`gh-account-check.sh`, in Igneum's own gh directory `~/.config/gh-igneum` through `gh-env.sh`, never the founder's) | A push or a landing from this Mac while Igneum's gh directory names any other account as active, or none (the refusal names the one step: the founder or main stores the Igneum token there with `GH_CONFIG_DIR=~/.config/gh-igneum gh auth login --with-token`; no lane does); skipped with a line while `github-suspended` stands. RULE: no lane switches gh accounts on this Mac, ever; the second owner's login belongs to other projects and must never touch Igneum; the stored entry's name is in ~/.config/igneum/gh-user, never in the repository. | 7 October 2026, 21:41 UK: a lane switched gh to the other login during the suspension; nobody could say which | | kill by exact command or pid file (owed as a check) | 6 October 2026, 21:09Z: a Mac-side `pkill -f ` matched nothing (the log name was a redirect, not part of the command line), the roll-everything script lived on and wiped a box it had been told to hold. Rule: a job is stopped by its pid file (`tools/fleet/fleet-bg.sh start|stop `) or by a pattern anchored on its exact command line (`^python3 -u /root/fleet/in/box-prover.py`), never by a word that may or may not appear in it. The check that flags a `pkill -f`/`pgrep -f` whose literal is a path or a name that never starts a command line is owed to the CI lane | diff --git a/tools/ci/ci-state.mjs b/tools/ci/ci-state.mjs index 1e91b5811..552b69781 100755 --- a/tools/ci/ci-state.mjs +++ b/tools/ci/ci-state.mjs @@ -27,7 +27,9 @@ const FIELDS = 'databaseId,status,conclusion,headSha,url,createdAt,event'; const RED = new Set(['failure', 'cancelled', 'timed_out', 'startup_failure', 'action_required']); function gh(args) { - const env = { ...process.env, PATH: `${process.env.PATH || ''}:/opt/homebrew/bin:/usr/local/bin` }; + // Igneum's own gh configuration directory, never the founder's (tools/ci/gh-env.sh; 8 October 2026) + const ghDir = process.env.IGNEUM_GH_CONFIG_DIR || path.join(os.homedir(), '.config', 'gh-igneum'); + const env = { ...process.env, GH_CONFIG_DIR: ghDir, PATH: `${process.env.PATH || ''}:/opt/homebrew/bin:/usr/local/bin` }; const r = spawnSync('gh', args, { encoding: 'utf8', env, timeout: 60000 }); if (r.error) throw new Error(`gh: ${r.error.message}`); if (r.status !== 0) throw new Error(`gh ${args.slice(0, 2).join(' ')}: exit ${r.status}: ${(r.stderr || '').trim().slice(0, 200)}`); diff --git a/tools/ci/gh-account-check.sh b/tools/ci/gh-account-check.sh index 376d14f21..3cad8c49e 100755 --- a/tools/ci/gh-account-check.sh +++ b/tools/ci/gh-account-check.sh @@ -9,7 +9,10 @@ # tools/ci/gh-account-check.sh --self-test # a fake gh whose active account is another login is refused and named; the stored one passes; # # a status with no active account is refused; no gh on PATH skips set -uo pipefail +HERE="$(cd "$(dirname "$0")" && pwd)" +. "$HERE/gh-env.sh" # every gh call below reads Igneum's own configuration directory, never the founder's (8 October 2026, 09:46 UK: the founder's gh had his other account active and every Igneum push met this check) STORED_FILE="${IGNEUM_GH_USER_FILE:-$HOME/.config/igneum/gh-user}" +SUSPENDED_FILE="${IGNEUM_GITHUB_SUSPENDED_FILE:-$HERE/github-suspended}" active_account() { # from `gh auth status`: the account whose block carries "Active account: true" gh auth status 2>&1 | awk ' @@ -21,8 +24,11 @@ check() { command -v gh >/dev/null 2>&1 || { echo "gh-account: skipped, no gh on this machine (the rule binds the Mac that pushes)"; return 0; } [ -s "$STORED_FILE" ] || { echo "gh-account: skipped, no stored-name file at $STORED_FILE (not the pushing Mac)"; return 0; } stored="$(tr -d '[:space:]' < "$STORED_FILE")" + # while GitHub is unreachable (tools/ci/github-suspended) no gh call can succeed and no push reaches GitHub (the hook refuses them), so the + # account question is moot: skip with the line. `gh auth login --with-token` cannot fill the Igneum directory until the suspension lifts. + if [ -f "$SUSPENDED_FILE" ]; then echo "gh-account: skipped, GitHub is unreachable ($(grep -E '^suspended-since' "$SUSPENDED_FILE" | head -1)); pushes go to the box mirror; the Igneum gh directory $GH_CONFIG_DIR is filled when GitHub answers again (the one step: GH_CONFIG_DIR=$GH_CONFIG_DIR gh auth login --with-token, by the founder or main)"; return 0; fi active="$(active_account)" - if [ -z "$active" ]; then echo "gh-account: REFUSED. gh has no active account (gh auth status); the Igneum rule: the stored entry, and only it, is active on this Mac: gh auth switch --user $stored" >&2; return 1; fi + if [ -z "$active" ]; then echo "gh-account: REFUSED. Igneum's gh directory $GH_CONFIG_DIR holds no active account. The one step, for the founder or main, never a lane: GH_CONFIG_DIR=$GH_CONFIG_DIR gh auth login --with-token < . The founder's own gh directory is never read or switched by a lane." >&2; return 1; fi if [ "$active" != "$stored" ]; then echo "gh-account: REFUSED. gh's active account is $active, not the stored Igneum entry; that login must never touch Igneum (rule of 7 October 2026). Run: gh auth switch --user $stored" >&2; return 1; fi echo "gh-account: gh's active account is the stored Igneum entry" } @@ -33,20 +39,34 @@ if [ "${1:-}" = --self-test ]; then fake="$d/bin/gh"; mkdir -p "$d/bin" cat > "$fake" <<'FAKE' #!/usr/bin/env bash -# fake gh: the status text, with the active account named by $FAKE_ACTIVE (empty = none active) +# fake gh: the status text from $GH_CONFIG_DIR (a file "active" there names the active account; no file = none); the dir it read is recorded +echo "$GH_CONFIG_DIR" > "${FAKE_SEEN:-/dev/null}" +FAKE_ACTIVE="$(cat "$GH_CONFIG_DIR/active" 2>/dev/null || true)" printf 'github.com\n' printf ' X Failed to log in to github.com account stored-login (keyring)\n - Active account: %s\n - Token: gho_x\n\n' "$([ "${FAKE_ACTIVE:-}" = stored-login ] && echo true || echo false)" printf ' %s Logged in to github.com account other-login (keyring)\n - Active account: %s\n - Token: gho_y\n' "$([ "${FAKE_ACTIVE:-}" = other-login ] && echo '✓' || echo '✓')" "$([ "${FAKE_ACTIVE:-}" = other-login ] && echo true || echo false)" FAKE chmod +x "$fake"; fails=0 - out="$(PATH="$d/bin:$PATH" IGNEUM_GH_USER_FILE="$d/gh-user" FAKE_ACTIVE=other-login bash "$0" 2>&1)" && { echo "self-test failed: another active login was not refused"; fails=1; } + ig="$d/gh-igneum"; founder="$d/gh-founder"; mkdir -p "$ig" "$founder"; echo other-login > "$founder/active" # the founder's dir has his other account active + common=(IGNEUM_GH_USER_FILE="$d/gh-user" IGNEUM_GH_CONFIG_DIR="$ig" IGNEUM_GITHUB_SUSPENDED_FILE="$d/no-marker" FAKE_SEEN="$d/seen" HOME="$d") + # an empty Igneum directory: refused, the line names the one step and the directory; the founder's directory is never read + out="$(PATH="$d/bin:$PATH" env "${common[@]}" bash "$0" 2>&1)" && { echo "self-test failed: an empty Igneum gh directory was not refused"; fails=1; } + case "$out" in *"REFUSED. Igneum's gh directory $ig holds no active account"*"gh auth login --with-token"*) ;; *) echo "self-test failed: the refusal did not name the step and the directory: $out"; fails=1 ;; esac + [ "$(cat "$d/seen")" = "$ig" ] || { echo "self-test failed: gh read $(cat "$d/seen"), not the Igneum directory"; fails=1; } + # the Igneum directory holding another login: refused and named; holding the stored entry: passes + echo other-login > "$ig/active" + out="$(PATH="$d/bin:$PATH" env "${common[@]}" bash "$0" 2>&1)" && { echo "self-test failed: another active login was not refused"; fails=1; } case "$out" in *"REFUSED. gh's active account is other-login"*) ;; *) echo "self-test failed: the refusal did not name the active login: $out"; fails=1 ;; esac - PATH="$d/bin:$PATH" IGNEUM_GH_USER_FILE="$d/gh-user" FAKE_ACTIVE=stored-login bash "$0" >/dev/null 2>&1 || { echo "self-test failed: the stored active login was refused"; fails=1; } - PATH="$d/bin:$PATH" IGNEUM_GH_USER_FILE="$d/gh-user" FAKE_ACTIVE= bash "$0" >/dev/null 2>&1 && { echo "self-test failed: no active account was let through"; fails=1; } + echo stored-login > "$ig/active" + PATH="$d/bin:$PATH" env "${common[@]}" bash "$0" >/dev/null 2>&1 || { echo "self-test failed: the stored active login was refused"; fails=1; } + # the suspension marker: skipped with the line, whatever the directory holds + rm -f "$ig/active"; printf 'suspended-since: 2026-10-07T17:02:00Z\n' > "$d/marker" + out="$(PATH="$d/bin:$PATH" env "${common[@]}" IGNEUM_GITHUB_SUSPENDED_FILE="$d/marker" bash "$0" 2>&1)" || { echo "self-test failed: the check did not skip under the suspension marker"; fails=1; } + case "$out" in *"skipped, GitHub is unreachable"*) ;; *) echo "self-test failed: no skip line under the marker: $out"; fails=1 ;; esac # a machine without gh: the system binaries on PATH, no gh - out="$(PATH="/usr/bin:/bin" IGNEUM_GH_USER_FILE="$d/gh-user" bash "$0" 2>&1)" || { echo "self-test failed: a machine without gh did not skip"; fails=1; } + out="$(PATH="/usr/bin:/bin" env "${common[@]}" bash "$0" 2>&1)" || { echo "self-test failed: a machine without gh did not skip"; fails=1; } case "$out" in *"skipped, no gh"*) ;; *) echo "self-test failed: no skip line without gh: $out"; fails=1 ;; esac - [ "$fails" = 0 ] && echo "self-test passed: another active login is refused and named, the stored one passes, no active account is refused, a machine without gh skips with its line" + [ "$fails" = 0 ] && echo "self-test passed: gh reads Igneum's own directory and never the founder's; an empty Igneum directory is refused naming the one step; another active login is refused and named; the stored one passes; the suspension marker skips with its line; a machine without gh skips with its line" exit $fails fi check diff --git a/tools/ci/gh-env.sh b/tools/ci/gh-env.sh new file mode 100755 index 000000000..93e163c01 --- /dev/null +++ b/tools/ci/gh-env.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env bash +# Igneum's own gh configuration directory (main's rule, 8 October 2026, 10:0x UK): every gh call the gate, the pre-push hook, the merge +# tool and the CI-state reader make runs with GH_CONFIG_DIR set to an Igneum-only directory, so the founder's own gh (which he switches +# between his accounts as he works) is never read and never switched by a lane. The directory holds the Igneum login's token and nothing +# else; storing that token there is the one step no lane takes (the founder or main: GH_CONFIG_DIR=~/.config/gh-igneum gh auth login +# --with-token < ). Source this file; IGNEUM_GH_CONFIG_DIR overrides the path (self-tests). +export GH_CONFIG_DIR="${IGNEUM_GH_CONFIG_DIR:-$HOME/.config/gh-igneum}" +mkdir -p "$GH_CONFIG_DIR" 2>/dev/null || true diff --git a/tools/ci/merge-to-master.sh b/tools/ci/merge-to-master.sh index 3a434f2ed..2cd5412d3 100755 --- a/tools/ci/merge-to-master.sh +++ b/tools/ci/merge-to-master.sh @@ -19,6 +19,7 @@ # unless --fixes-master, none pushes the branch, pending waits then goes set -euo pipefail ROOT=$(git rev-parse --show-toplevel); cd "$ROOT" +. 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 # --remote : land on another remote's master (a box mirror, build@:/srv/igneum.git, while GitHub is unreachable; main's diff --git a/tools/ci/pre-push.sh b/tools/ci/pre-push.sh index 78fa40f2b..34a540c23 100755 --- a/tools/ci/pre-push.sh +++ b/tools/ci/pre-push.sh @@ -26,7 +26,8 @@ cd "$(git rev-parse --show-toplevel)" || exit 1 # and fire this hook again inside the fixture: the first master push through the gate died that way (6 October 2026). 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)" # the readers below are called from fixture repositories in the self-test, so by absolute path +GATE_ROOT="$(pwd -P)" +. "$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 T0=$(date +%s)