Scrub: the founder list leaves the repository in every encoding; the never-push founder check decodes base64, hex and .b64 blobs too (7 October 2026, 21:4x UK)

tools/ci/founder-strings.b64 was the founder check's pattern list base64-encoded: a grep could not read it, any reader of the public host could (git.igneum.network was public from 21:26 UK; read off at 21:36). The list now lives only in a private file (~/.config/igneum/founder-strings on the Mac, /srv/discord-hooks/founder-strings on build-1 for the Discord guard; $IGNEUM_FOUNDER_STRINGS overrides) and site/forbidden-strings.txt carries no encoded copy. Readers: founder-strings-check.sh (skips with a line where the file is absent; the Mac's hook is the guard), site/scrub.mjs and launch-gates-check.mjs (the private file's patterns added where it exists), discord-hooks.mjs (three locations; the test writes a fixture list and loads the module after it), fresh-repo.sh (the private file; drops tools/ci/founder-strings.b64 from every commit; rewrites the base64 of every list regex out of every blob and scans for it).

The check's second pass (main's addition): every base64 literal of 24 characters or more, every hex literal of 24 or more and every *.b64 file is decoded and scanned, so no encoding hides a term again; the self-test plants each fixture sample in plain text, in a .b64 file, as a base64 literal and as a hex literal, each caught and named, and a tree without the list skips with its line.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
igneum-labs 2026-10-07 20:40:36 +00:00
parent 07e21189dd
commit ff0d0dde61
7 changed files with 121 additions and 76 deletions

View file

@ -13,16 +13,9 @@ intake[_-]?key
Tailscale Tailscale
tailscale tailscale
ts\.net ts\.net
# the founder's name, logins and the earlier businesses, base64-encoded so the list is not itself a hit (a `b64:` line is decoded # the founder's name, logins and the earlier businesses are NOT in this file in any encoding (a base64 line is a disclosure to any reader, found
# and compiled case-insensitive by site/scrub.mjs and tools/ci/launch-gates-check.mjs; the same patterns live in tools/ci/founder-strings.b64) # 7 October 2026, 21:3x UK, on the public host): they live in the private list ~/.config/igneum/founder-strings, read by site/scrub.mjs,
b64:[encoded-pattern-removed] # tools/ci/launch-gates-check.mjs and tools/ci/founder-strings-check.sh where it exists; the Mac's pre-push hook is the guard on every push.
b64:[encoded-pattern-removed]
b64:[encoded-pattern-removed]
b64:[encoded-pattern-removed]
b64:[encoded-pattern-removed]
b64:[encoded-pattern-removed]
b64:[encoded-pattern-removed]
b64:[encoded-pattern-removed]
Hetzner Hetzner
igneum-seed igneum-seed
/root/ /root/

View file

@ -3,6 +3,7 @@
// so the build runs the same on this Mac, on Vercel and on the CI runner. The build fails if any pattern in // so the build runs the same on this Mac, on Vercel and on the CI runner. The build fails if any pattern in
// site/forbidden-strings.txt survives, so a private name, host or address can never reach the page. // site/forbidden-strings.txt survives, so a private name, host or address can never reach the page.
import { readFileSync } from 'node:fs'; import { readFileSync } from 'node:fs';
import { homedir } from 'node:os';
import { join, dirname } from 'node:path'; import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
const here = dirname(fileURLToPath(import.meta.url)); const here = dirname(fileURLToPath(import.meta.url));
@ -59,13 +60,21 @@ const RULES = [
[/\(local time, UTC\+1\)/g, '(UTC)'], [/\(local time, UTC\+1\)/g, '(UTC)'],
[/\bB[S]T\b/g, 'UTC'], [/\bB[S]T\b/g, 'UTC'],
]; ];
// the private founder list (~/.config/igneum/founder-strings or $IGNEUM_FOUNDER_STRINGS; perl regex, a tab, a sample per row; # comments);
// absent here: no founder patterns from this reader (the Mac's pre-push hook carries the file and is the guard)
function founderPatternsFromFile() {
try {
const f = process.env.IGNEUM_FOUNDER_STRINGS || join(homedir(), '.config', 'igneum', 'founder-strings');
return readFileSync(f, 'utf8').split('\n').map((l) => l.trim()).filter((l) => l && !l.startsWith('#')).map((l) => new RegExp(l.split('\t')[0], 'i'));
} catch { return []; }
}
export function scrubBench(text) { export function scrubBench(text) {
let out = text; let out = text;
for (const [re, rep] of RULES) out = out.replace(re, rep); for (const [re, rep] of RULES) out = out.replace(re, rep);
const pats = readFileSync(join(here, 'forbidden-strings.txt'), 'utf8').split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#')) const pats = readFileSync(join(here, 'forbidden-strings.txt'), 'utf8').split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('#'))
.map(l => (l.startsWith('b64:') ? new RegExp(Buffer.from(l.slice(4), 'base64').toString('utf8'), 'i') : new RegExp(l))); // a b64: line is an encoded, case-insensitive pattern .map(l => new RegExp(l)).concat(founderPatternsFromFile()); // plus the private founder list where it exists
const hits = []; const hits = [];
out.split('\n').forEach((line, i) => { for (const p of pats) if (p.test(line)) { hits.push(`${i + 1}: ${p.source.startsWith('(?<!') || p.flags.includes('i') ? '(an encoded founder pattern)' : p.source}`); break; } }); out.split('\n').forEach((line, i) => { for (const p of pats) if (p.test(line)) { hits.push(`${i + 1}: ${p.flags.includes('i') ? '(a founder pattern from the private list)' : p.source}`); break; } });
if (hits.length) throw new Error(`scrub: forbidden strings remain on the bench page:\n${hits.slice(0, 20).join('\n')}`); if (hits.length) throw new Error(`scrub: forbidden strings remain on the bench page:\n${hits.slice(0, 20).join('\n')}`);
return out; return out;
} }

