Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
84 changes: 82 additions & 2 deletions .claude/hooks/guard-governed-enqueue.selftest.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,11 @@ set -uo pipefail

here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
hook="$here/guard-governed-enqueue.sh"
# The hook `run`/`stderr_of` actually invoke. It is `$hook` for every case but
# one: the bare `gh pr merge <n>` case asks the hook about a DIFFERENT checkout's
# origin, and the hook derives its repo root from its own path, so that case runs
# a copy sitting in a checkout it built — and restores this on the next line.
hook_under_test="$hook"
repo_root="$(cd "$here/../.." && pwd)"
pass=0
fail=0
Expand Down Expand Up @@ -112,7 +117,7 @@ MERGE=mcp__github__merge_pull_request
run() { # run <payload> [env assignments…] -> allow | block | exitN
local payload="$1"; shift
local rc
printf '%s' "$payload" | env "$@" "$hook" >/dev/null 2>&1
printf '%s' "$payload" | env "$@" "$hook_under_test" >/dev/null 2>&1
rc=$?
case "$rc" in
0) printf 'allow' ;;
Expand All @@ -123,7 +128,7 @@ run() { # run <payload> [env assignments…] -> allow | block | exitN

stderr_of() { # stderr_of <payload> [env…]
local payload="$1"; shift
printf '%s' "$payload" | env "$@" "$hook" 2>&1 >/dev/null
printf '%s' "$payload" | env "$@" "$hook_under_test" 2>&1 >/dev/null
}

expect() { # expect <block|allow> <label> <payload> [env…]
Expand Down Expand Up @@ -290,6 +295,38 @@ expect allow 'the same Bash spelling on an approved PR' \
"$(bash_call 'gh pr merge 13794 -R objectstack-ai/objectstack')" \
"OS_GOVERNED_ENQUEUE_FIXTURE=$F_PINNED"

echo "== a bare \`gh pr merge <n>\` derives the slug from the checkout's OWN origin =="
# The second half of this card's defect, and the expensive half: with no `-R` the
# target repo comes from the checkout's `origin`, so a `.git` suffix left on the
# slug rides into the API URL — `GET /repos/objectstack-ai/objectstack.git/pulls/N`
# answers 404 and this guard's read-failure branch ALLOWS. On any clone made with
# the URL `git clone` hands out by default, that spelling was unguarded.
#
# The checkout is BUILT here: the hook derives its repo root from its own path
# (`BASH_SOURCE`), so the only way to ask it about another origin is to run a copy
# that sits in one. `.claude/hooks/` and `scripts/` are symlinked back at this
# tree, so both predicates are the same files, running the same way.
#
# ⛔ The allow/block verdict is deliberately NOT asserted here, and that is
# measured rather than cautious: `OS_GOVERNED_ENQUEUE_FIXTURE` answers EVERY path,
# so the 404 that makes the real guard fail open cannot be reproduced without the
# network — under the fixture the wrong slug blocks exactly like the right one,
# and a verdict row here would pass in both worlds. What discriminates is the slug
# itself, which the refusal prints as `<owner>/<repo>#<n>`: it read
# `objectstack-ai/objectstack.git#13794` until `slug_of` stripped the suffix.
DOTGIT_CLONE="$root/dotgit-url-clone" # under $root: the existing trap removes it
mkdir -p "$DOTGIT_CLONE/.claude/hooks"
ln -s "$hook" "$DOTGIT_CLONE/.claude/hooks/guard-governed-enqueue.sh"
ln -s "$repo_root/scripts" "$DOTGIT_CLONE/scripts"
git -C "$DOTGIT_CLONE" init -q >/dev/null 2>&1
git -C "$DOTGIT_CLONE" remote add origin https://github.com/objectstack-ai/objectstack.git >/dev/null 2>&1
hook_under_test="$DOTGIT_CLONE/.claude/hooks/guard-governed-enqueue.sh"
expect_says 'objectstack-ai/objectstack#13794' 'the slug the guard reads is the one the API answers for' \
"$(bash_call 'gh pr merge 13794 --squash')" "OS_GOVERNED_ENQUEUE_FIXTURE=$F_UNAPPROVED"
expect_lacks 'objectstack.git' 'no .git suffix rides into the repo path the guard reads' \
"$(bash_call 'gh pr merge 13794 --squash')" "OS_GOVERNED_ENQUEUE_FIXTURE=$F_UNAPPROVED"
hook_under_test="$hook" # every case below is back on the real hook

echo "== an UNQUOTED \\\" opens no quote, so the merge behind it is still seen =="
# The #11738 class, carried by all three Bash-reading guards in this directory:
# segmentation used to read the escaped `\"` as OPENING a region that never
Expand Down Expand Up @@ -433,6 +470,49 @@ expect "$sibling_want" 'a sibling checkout that resolves is audited, never waved
# worlds and discriminated nothing. The verdict row above is the discriminator,
# and it is the one that goes red when the sibling stops being resolved.

echo "== every spelling of that same origin URL resolves to that same sibling =="
# The other half of this card's defect. `git clone` hands out
# `https://github.com/objectstack-ai/cloud.git`, and until `slug_of` stripped that
# suffix the slug read back as `objectstack-ai/cloud.git` — equal to no
# `owner/repo` this guard is ever asked about, so a sibling cloned the
# conventional way resolved NOTHING and the case above silently degraded into a
# second copy of the "cannot resolve" one. That is why its origin is pinned to the
# bare form in place; these are the four spellings it does not cover.
#
# Same `$sibling_want`, and deliberately the same variable rather than four more
# register calls: every tree here is built exactly as that one is (an empty
# `git init` plus an `origin`), so the register would be answering the identical
# question about identical content — and the verdict still comes FROM the register
# rather than from a word copied into this file.
#
# ⚠️ `-u GIT_CONFIG_COUNT -u GIT_CONFIG_PARAMETERS` is what makes the two
# `git@github.com:` rows MEAN what they say, and it was measured here rather than
# assumed: this container injects `url.https://github.com/.insteadOf
# git@github.com:` through the environment, and the hook reads the origin with
# `git remote get-url`, which HONOURS that rewrite — so without the unset git
# hands the hook `https://…` and both ssh rows silently become second copies of
# the https ones, green forever over a reader they never reach. A row whose
# premise is a property of the box is the defect the cross-repo case above was
# repaired for; these rows own theirs. On a machine that injects nothing, `env -u`
# on an unset name is a no-op.
spelling_n=0
for spelling_url in \
'https://github.com/objectstack-ai/cloud.git' \
'git@github.com:objectstack-ai/cloud' \
'git@github.com:objectstack-ai/cloud.git' \
'https://github.com/objectstack-ai/cloud.git/' \
; do
spelling_n=$((spelling_n + 1))
spelling_root="$root/sibling-spelling-$spelling_n" # under $root: the trap removes it
mkdir -p "$spelling_root/cloud"
git -C "$spelling_root/cloud" init -q >/dev/null 2>&1
git -C "$spelling_root/cloud" remote add origin "$spelling_url" >/dev/null 2>&1
expect "$sibling_want" "origin $spelling_url resolves and is audited" \
"$(mcp $AUTO 999 objectstack-ai cloud)" \
-u GIT_CONFIG_COUNT -u GIT_CONFIG_PARAMETERS \
"OS_GOVERNED_ENQUEUE_FIXTURE=$F_CROSS_REGEN" "OS_GOVERNED_ENQUEUE_SIBLING_ROOT=$spelling_root"
done

echo "== the deliberate exception switch =="
expect allow 'OS_ALLOW_GOVERNED_ENQUEUE=1 on the blocking case' \
"$(mcp $AUTO 13794)" "OS_GOVERNED_ENQUEUE_FIXTURE=$F_UNAPPROVED" OS_ALLOW_GOVERNED_ENQUEUE=1
Expand Down
32 changes: 28 additions & 4 deletions .claude/hooks/guard-governed-enqueue.sh
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,31 @@ url_target() { # url_target <word> -> "owner repo pull" or empty
printf '%s %s %s' "${BASH_REMATCH[1]}" "${BASH_REMATCH[2]}" "${BASH_REMATCH[3]}"
}

# The `owner/repo` a checkout's `origin` names, or nothing. ONE reader with two
# call sites — the bare `gh pr merge <n>` target derivation just below, and the
# sibling admission rule further down — because the two inlined copies it
# replaces agreed on a defect: the path character class owns `.` and is greedy,
# so the optional `\(\.git\)` group matched EMPTY and
# `https://github.com/objectstack-ai/cloud.git` read as the slug
# `objectstack-ai/cloud.git`, which equals no `owner/repo` this guard is ever
# asked about. A sibling cloned with the conventional URL therefore never
# resolved, and a bare `gh pr merge <n>` in such a clone derived a slug the API
# answers 404 for — this guard's fail-open, on the clone URL `git clone` hands
# out by default.
#
# The suffix is stripped AFTER the match rather than carved out of the class:
# the class still has to own `.` (a repository may legitimately be called
# `objectstack.ai`), and which of the two the class-vs-group race gives it up to
# is exactly the kind of thing GNU and BSD sed are free to disagree about. A
# trailing `.git` is removed by the shell either way, and nothing legitimate is
# lost with it — GitHub refuses a repository name that ends in `.git`.
slug_of() { # slug_of <checkout dir> -> owner/repo, or empty
local slug
slug="$(git -C "$1" remote get-url origin 2>/dev/null \
| sed -n 's#.*github\.com[:/]\([A-Za-z0-9._-]*/[A-Za-z0-9._-]*\)/*$#\1#p')"
printf '%s' "${slug%.git}"
}

# The enqueue-class target one shell segment names, or nothing.
segment_target() { # segment_target <segment> -> "owner repo pull" or empty
local seg="$1"
Expand Down Expand Up @@ -354,8 +379,7 @@ segment_target() { # segment_target <segment> -> "owner repo pull" or empty
return 1
fi
if [ -z "$slug" ]; then
slug="$(git -C "$repo_root" remote get-url origin 2>/dev/null || true)"
slug="$(printf '%s' "$slug" | sed -n 's#.*github\.com[:/]\([A-Za-z0-9._-]*/[A-Za-z0-9._-]*\)\(\.git\)\{0,1\}/*$#\1#p')"
slug="$(slug_of "$repo_root")"
fi
[ -n "$slug" ] || return 1
printf '%s %s %s' "${slug%%/*}" "${slug#*/}" "$num"
Expand Down Expand Up @@ -473,8 +497,8 @@ done < "$work/files.txt"
# has to be the target repo's own tree. Resolve a checkout whose origin actually
# declares owner/repo — never audit one repo's paths against another's files.

slug_of() { git -C "$1" remote get-url origin 2>/dev/null | sed -n 's#.*github\.com[:/]\([A-Za-z0-9._-]*/[A-Za-z0-9._-]*\)\(\.git\)\{0,1\}/*$#\1#p'; }

# `slug_of` is defined once, up beside the Bash pass that shares it.
#
# WHERE a sibling is looked for is injectable (OS_GOVERNED_ENQUEUE_SIBLING_ROOT,
# header); WHAT is accepted as one is not. The slug comparison below is the whole
# admission rule and is untouched by it, so the variable can only move the
Expand Down
Loading