fix: refuse non-regular nodes everywhere; machine-verify the mirror README

Round 1 (PR #43): codex-bot + kimi-bot reproduced cp writing through a
committed symlink and cmp/find resolving through or skipping link nodes —
both modes now refuse any non-regular node (.ceremony itself, anything
under it, the root AGENTS.md incl. dangling links and non-file nodes)
before touching the tree. kimi-bot also found .ceremony/README.md was
machine-written but never machine-verified: --check now cmps it against
the generated text (missing or drifted -> red). 22 regression rows; every
symlink row asserts the victim untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
claude-bot-andresmgsl 2026-07-22 21:59:37 +00:00
parent 77f195a6ae
commit 85d4eb3770
2 changed files with 162 additions and 1 deletions

View file

@ -39,11 +39,24 @@ set -euo pipefail
# vendored copy — mirror means mirror, or "extra" files accumulate as
# unverified doctrine).
#
# AND THE MIRROR IS PLAIN FILES. A symlink committed anywhere this tool
# touches — a vendored path, a subdirectory, `.ceremony/` itself, the root
# AGENTS.md — redirects the tool outside the mirror: cp writes THROUGH the
# link (anywhere the CI token can reach), cmp reads through it and reports
# the target's bytes as the mirror's, and a `find -type f` scan skips link
# nodes entirely, so the stray poses as doctrine while staying invisible
# (PR #43's review round, both findings reproduced). So both modes refuse
# any non-regular node before touching anything: the fix for a symlink is
# a human deleting it, never a tool following it.
#
# TWO FILES ARE SPECIAL, both deliberately:
# * `.ceremony/README.md` is GENERATED here — the machine-managed marker
# plus where the pin lives — instead of per-file banners, so every
# vendored file stays byte-identical to its source and the check is a
# plain cmp, never a strip-the-banner parse.
# plain cmp, never a strip-the-banner parse. It is verified like
# everything else (against the generated text, not the source tree):
# the file that says "a hand edit goes red" must itself go red when
# hand-edited, or the marker is the one unverified spot in the mirror.
# * the consumer's ROOT AGENTS.md is scaffolded once by --fix and never
# overwritten. Agent harnesses auto-load root AGENTS.md (the cross-agent
# convention), so the stub is what makes "you are a reviewer here" a
@ -221,6 +234,44 @@ a drill means here, code conventions) live in CONTRIBUTING.md.
EOF
}
# --- the mirror is plain files (see the header; PR #43's review round) ----------
# Refusals, not repairs, in BOTH modes — deliberately unlike drift, where
# --fix is the advertised cure: repairing a symlink means either deleting a
# node that points somewhere or writing through it, and a tool must do
# neither on its own. -L before -d/-f everywhere: the test that follows the
# link is exactly the bug.
guard_plain_tree() {
local offenders
if [ -L "$MIRROR" ]; then
die "docs-sync: $MIRROR is a symlink, not a directory — a linked mirror" \
" redirects every write outside the tree this tool is allowed to" \
" touch. Refusing both modes: delete the symlink, then re-run" \
" docs-sync --fix."
fi
if [ -d "$MIRROR" ]; then
offenders="$(find "$MIRROR" -mindepth 1 ! -type f ! -type d | LC_ALL=C sort)"
[ -z "$offenders" ] || die \
"docs-sync: non-regular node(s) in the mirror — a symlink (or fifo," \
" socket, …) under $MIRROR/ makes cp write and cmp read outside the" \
" mirror, and hides from the file scan. Refusing both modes; delete" \
" these by hand, then re-run docs-sync --fix:" \
"$offenders"
fi
if [ -L AGENTS.md ]; then
die "docs-sync: the root AGENTS.md is a symlink — the scaffold and the" \
" existence check must never resolve through a link (a dangling one" \
" would even make --fix write through it). Refusing both modes:" \
" replace the symlink with a regular file (or delete it and let" \
" docs-sync --fix scaffold the stub)."
fi
if [ -e AGENTS.md ] && [ ! -f AGENTS.md ]; then
die "docs-sync: the root AGENTS.md exists but is not a regular file —" \
" nothing this tool could do to it is right. Refusing both modes:" \
" remove it, then re-run docs-sync --fix to scaffold the stub."
fi
}
# --- check ----------------------------------------------------------------------
run_check() {
@ -253,6 +304,18 @@ run_check() {
" $MIRROR/ must be vendored and verified, or it poses as doctrine" \
" without being checked. Fix: run docs-sync --fix (it deletes orphans)."
done < <(mirror_files)
# The README is machine-written against generated text, so it is
# machine-verified against the same text — the marker that warns "a hand
# edit goes red" is not itself an unverified hole (kimi-bot, PR #43).
if [ ! -f "$MIRROR/$README_NAME" ]; then
complain "docs-sync: $MIRROR/$README_NAME is missing — the machine-managed" \
" marker is part of the mirror. Fix: run docs-sync --fix."
elif ! readme_content | cmp -s - "$MIRROR/$README_NAME"; then
complain "docs-sync: $MIRROR/$README_NAME has drifted from its generated" \
" content — the README is machine-written, and a hand edit here is" \
" exactly what its own text warns against. Fix: run docs-sync --fix."
fi
fi
# Existence only, content free: the stub is per-repo the moment the repo
@ -313,6 +376,7 @@ run_fix() {
fi
}
guard_plain_tree
case "$mode" in
check) run_check ;;
fix) run_fix ;;

View file