View file

@ -1,37 +1,49 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# No founder name, personal login, earlier business or personal address in any tracked text file (the pre-public scrub, # No founder name, personal login, earlier business or personal address in any tracked text file, in plain text OR in an encoding
# 7 October 2026, main's item (4): forbidden strings over tracked files on every merge, known-failed first). The identity # (the pre-public scrub, 7 October 2026; a never-push class since 20:5x UK: every push, every branch). The identity check
# check (tools/ci/identity-check.sh) reads the public EXPORT list; this one reads EVERY tracked file, because the repository # (tools/ci/identity-check.sh) reads the public EXPORT list; this one reads EVERY tracked file, because the repository itself
# itself goes public at the testnet (docs/fud-fixes.md section 5). # is public (git.igneum.network).
# #
# The patterns are not written in this tree in plain text: a plaintext list would be the hit it looks for. They live # The patterns are NOT in the repository in any form. They live in a private file, ~/.config/igneum/founder-strings
# base64-encoded in tools/ci/founder-strings.b64 (one decoded line per pattern: perl regex, a tab, a sample the self-test plants; # ($IGNEUM_FOUNDER_STRINGS overrides; one row per pattern: perl regex, a tab, a sample the self-test plants; # comments), on the
# case-insensitive; # comments ignored) # Mac that pushes. A base64 copy in the tree (tools/ci/founder-strings.b64, 19:5x to 21:3x UK) was a disclosure to any reader of
# and are decoded into a private temporary file at run time. The login's pre-rename spelling is in the list too (main's ruling, # the public host and is gone from every commit. Where the file is absent (a hosted CI runner, a box) the check prints a skip
# 7 October 2026: the public tree names igneum-labs only); the fresh-repository step rewrites it in the history (tools/repo/fresh-repo.sh). # line and passes; the Mac's pre-push hook, which has the file, is the guard.
# #
# tools/ci/founder-strings-check.sh # exit 1 with file:line for every hit over the tracked text files # Two passes over every tracked text file: the plain text, then every encoded blob decoded and scanned (base64 literals of 24
# tools/ci/founder-strings-check.sh --self-test # a fixture tree with one hit per pattern class fails and names the file; a clean # characters or more, every *.b64 file whole, hex literals of 24 characters or more), so an encoding never hides a term again.
# # fixture passes; the encoded list decodes to at least five patterns #
# tools/ci/founder-strings-check.sh # exit 1 with file:line for every hit (decoded hits say so); exit 0 with a skip line without the list
# tools/ci/founder-strings-check.sh --self-test # with a FIXTURE list of made-up names (never the real file): a clean tree passes; each
# # sample planted in plain text, in a .b64 file, as a base64 literal and as a hex literal is
# # caught and names its file; a tree without the list skips with the line
set -euo pipefail set -euo pipefail
HERE="$(cd "$(dirname "$0")" && pwd)" HERE="$(cd "$(dirname "$0")" && pwd)"
LIST="$HERE/founder-strings.b64" LIST="${IGNEUM_FOUNDER_STRINGS:-$HOME/.config/igneum/founder-strings}"
REPO="${FOUNDER_CHECK_REPO:-$(cd "$HERE/../.." && pwd)}" REPO="${FOUNDER_CHECK_REPO:-$(cd "$HERE/../.." && pwd)}"
decode() { # [samples]: the regexes (or, with "samples", the sample per regex), one per line, into a 0600 file whose name is printed rows() { grep -vE '^\s*(#|$)' "$LIST"; } # the live rows
local f col=1; [ "${1:-}" = samples ] && col=2 scan() { # <repo> <list>: plain pass, then the decoded pass; prints "file:line:text" or "file:line:(decoded <kind>) text"; exit 1 on any hit
f="$(mktemp)"; chmod 600 "$f" local repo="$1" list="$2" pats hits
base64 -d < "$LIST" | grep -vE '^\s*(#|$)' | cut -f"$col" > "$f" pats="$(mktemp)"; chmod 600 "$pats"; grep -vE '^\s*(#|$)' "$list" | cut -f1 > "$pats"
echo "$f" hits="$(cd "$repo" && git ls-files -z -- . ':!*.png' ':!*.jpg' ':!*.jpeg' ':!*.gif' ':!*.ico' ':!*.woff' ':!*.woff2' ':!*.ttf' ':!*.pdf' ':!*.zip' ':!*.bin' \
}
scan() { # <repo>: every tracked text file against the decoded list; prints file:line:text, exit 1 on any hit (perl: the Mac's grep has no -P)
local repo="$1" pats hits
pats="$(decode)"
hits="$(cd "$repo" && git ls-files -z -- . ':!*.png' ':!*.jpg' ':!*.jpeg' ':!*.gif' ':!*.ico' ':!*.woff' ':!*.woff2' ':!*.ttf' ':!*.pdf' ':!*.zip' ':!*.bin' ':!*.b64' \
| xargs -0 perl -e ' | xargs -0 perl -e '
use MIME::Base64 qw(decode_base64);
my $pf = shift @ARGV; open(my $ph, "<", $pf) or die; my @pats = map { chomp; qr/$_/i } grep { /\S/ } <$ph>; close $ph; my $pf = shift @ARGV; open(my $ph, "<", $pf) or die; my @pats = map { chomp; qr/$_/i } grep { /\S/ } <$ph>; close $ph;
for my $f (@ARGV) { next unless -f $f && -T $f; open(my $h, "<", $f) or next; my $n = 0; sub hit { my ($t) = @_; for my $p (@pats) { return 1 if $t =~ $p } 0 }
while (my $l = <$h>) { $n++; for my $p (@pats) { if ($l =~ $p) { chomp $l; print "$f:$n:" . substr($l, 0, 160) . "\n"; last } } } close $h; } for my $f (@ARGV) {
next unless -f $f && -T $f; open(my $h, "<", $f) or next; my $n = 0; my $whole = "";
while (my $l = <$h>) {
$n++; $whole .= $l;
if (hit($l)) { chomp $l; print "$f:$n:" . substr($l, 0, 160) . "\n"; next }
# the encoded pass on this line: base64 literals and hex literals of 24 characters or more
while ($l =~ /([A-Za-z0-9+\/]{24,}={0,2})/g) { my $d = decode_base64($1); next unless length $d; if (hit($d)) { print "$f:$n:(decoded base64) " . substr($d =~ s/[^\x20-\x7e]/./gr, 0, 120) . "\n"; last } }
while ($l =~ /\b([0-9a-fA-F]{24,})\b/g) { my $x = $1; next if length($x) % 2; my $d = pack("H*", $x); if (hit($d)) { print "$f:$n:(decoded hex) " . substr($d =~ s/[^\x20-\x7e]/./gr, 0, 120) . "\n"; last } }
}
close $h;
# a .b64 file as one blob
if ($f =~ /\.b64$/) { (my $b = $whole) =~ s/\s+//g; my $d = decode_base64($b); if (length $d && hit($d)) { print "$f:1:(decoded .b64 file) " . substr($d =~ s/[^\x20-\x7e]/./gr, 0, 120) . "\n" } }
}
' "$pats" 2>/dev/null || true)" ' "$pats" 2>/dev/null || true)"
rm -f "$pats" rm -f "$pats"
if [ -n "$hits" ]; then printf '%s\n' "$hits" | sed 's/^/founder-strings: /' >&2; return 1; fi if [ -n "$hits" ]; then printf '%s\n' "$hits" | sed 's/^/founder-strings: /' >&2; return 1; fi
@ -39,28 +51,34 @@ scan() { # <repo>: every tracked text file against the decoded list; prints fi
} }
if [ "${1:-}" = "--self-test" ]; then if [ "${1:-}" = "--self-test" ]; then
n="$(base64 -d < "$LIST" | grep -vcE '^\s*(#|$)')" top="$(mktemp -d)"; trap 'rm -rf "$top"' EXIT; fx="$top/repo"; mkdir -p "$fx"
[ "$n" -ge 5 ] || { echo "self-test failed: the encoded list decodes to $n pattern(s), expected at least 5"; exit 1; } fixture="$top/list"; printf '# fixture\n\\bfoundername\\b\tfoundername\n\\bsurnamex\\b\tSurnamex\n\\bbiznamez\\b\tbiznamez\n' > "$fixture"
fx="$(mktemp -d)"; trap 'rm -rf "$fx"' EXIT ( cd "$fx" && git init -q -b master . ); mkdir -p "$fx/docs" "$fx/tools/ci" "$fx/site"
( cd "$fx" && git init -q -b master . )
mkdir -p "$fx/docs"
# a clean tree: the standing login, the project, a neutral owner word
printf 'Commit as igneum-labs. The founder decided on 5 October 2026. Igneum Labs LTD, DIFC.\n' > "$fx/docs/clean.md" printf 'Commit as igneum-labs. The founder decided on 5 October 2026. Igneum Labs LTD, DIFC.\n' > "$fx/docs/clean.md"
printf 'aGVsbG8gd29ybGQsIG5vdGhpbmcgaGVyZQ==\n' > "$fx/tools/ci/clean.b64" # "hello world, nothing here"
( cd "$fx" && git add -A && git -c user.name=t -c user.email=t@t commit -q -m c ) ( cd "$fx" && git add -A && git -c user.name=t -c user.email=t@t commit -q -m c )
FOUNDER_CHECK_REPO="$fx" bash "$0" >/dev/null 2>&1 || { echo "self-test failed: a clean tree was reported"; exit 1; } fails=0
# one hit per pattern class, each from the encoded list's own sample column, so this script never spells them; every hit scan "$fx" "$fixture" >/dev/null 2>&1 || { echo "self-test failed: a clean tree (with a harmless .b64) was reported"; fails=1; }
# must name its file i=0
samples="$(decode samples)"; i=0; fails=0 while IFS=$'\t' read -r re sample; do
while IFS= read -r word; do i=$((i + 1)); [ -n "$sample" ] || continue
i=$((i + 1)); [ -n "$word" ] || continue for kind in plain b64file base64 hex; do
printf 'a line that names %s in passing\n' "$word" > "$fx/docs/hit-$i.md" case "$kind" in
( cd "$fx" && git add -A && git -c user.name=t -c user.email=t@t commit -q -m "h$i" ) plain) f="docs/hit-$i.md"; printf 'a line that names %s in passing\n' "$sample" > "$fx/$f" ;;
if out="$(FOUNDER_CHECK_REPO="$fx" bash "$0" 2>&1)"; then echo "self-test failed: pattern $i was not caught"; fails=1 b64file) f="tools/ci/hit-$i.b64"; printf 'a line that names %s in passing\n' "$sample" | base64 > "$fx/$f" ;;
else case "$out" in *"docs/hit-$i.md"*) ;; *) echo "self-test failed: the hit for pattern $i did not name its file: $out"; fails=1 ;; esac; fi base64) f="site/hit-$i.mjs"; printf 'const X = "%s";\n' "$(printf 'a line that names %s in passing' "$sample" | base64 | tr -d '\n')" > "$fx/$f" ;;
rm -f "$fx/docs/hit-$i.md"; ( cd "$fx" && git add -A && git -c user.name=t -c user.email=t@t commit -q -m "r$i" ) hex) f="site/hit-$i-hex.mjs"; printf 'const H = "%s";\n' "$(printf 'a line that names %s in passing' "$sample" | xxd -p | tr -d '\n')" > "$fx/$f" ;;
done < "$samples"; rm -f "$samples" esac
# a binary file carrying a pattern is not read (images are not text) ( cd "$fx" && git add -A && git -c user.name=t -c user.email=t@t commit -q -m "h" )
[ "$fails" = 0 ] && echo "self-test passed: a clean tree passes; every pattern class in the encoded list is caught in a fixture file and named; the list decodes to $n patterns" if out="$(scan "$fx" "$fixture" 2>&1)"; then echo "self-test failed: pattern $i was not caught as $kind"; fails=1
else case "$out" in *"$f"*) ;; *) echo "self-test failed: the $kind hit for pattern $i did not name $f: $out"; fails=1 ;; esac; fi
rm -f "$fx/$f"; ( cd "$fx" && git add -A && git -c user.name=t -c user.email=t@t commit -q -m "r" )
done
done < <(grep -vE '^\s*(#|$)' "$fixture")
# without a list: the skip line, exit 0
out="$(IGNEUM_FOUNDER_STRINGS="$fx/no-such-list" FOUNDER_CHECK_REPO="$fx" bash "$0" 2>&1)" && case "$out" in *"skipped, no private list"*) ;; *) echo "self-test failed: no skip line without the list: $out"; fails=1 ;; esac || { echo "self-test failed: a tree without the list did not pass with a skip line"; fails=1; }
[ "$fails" = 0 ] && echo "self-test passed: a clean tree passes; every fixture pattern is caught in plain text, in a .b64 file, as a base64 literal and as a hex literal, each naming its file; without the private list the check skips with its line"
exit $fails exit $fails
fi fi
scan "$REPO" && echo "founder-strings: no founder name, personal login, earlier business or personal address in any tracked text file" if [ ! -s "$LIST" ]; then echo "founder-strings: skipped, no private list at $LIST (the Mac's pre-push hook carries the list and is the guard; a runner or box has none)"; exit 0; fi
scan "$REPO" "$LIST" && echo "founder-strings: no founder name, personal login, earlier business or personal address in any tracked text file, plain or encoded ($(rows | wc -l | tr -d ' ') patterns from the private list)"

