feat(lint): wire Vale as deterministic prefilter for skill-audit/agent-audit #85
@@ -8,5 +8,5 @@
|
|||||||
"keywords": [],
|
"keywords": [],
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"name": "kyberforge",
|
"name": "kyberforge",
|
||||||
"version": "1.2.6"
|
"version": "1.2.7"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,5 +13,5 @@
|
|||||||
"skills": [
|
"skills": [
|
||||||
"skills/"
|
"skills/"
|
||||||
],
|
],
|
||||||
"version": "1.2.6"
|
"version": "1.2.7"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ set -euo pipefail
|
|||||||
|
|
||||||
cwd="$(pwd -P)"
|
cwd="$(pwd -P)"
|
||||||
|
|
||||||
|
# Every array below is expanded as `${arr[@]+"${arr[@]}"}`: bash before 4.4 —
|
||||||
|
# including the 3.2 that macOS still ships as /bin/bash — treats `"${arr[@]}"`
|
||||||
|
# on an empty array as an unbound variable under `set -u`. No expansion site is
|
||||||
|
# reachable while empty on today's control flow, so this is insurance against a
|
||||||
|
# later edit breaking that invariant, not a live fix.
|
||||||
vale_args=()
|
vale_args=()
|
||||||
path_args=()
|
path_args=()
|
||||||
config_next=false
|
config_next=false
|
||||||
@@ -93,7 +98,7 @@ fi
|
|||||||
if [[ ${#path_args[@]} -eq 0 ]]; then
|
if [[ ${#path_args[@]} -eq 0 ]]; then
|
||||||
# Nothing to flatten. Hand off directly, with stdin closed so vale doesn't
|
# Nothing to flatten. Hand off directly, with stdin closed so vale doesn't
|
||||||
# block waiting on a pipe that will never carry content.
|
# block waiting on a pipe that will never carry content.
|
||||||
exec vale "${vale_args[@]}" < /dev/null
|
exec vale ${vale_args[@]+"${vale_args[@]}"} < /dev/null
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# `realpath -m` would be the obvious normalizer, but `-m` (canonicalize-missing)
|
# `realpath -m` would be the obvious normalizer, but `-m` (canonicalize-missing)
|
||||||
@@ -178,7 +183,7 @@ mirror="$tmpdir$cwd"
|
|||||||
mkdir -p "$mirror"
|
mkdir -p "$mirror"
|
||||||
|
|
||||||
argv_paths=()
|
argv_paths=()
|
||||||
for arg in "${path_args[@]}"; do
|
for arg in ${path_args[@]+"${path_args[@]}"}; do
|
||||||
if [[ "$arg" == /* ]]; then
|
if [[ "$arg" == /* ]]; then
|
||||||
dest="$tmpdir$arg"
|
dest="$tmpdir$arg"
|
||||||
else
|
else
|
||||||
@@ -220,4 +225,4 @@ for arg in "${path_args[@]}"; do
|
|||||||
done
|
done
|
||||||
|
|
||||||
cd "$mirror"
|
cd "$mirror"
|
||||||
vale "${vale_args[@]}" "${argv_paths[@]}"
|
vale ${vale_args[@]+"${vale_args[@]}"} ${argv_paths[@]+"${argv_paths[@]}"}
|
||||||
|
|||||||
@@ -34,6 +34,11 @@ set -euo pipefail
|
|||||||
|
|
||||||
cwd="$(pwd -P)"
|
cwd="$(pwd -P)"
|
||||||
|
|
||||||
|
# Every array below is expanded as `${arr[@]+"${arr[@]}"}`: bash before 4.4 —
|
||||||
|
# including the 3.2 that macOS still ships as /bin/bash — treats `"${arr[@]}"`
|
||||||
|
# on an empty array as an unbound variable under `set -u`. No expansion site is
|
||||||
|
# reachable while empty on today's control flow, so this is insurance against a
|
||||||
|
# later edit breaking that invariant, not a live fix.
|
||||||
vale_args=()
|
vale_args=()
|
||||||
path_args=()
|
path_args=()
|
||||||
config_next=false
|
config_next=false
|
||||||
@@ -93,7 +98,7 @@ fi
|
|||||||
if [[ ${#path_args[@]} -eq 0 ]]; then
|
if [[ ${#path_args[@]} -eq 0 ]]; then
|
||||||
# Nothing to flatten. Hand off directly, with stdin closed so vale doesn't
|
# Nothing to flatten. Hand off directly, with stdin closed so vale doesn't
|
||||||
# block waiting on a pipe that will never carry content.
|
# block waiting on a pipe that will never carry content.
|
||||||
exec vale "${vale_args[@]}" < /dev/null
|
exec vale ${vale_args[@]+"${vale_args[@]}"} < /dev/null
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# `realpath -m` would be the obvious normalizer, but `-m` (canonicalize-missing)
|
# `realpath -m` would be the obvious normalizer, but `-m` (canonicalize-missing)
|
||||||
@@ -178,7 +183,7 @@ mirror="$tmpdir$cwd"
|
|||||||
mkdir -p "$mirror"
|
mkdir -p "$mirror"
|
||||||
|
|
||||||
argv_paths=()
|
argv_paths=()
|
||||||
for arg in "${path_args[@]}"; do
|
for arg in ${path_args[@]+"${path_args[@]}"}; do
|
||||||
if [[ "$arg" == /* ]]; then
|
if [[ "$arg" == /* ]]; then
|
||||||
dest="$tmpdir$arg"
|
dest="$tmpdir$arg"
|
||||||
else
|
else
|
||||||
@@ -220,4 +225,4 @@ for arg in "${path_args[@]}"; do
|
|||||||
done
|
done
|
||||||
|
|
||||||
cd "$mirror"
|
cd "$mirror"
|
||||||
vale "${vale_args[@]}" "${argv_paths[@]}"
|
vale ${vale_args[@]+"${vale_args[@]}"} ${argv_paths[@]+"${argv_paths[@]}"}
|
||||||
|
|||||||
@@ -425,6 +425,81 @@ else
|
|||||||
fail "a path with a space was dropped from the directory walk"
|
fail "a path with a space was dropped from the directory walk"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# --- 16. No unguarded `"${arr[@]}"` expansion survives in the wrapper. bash
|
||||||
|
# before 4.4 — including the 3.2 that macOS still ships as /bin/bash — treats
|
||||||
|
# that form on an empty array as an unbound variable under `set -u` and aborts.
|
||||||
|
# The portable form is `${arr[@]+"${arr[@]}"}`. This is a static check because
|
||||||
|
# no bash 5 host can reproduce the abort at runtime: the construct is only fatal
|
||||||
|
# on the older shell, so absence of the construct is the property to assert.
|
||||||
|
# `${#arr[@]}` is deliberately not flagged — the count form is safe on 3.2.
|
||||||
|
echo ""
|
||||||
|
echo "--- no unguarded array expansion remains in vale-wrap.sh ---"
|
||||||
|
unguarded_expansions() {
|
||||||
|
# Blank out whole-line comments (keeping line numbers), delete every correctly
|
||||||
|
# guarded expansion, then anything still matching is a real hazard.
|
||||||
|
awk '{ if ($0 ~ /^[[:space:]]*#/) print ""; else print }' "$1" \
|
||||||
|
| sed -E 's/\$\{([A-Za-z_][A-Za-z0-9_]*)\[@\]\+"\$\{\1\[@\]\}"\}//g' \
|
||||||
|
| grep -nE '\$\{[A-Za-z_][A-Za-z0-9_]*\[@\]\}' || true
|
||||||
|
}
|
||||||
|
HAZARDS16="$(unguarded_expansions "$SCRIPT")"
|
||||||
|
if [[ -n "$HAZARDS16" ]]; then
|
||||||
|
fail "unguarded array expansion(s) abort on bash < 4.4 under set -u: $(echo "$HAZARDS16" | tr '\n' ' ')"
|
||||||
|
else
|
||||||
|
pass "every array expansion uses the bash-3.2-safe \${arr[@]+\"\${arr[@]}\"} form"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 17. The invocations whose arrays are closest to empty actually run. Under
|
||||||
|
# a bash older than 4.4 this is genuine macOS-shell coverage; on a modern bash it
|
||||||
|
# degrades to a smoke test, so the pass message names the shell that really ran.
|
||||||
|
# Point VALE_WRAP_TEST_BASH at a 3.2 build to get the real thing in CI.
|
||||||
|
echo ""
|
||||||
|
echo "--- degenerate invocations survive on the oldest available bash ---"
|
||||||
|
OLD_BASH="bash"
|
||||||
|
OLD_BASH_VER="$(bash -c 'echo "${BASH_VERSINFO[0]}.${BASH_VERSINFO[1]}"')"
|
||||||
|
for CAND in "${VALE_WRAP_TEST_BASH:-}" bash-3.2 bash3 /bin/bash /usr/local/bin/bash; do
|
||||||
|
[[ -n "$CAND" ]] && command -v "$CAND" >/dev/null 2>&1 || continue
|
||||||
|
CAND_VER="$("$CAND" -c 'echo "${BASH_VERSINFO[0]}.${BASH_VERSINFO[1]}"' 2>/dev/null)" || continue
|
||||||
|
[[ -n "$CAND_VER" ]] || continue
|
||||||
|
if (( ${CAND_VER%.*} * 100 + ${CAND_VER#*.} < ${OLD_BASH_VER%.*} * 100 + ${OLD_BASH_VER#*.} )); then
|
||||||
|
OLD_BASH="$CAND"
|
||||||
|
OLD_BASH_VER="$CAND_VER"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
FIXTURE17="$(make_fixture 2)"
|
||||||
|
mkdir -p "$FIXTURE17/emptydir"
|
||||||
|
trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6" "$FIXTURE7" "$FIXTURE8" "$FIXTURE10" "$FIXTURE11" "$FIXTURE12" "$STUB13" "$FIXTURE14" "$FIXTURE17"' EXIT
|
||||||
|
# Zero args, flags with no path, and a directory that walks to nothing are the
|
||||||
|
# three shapes that leave vale_args/path_args/argv_paths at their emptiest.
|
||||||
|
OUT17=""
|
||||||
|
for ARGS17 in "" "--config $VALE_CONFIG" "--config $VALE_CONFIG emptydir"; do
|
||||||
|
# shellcheck disable=SC2086 # deliberate word splitting of the argv fixture
|
||||||
|
OUT17+="$( (cd "$FIXTURE17" && "$OLD_BASH" "$SCRIPT" $ARGS17 </dev/null 2>&1) || true)"
|
||||||
|
done
|
||||||
|
if echo "$OUT17" | grep -q "unbound variable"; then
|
||||||
|
fail "aborted with 'unbound variable' on bash $OLD_BASH_VER — the bug this test guards against"
|
||||||
|
else
|
||||||
|
pass "degenerate invocations run clean under bash $OLD_BASH_VER ($OLD_BASH)"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# --- 18. The guarded expansion must keep argv word boundaries intact. Dropping
|
||||||
|
# the quotes (`${arr[@]}`) also silences the unbound-variable abort, so it is the
|
||||||
|
# tempting wrong fix — and it splits any path containing a space into two bogus
|
||||||
|
# arguments. Case 15 covers spaces found by the directory walk; this covers a
|
||||||
|
# space in the path argument itself, which is what argv_paths expands.
|
||||||
|
echo ""
|
||||||
|
echo "--- a path argument containing a space survives the guarded expansion ---"
|
||||||
|
FIXTURE18="$(make_fixture 2)"
|
||||||
|
trap 'rm -rf "$FIXTURE1" "$FIXTURE2" "$FIXTURE3" "$FIXTURE4" "$FIXTURE5" "$FIXTURE6" "$FIXTURE7" "$FIXTURE8" "$FIXTURE10" "$FIXTURE11" "$FIXTURE12" "$STUB13" "$FIXTURE14" "$FIXTURE17" "$FIXTURE18"' EXIT
|
||||||
|
SPACED18="$FIXTURE18/plugins/testplugin/skills/zzz skill dir"
|
||||||
|
mkdir -p "$SPACED18"
|
||||||
|
mv "$FIXTURE18/plugins/testplugin/skills/zzzskill/SKILL.md" "$SPACED18/SKILL.md"
|
||||||
|
OUT18=$(run_wrap "$FIXTURE18" --config "$VALE_CONFIG" "plugins/testplugin/skills/zzz skill dir/SKILL.md")
|
||||||
|
if echo "$OUT18" | grep -q "zzz skill dir/SKILL.md" && echo "$OUT18" | grep -q "VagueWording"; then
|
||||||
|
pass "a path argument with a space is passed to vale as one word"
|
||||||
|
else
|
||||||
|
fail "a path argument with a space was split by the array expansion: $OUT18"
|
||||||
|
fi
|
||||||
|
|
||||||
echo ""
|
echo ""
|
||||||
echo "Results: $PASS passed, $FAIL failed"
|
echo "Results: $PASS passed, $FAIL failed"
|
||||||
[[ $FAIL -eq 0 ]]
|
[[ $FAIL -eq 0 ]]
|
||||||
|
|||||||
Reference in New Issue
Block a user