@ -176,6 +176,103 @@ check "root AGENTS.md missing → check fails, teaching --fix" 1 \
"run docs-sync --fix" in_consumer fresh --check --source "$SRC"
in_consumer fresh --fix --source "$SRC" >/dev/null
# --- the README is machine-verified, not just machine-written -------------------
# The marker that says "a hand edit goes red" must itself go red when
# hand-edited (kimi-bot, PR #43's review round).
printf 'hand edit\n' >>"$TMP/fresh/.ceremony/README.md"
check "hand-edited README → check fails naming it" 1 ".ceremony/README.md" \
in_consumer fresh --check --source "$SRC"
check "--fix rewrites the drifted README" 0 "wrote .ceremony/README.md" \
in_consumer fresh --fix --source "$SRC"
rm "$TMP/fresh/.ceremony/README.md"
check "missing README → check fails naming it" 1 \
".ceremony/README.md is missing" in_consumer fresh --check --source "$SRC"
in_consumer fresh --fix --source "$SRC" >/dev/null
check "README repaired → check green again" 0 "exact mirror" \
in_consumer fresh --check --source "$SRC"
# --- the mirror is plain files: symlinks and friends refused ---------------------
# PR #43's review round (codex-bot + kimi-bot, independent repros): cp
# writes THROUGH a committed link, cmp reads through it, and a `find
# -type f` scan cannot even see it. Both modes refuse; every row with a
# victim asserts the victim untouched.
consumer sneaky 0.3.0
in_consumer sneaky --fix --source "$SRC" >/dev/null
printf 'victim v1\n' >"$TMP/sneaky/victim.md"
rm "$TMP/sneaky/.ceremony/RULES.md"
ln -s ../victim.md "$TMP/sneaky/.ceremony/RULES.md"
check "vendored path as symlink → check refuses naming it" 1 \
".ceremony/RULES.md" in_consumer sneaky --check --source "$SRC"
check "vendored path as symlink → fix refuses (never writes through)" 1 \
"non-regular" in_consumer sneaky --fix --source "$SRC"
check "the link's target is untouched" 0 "victim v1" cat "$TMP/sneaky/victim.md"
rm "$TMP/sneaky/.ceremony/RULES.md"
in_consumer sneaky --fix --source "$SRC" >/dev/null
# A stray link is exactly what the -type f extra-file scan was blind to:
# unlisted doctrine, previously invisible.
ln -s ../victim.md "$TMP/sneaky/.ceremony/STRAYLINK.md"
check "stray symlink (invisible to -type f) → check refuses" 1 \
"STRAYLINK.md" in_consumer sneaky --check --source "$SRC"
check "stray symlink → fix refuses too (no silent deletion of a link)" 1 \
"STRAYLINK.md" in_consumer sneaky --fix --source "$SRC"
rm "$TMP/sneaky/.ceremony/STRAYLINK.md"
mkfifo "$TMP/sneaky/.ceremony/PIPE"
check "a fifo in the mirror → refused, not read" 1 "non-regular" \
in_consumer sneaky --check --source "$SRC"
rm "$TMP/sneaky/.ceremony/PIPE"
rm -rf "$TMP/sneaky/.ceremony/guide"
mkdir -p "$TMP/sneaky/elsewhere"
ln -s ../elsewhere "$TMP/sneaky/.ceremony/guide"
check "vendored subdirectory as symlink → fix refuses" 1 \
".ceremony/guide" in_consumer sneaky --fix --source "$SRC"
check "nothing was written into the linked directory's target" 1 "" \
test -e "$TMP/sneaky/elsewhere/DEEP.md"
rm "$TMP/sneaky/.ceremony/guide"
in_consumer sneaky --fix --source "$SRC" >/dev/null
check "sneaky consumer repaired → check green" 0 "exact mirror" \
in_consumer sneaky --check --source "$SRC"
consumer linked-mirror 0.3.0
mkdir -p "$TMP/linked-mirror-target"
ln -s ../linked-mirror-target "$TMP/linked-mirror/.ceremony"
check ".ceremony/ itself a symlink → check refuses" 1 "symlink" \
in_consumer linked-mirror --check --source "$SRC"
check ".ceremony/ itself a symlink → fix refuses" 1 "symlink" \
in_consumer linked-mirror --fix --source "$SRC"
check "the link's target directory stayed empty" 0 "" \
test -z "$(ls -A "$TMP/linked-mirror-target")"
consumer linked-stub 0.3.0
printf 'stub victim\n' >"$TMP/linked-stub/other.md"
ln -s other.md "$TMP/linked-stub/AGENTS.md"
check "root AGENTS.md as symlink → check refuses" 1 \
"AGENTS.md is a symlink" in_consumer linked-stub --check --source "$SRC"
check "root AGENTS.md as symlink → fix refuses" 1 \
"AGENTS.md is a symlink" in_consumer linked-stub --fix --source "$SRC"
check "the scaffold did not write through the link" 0 "stub victim" \
cat "$TMP/linked-stub/other.md"
# Dangling is the sharpest case: -e is false through a dangling link, so a
# naive `[ ! -e ] && scaffold` writes the stub through it.
rm "$TMP/linked-stub/AGENTS.md"
ln -s does-not-exist.md "$TMP/linked-stub/AGENTS.md"
check "dangling AGENTS.md symlink → fix refuses (would write through)" 1 \
"symlink" in_consumer linked-stub --fix --source "$SRC"
check "nothing appeared at the dangling target" 1 "" \
test -e "$TMP/linked-stub/does-not-exist.md"
rm "$TMP/linked-stub/AGENTS.md"
mkdir "$TMP/linked-stub/AGENTS.md"
check "root AGENTS.md as a directory → refused, named" 1 \
"not a regular file" in_consumer linked-stub --fix --source "$SRC"
# --- the action's wiring: inputs arrive as env vars -----------------------------
consumer env-wired 0.3.0