View file

@ -12,7 +12,8 @@
// node tools/ci/launch-gates-check.mjs --self-test # a gate row without a check fails; a handoff with the founder's name fails // node tools/ci/launch-gates-check.mjs --self-test # a gate row without a check fails; a handoff with the founder's name fails
import { existsSync, readFileSync, mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'node:fs'; import { existsSync, readFileSync, mkdtempSync, writeFileSync, mkdirSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os'; import { tmpdir } from 'node:os';
import path from 'node:path'; import path, { join } from 'node:path';
import { homedir } from 'node:os';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
const HERE = path.dirname(fileURLToPath(import.meta.url)); const HERE = path.dirname(fileURLToPath(import.meta.url));
@ -21,11 +22,18 @@ const GO = 'docs/plans/testnet-go.md';
const PACK = 'docs/plans/launch-pack.md'; const PACK = 'docs/plans/launch-pack.md';
const INCOME = 'docs/analysis/income-tiers.md'; const INCOME = 'docs/analysis/income-tiers.md';
// the private founder list (~/.config/igneum/founder-strings or $IGNEUM_FOUNDER_STRINGS; perl regex, a tab, a sample per row; # comments);
// absent here: no founder patterns from this reader (the Mac's pre-push hook carries the file and is the guard)
function founderPatternsFromFile() {
try {
const f = process.env.IGNEUM_FOUNDER_STRINGS || join(homedir(), '.config', 'igneum', 'founder-strings');
return readFileSync(f, 'utf8').split('\n').map((l) => l.trim()).filter((l) => l && !l.startsWith('#')).map((l) => new RegExp(l.split('\t')[0], 'i'));
} catch { return []; }
}
function patterns(root) { function patterns(root) {
const read = f => { try { return readFileSync(path.join(root, f), 'utf8'); } catch { return ''; } }; const read = f => { try { return readFileSync(path.join(root, f), 'utf8'); } catch { return ''; } };
const lines = [...read('site/forbidden-strings.txt').split('\n'), ...read('tools/ci/forbidden-strings.txt').split('\n')]; const lines = [...read('site/forbidden-strings.txt').split('\n'), ...read('tools/ci/forbidden-strings.txt').split('\n')];
return lines.map(l => l.trim()).filter(l => l && !l.startsWith('#')) return lines.map(l => l.trim()).filter(l => l && !l.startsWith('#')).map(l => new RegExp(l)).concat(root === process.cwd() || root === ROOT ? founderPatternsFromFile() : []); // plus the private founder list for the real tree
.map(l => (l.startsWith('b64:') ? new RegExp(Buffer.from(l.slice(4), 'base64').toString('utf8'), 'i') : new RegExp(l))); // b64: = an encoded founder pattern
} }
export function gateRows(text) { export function gateRows(text) {
@ -100,7 +108,7 @@ function selfTest() {
const fx = mkdtempSync(path.join(tmpdir(), 'launch-gates-')); const fx = mkdtempSync(path.join(tmpdir(), 'launch-gates-'));
try { try {
for (const d of ['docs/plans', 'docs/analysis', 'site', 'tools/ci', 'tools/launch']) mkdirSync(path.join(fx, d), { recursive: true }); for (const d of ['docs/plans', 'docs/analysis', 'site', 'tools/ci', 'tools/launch']) mkdirSync(path.join(fx, d), { recursive: true });
writeFileSync(path.join(fx, 'site/forbidden-strings.txt'), 'b64:XGJmb3VuZGVybmFtZVxi\n'); // an encoded, case-insensitive pattern for a made-up name writeFileSync(path.join(fx, 'site/forbidden-strings.txt'), '(?i)\\bfoundername\\b\n'.replace('(?i)', '')); // a made-up name as a plain pattern (the private list is not read for a fixture root)
writeFileSync(path.join(fx, 'tools/ci/forbidden-strings.txt'), '/Users/\n'); writeFileSync(path.join(fx, 'tools/ci/forbidden-strings.txt'), '/Users/\n');
writeFileSync(path.join(fx, 'tools/launch/x.mjs'), ''); writeFileSync(path.join(fx, 'tools/launch/x.mjs'), '');
const sentences = Array.from({ length: 8 }, (_, i) => `${i + 1}. sentence (8.3 sentence ${i + 1})`).join('\n'); const sentences = Array.from({ length: 8 }, (_, i) => `${i + 1}. sentence (8.3 sentence ${i + 1})`).join('\n');
@ -117,8 +125,8 @@ function selfTest() {
r = run(fx); if (!r.problems.some(p => /does not exist/.test(p))) throw new Error('self-test: a check naming a missing script passed'); r = run(fx); if (!r.problems.some(p => /does not exist/.test(p))) throw new Error('self-test: a check naming a missing script passed');
writeFileSync(path.join(fx, GO), good); writeFileSync(path.join(fx, GO), good);
// the founder's name in the handoff, an em dash, a missing sentence // the founder's name in the handoff, an em dash, a missing sentence
writeFileSync(path.join(fx, PACK), goodPack.replace('1. sentence', '1. Foundername says')); writeFileSync(path.join(fx, PACK), goodPack.replace('1. sentence', '1. foundername says'));
r = run(fx); if (!r.problems.some(p => /forbidden pattern .*foundername/i.test(p))) throw new Error('self-test: the founder\'s name in the handoff passed (the encoded pattern did not match case-insensitively)'); r = run(fx); if (!r.problems.some(p => /forbidden pattern .*foundername/i.test(p))) throw new Error('self-test: the founder\'s name in the handoff passed');
writeFileSync(path.join(fx, PACK), goodPack.replace('2. sentence', '2. a \u2014 dash')); writeFileSync(path.join(fx, PACK), goodPack.replace('2. sentence', '2. a \u2014 dash'));
r = run(fx); if (!r.problems.some(p => /em dash/.test(p))) throw new Error('self-test: an em dash in the handoff passed'); r = run(fx); if (!r.problems.some(p => /em dash/.test(p))) throw new Error('self-test: an em dash in the handoff passed');
writeFileSync(path.join(fx, PACK), goodPack.replace('(8.3 sentence 5)', '')); writeFileSync(path.join(fx, PACK), goodPack.replace('(8.3 sentence 5)', ''));

View file

@ -49,12 +49,18 @@ const STATE_FILE_LIVE = process.env.IGNEUM_DISCORD_STATE || path.join(os.homedir
// ---------- guards ---------- // ---------- guards ----------
// Forbidden in any post: the founder's name and logins, rented-box and build hosts, machine ids, internal paths, IP addresses, // Forbidden in any post: the founder's name and logins, rented-box and build hosts, machine ids, internal paths, IP addresses,
// a standalone 32-hex token (a sha256 is 64 hex and passes), any dl.igneum.network path outside /public/, any mention. // a standalone 32-hex token (a sha256 is 64 hex and passes), any dl.igneum.network path outside /public/, any mention.
// The founder's name, logins and the earlier businesses come from tools/ci/founder-strings.b64 (base64, so no tracked file // The founder's name, logins and the earlier businesses come from the PRIVATE list (never a tracked file in any encoding: a base64
// spells them; the first three decoded patterns are the founder, the rest the earlier businesses). fs is read once at load. // copy in the tree was a disclosure on the public host, 7 October 2026, 21:3x UK): $IGNEUM_FOUNDER_STRINGS, else
const FOUNDER_LIST = path.join(HERE, '..', 'ci', 'founder-strings.b64'); // ~/.config/igneum/founder-strings (the Mac), else /srv/discord-hooks/founder-strings (build-1, beside the webhook env). One row per
export function founderPatterns(file = FOUNDER_LIST) { // pattern: perl regex, a tab, a sample. Absent everywhere: no founder rows (the host that posts carries the file).
const rows = Buffer.from(fs.readFileSync(file, 'utf8'), 'base64').toString('utf8').split('\n').filter((l) => l.trim() && !l.startsWith('#')).map((l) => l.split('\t')); export function founderListPath(env = process.env) {
return rows.map(([re, sample], i) => ({ re: new RegExp(re, 'i'), sample, why: i < 3 ? "the founder's name or login" : i < 8 ? 'an earlier business' : "the founder's address" })); for (const f of [env.IGNEUM_FOUNDER_STRINGS, path.join(os.homedir(), '.config', 'igneum', 'founder-strings'), '/srv/discord-hooks/founder-strings']) if (f && fs.existsSync(f)) return f;
return '';
}
export function founderPatterns(file = founderListPath()) {
if (!file) return [];
const rows = fs.readFileSync(file, 'utf8').split('\n').filter((l) => l.trim() && !l.startsWith('#')).map((l) => l.split('\t'));
return rows.map(([re, sample], i) => ({ re: new RegExp(re, 'i'), sample, why: i < 3 ? "the founder's name or login" : i < 8 ? 'an earlier business' : i === 8 ? "the founder's address" : 'the login before its rename' }));
} }
export const FORBIDDEN = [ export const FORBIDDEN = [
...founderPatterns().map(({ re, why }) => ({ re, why })), ...founderPatterns().map(({ re, why }) => ({ re, why })),

View file

@ -7,11 +7,14 @@ import fs from 'node:fs';
import os from 'node:os'; import os from 'node:os';
import path from 'node:path'; import path from 'node:path';
import { fileURLToPath } from 'node:url'; import { fileURLToPath } from 'node:url';
import { // the founder guard reads a private list; the test writes a FIXTURE list of made-up names and points the module at it before loading
import { mkdtempSync as _mkd, writeFileSync as _wf } from 'node:fs'; import { tmpdir as _tmp } from 'node:os'; import { join as _join } from 'node:path';
{ const d = _mkd(_join(_tmp(), 'founder-fixture-')); _wf(_join(d, 'list'), '\\bfoundername\\b\tFoundername\n\\bsurnamex\\b\tSurnamex\n\\bloginx\\b\tloginx\n\\bbiz1\\b\tbiz1\n\\bbiz2\\b\tbiz2\n\\bbiz3\\b\tbiz3\n\\bbiz4\\b\tbiz4\n\\bbiz5\\b\tbiz5\n\\bmail@x\\.y\\b\tmail@x.y\n\\boldlogin\\b\toldlogin\n'); process.env.IGNEUM_FOUNDER_STRINGS = _join(d, 'list'); }
const {
shapePulse, shapeDigest, shapeWeekly, shapeRelease, shapeIncidentOpen, shapeIncidentResolve, changedLinesFromPlan, plainLine, shapePulse, shapeDigest, shapeWeekly, shapeRelease, shapeIncidentOpen, shapeIncidentResolve, changedLinesFromPlan, plainLine,
guardText, guardPayload, embed, embedLength, LIMITS, snapshot, devnet2Line, versionShare, watchPass, WATCH, WATCH_CONDITIONS, guardText, guardPayload, embed, embedLength, LIMITS, snapshot, devnet2Line, versionShare, watchPass, WATCH, WATCH_CONDITIONS,
Poster, postWebhook, dueNow, ukStamp, londonParts, fmtHash, fmtDur, fmtDelta, fmtChangePct, renderPreview, ICON, EMBER, Poster, postWebhook, dueNow, ukStamp, londonParts, fmtHash, fmtDur, fmtDelta, fmtChangePct, renderPreview, ICON, EMBER,
shapeFeed, feedMilestones, shapeMilestone, hashOriginLine, runFeed, FEED_MILESTONES, founderPatterns } from './discord-hooks.mjs'; shapeFeed, feedMilestones, shapeMilestone, hashOriginLine, runFeed, FEED_MILESTONES, founderPatterns } = await import('./discord-hooks.mjs');
const HERE = path.dirname(fileURLToPath(import.meta.url)); const HERE = path.dirname(fileURLToPath(import.meta.url));
const fx = name => JSON.parse(fs.readFileSync(path.join(HERE, 'fixtures', 'discord', `${name}.json`), 'utf8')); const fx = name => JSON.parse(fs.readFileSync(path.join(HERE, 'fixtures', 'discord', `${name}.json`), 'utf8'));

View file

@ -27,7 +27,9 @@ set -euo pipefail
export TZ=UTC export TZ=UTC
HERE="$(cd "$(dirname "$0")" && pwd)" HERE="$(cd "$(dirname "$0")" && pwd)"
ROOT="$(cd "$HERE/../.." && pwd)" ROOT="$(cd "$HERE/../.." && pwd)"
STANDING_LOGIN="${IGNEUM_STANDING_LOGIN:-$(base64 -d < "$(cd "$(dirname "$0")/../.." && pwd)/tools/ci/founder-strings.b64" | grep -vE '^#' | sed -n '10p' | cut -f2)}" # the login's pre-rename spelling, from the encoded list (no tracked file spells it) FOUNDER_LIST="${IGNEUM_FOUNDER_STRINGS:-$HOME/.config/igneum/founder-strings}" # the PRIVATE list (perl regex, tab, sample per row); never a tracked file in any encoding
[ -s "$FOUNDER_LIST" ] || { echo "fresh-repo: no private founder list at $FOUNDER_LIST (the Mac holds it; the rewrite needs its rows)" >&2; exit 1; }
STANDING_LOGIN="${IGNEUM_STANDING_LOGIN:-$(grep -vE '^#' "$FOUNDER_LIST" | sed -n '10p' | cut -f2)}" # row 10: the login's pre-rename spelling
ORG="igneum-network" ORG="igneum-network"
SOURCE="" WORK="" NEW_REPO="$ORG/igneum-core" NEW_LOGIN="" PUBLIC_CLAUDE="" CLEAN=0 SOURCE="" WORK="" NEW_REPO="$ORG/igneum-core" NEW_LOGIN="" PUBLIC_CLAUDE="" CLEAN=0
while [ $# -gt 0 ]; do while [ $# -gt 0 ]; do
@ -90,14 +92,14 @@ PERSONAL_EMAILS="$(printf '%s\n' "$PERSONAL_PAIRS" | awk -F'|' 'NF==2{print $2}'
PERSONAL_NAMES="$(printf '%s\n' "$PERSONAL_PAIRS" | awk -F'|' 'NF==2 && $1 ~ / /{print $1}' | grep -v -E "^($STANDING_LOGIN|$TARGET_LOGIN)$" | sort -u || true)" # a name has a space; a login is not a name PERSONAL_NAMES="$(printf '%s\n' "$PERSONAL_PAIRS" | awk -F'|' 'NF==2 && $1 ~ / /{print $1}' | grep -v -E "^($STANDING_LOGIN|$TARGET_LOGIN)$" | sort -u || true)" # a name has a space; a login is not a name
# the founder's first name, surname and second login from the encoded list (rows 1 to 3, sample column), so the rules never depend on # the founder's first name, surname and second login from the encoded list (rows 1 to 3, sample column), so the rules never depend on
# which commits a given mirror carries (7 Oct 2026: build-1's mirror holds none of the 40 personal-identity commits) # which commits a given mirror carries (7 Oct 2026: build-1's mirror holds none of the 40 personal-identity commits)
LIST_ROWS="$(base64 -d < "$ROOT/tools/ci/founder-strings.b64" | grep -vE '^#')" LIST_ROWS="$(grep -vE '^#' "$FOUNDER_LIST")"
LIST_FIRST="$(printf '%s\n' "$LIST_ROWS" | sed -n '1p' | cut -f2 | awk '{print $1}')"; LIST_LAST="$(printf '%s\n' "$LIST_ROWS" | sed -n '2p' | cut -f2)"; LIST_SECOND="$(printf '%s\n' "$LIST_ROWS" | sed -n '3p' | cut -f2)" LIST_FIRST="$(printf '%s\n' "$LIST_ROWS" | sed -n '1p' | cut -f2 | awk '{print $1}')"; LIST_LAST="$(printf '%s\n' "$LIST_ROWS" | sed -n '2p' | cut -f2)"; LIST_SECOND="$(printf '%s\n' "$LIST_ROWS" | sed -n '3p' | cut -f2)"
FIRST_NAMES="$( { printf '%s\n' "$PERSONAL_NAMES" | awk 'NF>=2{print $1}'; printf '%s\n' "$LIST_FIRST"; } | grep . | sort -u)" FIRST_NAMES="$( { printf '%s\n' "$PERSONAL_NAMES" | awk 'NF>=2{print $1}'; printf '%s\n' "$LIST_FIRST"; } | grep . | sort -u)"
LAST_NAMES="$( { printf '%s\n' "$PERSONAL_NAMES" | awk 'NF>=2{print $NF}'; printf '%s\n' "$LIST_LAST"; } | grep . | sort -u)" LAST_NAMES="$( { printf '%s\n' "$PERSONAL_NAMES" | awk 'NF>=2{print $NF}'; printf '%s\n' "$LIST_LAST"; } | grep . | sort -u)"
SECOND_LOGINS="$( { printf '%s\n' "$PERSONAL_EMAILS" | sed -nE 's/^[0-9]+\+([A-Za-z0-9-]+)@users\.noreply\.github\.com$/\1/p'; printf '%s\n' "$LIST_SECOND"; } | grep . | grep -v -E "^($STANDING_LOGIN|$TARGET_LOGIN)$" | sort -u || true)" SECOND_LOGINS="$( { printf '%s\n' "$PERSONAL_EMAILS" | sed -nE 's/^[0-9]+\+([A-Za-z0-9-]+)@users\.noreply\.github\.com$/\1/p'; printf '%s\n' "$LIST_SECOND"; } | grep . | grep -v -E "^($STANDING_LOGIN|$TARGET_LOGIN)$" | sort -u || true)"
# the other businesses named in the plan (brand names, not people), from the encoded list tools/ci/founder-strings.b64 (rows 4 to 8; # the other businesses named in the plan (brand names, not people), from the encoded list tools/ci/founder-strings.b64 (rows 4 to 8;
# no tracked file spells them: the founder-strings check reads every tracked file) # no tracked file spells them: the founder-strings check reads every tracked file)
OTHER_BUSINESSES="$(base64 -d < "$ROOT/tools/ci/founder-strings.b64" | grep -vE '^#' | sed -n '4,8p' | cut -f1 | sed -E 's/\\b//g' | paste -sd'|' -)" OTHER_BUSINESSES="$(grep -vE '^#' "$FOUNDER_LIST" | sed -n '4,8p' | cut -f1 | sed -E 's/\\b//g' | paste -sd'|' -)"
[ -n "$OTHER_BUSINESSES" ] || { echo "fresh-repo: the encoded founder list decoded to no business names" >&2; exit 1; } [ -n "$OTHER_BUSINESSES" ] || { echo "fresh-repo: the encoded founder list decoded to no business names" >&2; exit 1; }
# the secrets: whichever of the four files exist, plus every dated copy the rotation left behind # the secrets: whichever of the four files exist, plus every dated copy the rotation left behind
# (log-intake-key.old-<date>, dl-token.old-<date>: rotation phase 2 section 5 step 2 renames the .next files to the # (log-intake-key.old-<date>, dl-token.old-<date>: rotation phase 2 section 5 step 2 renames the .next files to the
@ -157,6 +159,12 @@ while IFS= read -r login; do
printf '\\b%s\\b\n' "$login" >> "$IDENT" printf '\\b%s\\b\n' "$login" >> "$IDENT"
done <<< "$SECOND_LOGINS" done <<< "$SECOND_LOGINS"
printf 'regex:(?i)\\b(%s)\\b==>[other-business]\n' "$OTHER_BUSINESSES" >> "$REPLACE" printf 'regex:(?i)\\b(%s)\\b==>[other-business]\n' "$OTHER_BUSINESSES" >> "$REPLACE"
# the encoded forms: the base64 of every list regex (site/forbidden-strings.txt carried them as b64: lines until 21:3x UK on 7 October 2026)
while IFS= read -r re; do
[ -n "$re" ] || continue; b="$(printf '%s' "$re" | base64 | tr -d '\n')"
printf 'literal:%s==>[encoded-pattern-removed]\n' "$b" >> "$REPLACE"
printf '%s\n' "$b" | sed 's/[][\\.*^$?+(){}|/]/\\&/g' >> "$IDENT"
done < <(printf '%s\n' "$LIST_ROWS" | cut -f1)
printf '\\b(%s)\\b\n' "$OTHER_BUSINESSES" >> "$IDENT" printf '\\b(%s)\\b\n' "$OTHER_BUSINESSES" >> "$IDENT"
say "rules: $(grep -c . "$REPLACE") replace lines, $(grep -c . "$MAILMAP") mailmap lines, $(grep -c . "$IDENT") identity patterns, $(grep -c . "$SECRETS") secret patterns (files under $RULES, removed at exit)" say "rules: $(grep -c . "$REPLACE") replace lines, $(grep -c . "$MAILMAP") mailmap lines, $(grep -c . "$IDENT") identity patterns, $(grep -c . "$SECRETS") secret patterns (files under $RULES, removed at exit)"
@ -169,7 +177,7 @@ scan_blobs() {
| perl -ne 'BEGIN { open(P, "<", shift) or die; @p = map { chomp; qr/$_/i } grep { /\S/ } <P>; $n = 0 } for my $p (@p) { if ($_ =~ $p) { $n++; last } } END { print "$n\n" }' "$1" | perl -ne 'BEGIN { open(P, "<", shift) or die; @p = map { chomp; qr/$_/i } grep { /\S/ } <P>; $n = 0 } for my $p (@p) { if ($_ =~ $p) { $n++; last } } END { print "$n\n" }' "$1"
} }
scan_meta() { git log --all --format='%an%n%ae%n%cn%n%ce%n%s%n%b' | perl -ne 'BEGIN { open(P, "<", shift) or die; @p = map { chomp; qr/$_/i } grep { /\S/ } <P>; $n = 0 } for my $p (@p) { if ($_ =~ $p) { $n++; last } } END { print "$n\n" }' "$1"; } scan_meta() { git log --all --format='%an%n%ae%n%cn%n%ce%n%s%n%b' | perl -ne 'BEGIN { open(P, "<", shift) or die; @p = map { chomp; qr/$_/i } grep { /\S/ } <P>; $n = 0 } for my $p (@p) { if ($_ =~ $p) { $n++; last } } END { print "$n\n" }' "$1"; }
DROPPED=(docs/review) # the ledger, its fixes file and the ledger page are published with the repository (decision of 5 October 2026, docs/fud-fixes.md section 5); the review folder stays internal DROPPED=(docs/review tools/ci/founder-strings.b64) # the encoded founder list (19:5x to 21:3x UK, 7 October 2026) leaves every commit # the ledger, its fixes file and the ledger page are published with the repository (decision of 5 October 2026, docs/fud-fixes.md section 5); the review folder stays internal
counts() { # <label> counts() { # <label>
local label="$1" local label="$1"
say "" say ""