Merge bugfix/audit-bugs into develop

This commit is contained in:
Bastien Chanot
2026-07-02 14:35:31 +02:00
6 changed files with 93 additions and 47 deletions
+29 -14
View File
@@ -213,11 +213,20 @@ print(len(d.get('permissions',{}).get('deny',[])))
if [ "$DENY_COUNT" = "?" ]; then
warn "Could not parse deny count (python3 unavailable or JSON parse error)"
else
EXPECTED_DENY=100
if [ "$DENY_COUNT" -eq "$EXPECTED_DENY" ] 2>/dev/null; then
pass "Deny rules: $DENY_COUNT"
# Expected = deny count in the last COMMITTED settings.json. A hardcoded
# number drifts on every legit deny-list edit (false-warned for weeks at
# 100 vs 99 — LRN-047 class); deriving from HEAD auto-tracks legit edits
# and still flags live-vs-committed divergence.
EXPECTED_DENY=$(git -C "$REPO" show HEAD:settings.json 2>/dev/null | python3 -c "
import json,sys
print(len(json.load(sys.stdin).get('permissions',{}).get('deny',[])))
" 2>/dev/null || echo "?")
if [ "$EXPECTED_DENY" = "?" ]; then
warn "Could not derive expected deny count from committed settings.json"
elif [ "$DENY_COUNT" -eq "$EXPECTED_DENY" ] 2>/dev/null; then
pass "Deny rules: $DENY_COUNT (matches committed settings.json)"
else
warn "Deny rules: $DENY_COUNT (expected $EXPECTED_DENY) — settings may have been manually modified"
warn "Deny rules: $DENY_COUNT (committed: $EXPECTED_DENY) — live settings diverge from last commit"
fi
fi
else
@@ -310,8 +319,11 @@ else
warn "gstack/browse/dist/ symlink missing — run: bash link.sh"
fi
# Check owned skills have disable-model-invocation (skip external/symlinked skills)
MISSING_DMI=()
# BDR-019 (2026-06-09) stripped disable-model-invocation repo-wide so the
# model/orchestrators can self-route. The old check required the key on
# every owned skill — permanent false-warn since. Inverted: warn if any
# owned skill REintroduces the key (regression watch on BDR-019).
PRESENT_DMI=()
for f in "$HOME/.claude/skills/"*/SKILL.md; do
[ -f "$f" ] || continue
dir=$(dirname "$f")
@@ -319,19 +331,22 @@ for f in "$HOME/.claude/skills/"*/SKILL.md; do
[ -L "$dir" ] && continue
[ -L "$f" ] && continue
name=$(basename "$dir")
if ! grep -q "disable-model-invocation" "$f" 2>/dev/null; then
MISSING_DMI+=("$name")
if grep -q "disable-model-invocation" "$f" 2>/dev/null; then
PRESENT_DMI+=("$name")
fi
done
if [ ${#MISSING_DMI[@]} -eq 0 ]; then
pass "All owned skills have disable-model-invocation"
if [ ${#PRESENT_DMI[@]} -eq 0 ]; then
pass "No owned skill carries disable-model-invocation (BDR-019)"
else
warn "Owned skills missing disable-model-invocation: ${MISSING_DMI[*]}"
warn "Owned skills reintroduce disable-model-invocation (BDR-019 regression): ${PRESENT_DMI[*]}"
fi
# Check expected skills are present
# Check expected skills are present. Repo-owned skills only: gstack skills
# (health, status, …) are OFF by default and toggled per profile — requiring
# them here false-warns on a default install, and "run link.sh" cannot
# restore them (they are profile-managed, not link.sh-managed).
EXPECTED_SKILLS=(
"analyze" "doc" "health" "init-project" "onboard" "plugin-check"
"analyze" "doc" "init-project" "onboard" "plugin-check"
"refactor" "ship-feature" "status"
)
MISSING_SKILLS=()
@@ -341,7 +356,7 @@ for skill in "${EXPECTED_SKILLS[@]}"; do
fi
done
if [ ${#MISSING_SKILLS[@]} -eq 0 ]; then
pass "All ${#EXPECTED_SKILLS[@]} expected skills present (analyze, doc, health, init-project, onboard, plugin-check, refactor, ship-feature, status)"
pass "All ${#EXPECTED_SKILLS[@]} expected skills present (${EXPECTED_SKILLS[*]})"
else
warn "Missing skills: ${MISSING_SKILLS[*]} — run: bash link.sh"
fi
+1 -1
View File
@@ -1 +1 @@
ef0d630994fd7ef5f2b84fb66cd6249c493bb8736bcacd4734d7c798125018fb rtk-rewrite.sh
871efa28daf7c06a9c9039a2875407e2536646f5d82f7e7a9c6a80dd3742929c rtk-rewrite.sh
+55 -29
View File
@@ -7,8 +7,17 @@
# which is the single source of truth (src/discover/registry.rs).
# To add or change rewrite rules, edit the Rust registry — not this file.
#
# INTEGRITY PIN: the rtk binary verifies this file against
# hooks/.rtk-hook.sha256 at execution time and refuses to run on mismatch.
# ANY edit here must re-pin: (cd hooks && sha256sum rtk-rewrite.sh > .rtk-hook.sha256)
#
# Exit code protocol for `rtk rewrite`:
# 0 + stdout Rewrite found, no deny/ask rule matched → auto-allow
# 0 + stdout Rewrite found, no rtk deny/ask rule matched → rewrite. NO
# permissionDecision is emitted (auto-allow dropped 2026-07-02:
# it made rtk's registry a parallel permission authority that
# bypassed settings.json deny/ask). The REWRITTEN command goes
# through native evaluation; explicit `rtk <tool>` allow rules
# in settings.json keep read-only forms frictionless.
# 1 No RTK equivalent → pass through unchanged
# 2 Deny rule matched → pass through (Claude Code native deny handles it)
# 3 + stdout Ask rule matched → rewrite but let Claude Code prompt the user
@@ -18,14 +27,27 @@ if ! command -v jq &>/dev/null; then
exit 0
fi
if ! command -v rtk &>/dev/null; then
# PATH heal: hook/tool-shell PATH may lack the cargo bin dir (hand-managed
# ~/.bashrc can lose the cargo line — LRN-036 class). Resolve the ABSOLUTE
# binary path: the rewritten command executes in the tool shell, whose PATH
# the hook cannot fix — a bare `rtk …` rewrite would exit 127 there.
RTK_BIN="$(command -v rtk 2>/dev/null || true)"
RTK_ON_PATH=1
if [ -z "$RTK_BIN" ]; then
RTK_ON_PATH=0
for _d in "$HOME/.cargo/bin" "$HOME/.local/bin"; do
if [ -x "$_d/rtk" ]; then RTK_BIN="$_d/rtk"; break; fi
done
fi
if [ -z "$RTK_BIN" ]; then
echo "[rtk] WARNING: rtk is not installed or not in PATH. Hook cannot rewrite commands. Install: https://github.com/rtk-ai/rtk#installation" >&2
exit 0
fi
# Version guard: rtk rewrite was added in 0.23.0.
# Older binaries: warn once and exit cleanly (no silent failure).
RTK_VERSION=$(rtk --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
RTK_VERSION=$("$RTK_BIN" --version 2>/dev/null | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [ -n "$RTK_VERSION" ]; then
MAJOR=$(echo "$RTK_VERSION" | cut -d. -f1)
MINOR=$(echo "$RTK_VERSION" | cut -d. -f2)
@@ -44,13 +66,13 @@ if [ -z "$CMD" ]; then
fi
# Delegate all rewrite + permission logic to the Rust binary.
REWRITTEN=$(rtk rewrite "$CMD" 2>/dev/null)
REWRITTEN=$("$RTK_BIN" rewrite "$CMD" 2>/dev/null)
EXIT_CODE=$?
case $EXIT_CODE in
0)
# Rewrite found, no permission rules matched — safe to auto-allow.
# If the output is identical, the command was already using RTK.
# Rewrite found. If the output is identical, the command was
# already using RTK — nothing to do.
[ "$CMD" = "$REWRITTEN" ] && exit 0
;;
1)
@@ -70,29 +92,33 @@ case $EXIT_CODE in
;;
esac
# When rtk is NOT on PATH, a bare `rtk …` rewrite exits 127 in the tool
# shell (whose PATH the hook cannot fix). Substitute the absolute path at
# the string head — the only position safe to rewrite. Compound commands
# (`a && b`) can carry further bare rtk segments we canNOT substitute
# safely (quoted text, e.g. commit messages, may contain the same
# pattern): if any remain at a command position, pass through unrewritten
# — lose the compression, never emit a command that 127s.
if [ "$RTK_ON_PATH" -eq 0 ]; then
case "$REWRITTEN" in
rtk\ *) REWRITTEN="$RTK_BIN ${REWRITTEN#rtk }" ;;
esac
if printf '%s' "$REWRITTEN" | grep -Eq '(^|[;&|][[:space:]]*)rtk[[:space:]]'; then
exit 0
fi
fi
ORIGINAL_INPUT=$(echo "$INPUT" | jq -c '.tool_input')
UPDATED_INPUT=$(echo "$ORIGINAL_INPUT" | jq --arg cmd "$REWRITTEN" '.command = $cmd')
if [ "$EXIT_CODE" -eq 3 ]; then
# Ask: rewrite the command, omit permissionDecision so Claude Code prompts.
jq -n \
--argjson updated "$UPDATED_INPUT" \
'{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"updatedInput": $updated
}
}'
else
# Allow: rewrite the command and auto-allow.
jq -n \
--argjson updated "$UPDATED_INPUT" \
'{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "allow",
"permissionDecisionReason": "RTK auto-rewrite",
"updatedInput": $updated
}
}'
fi
# Rewrite WITHOUT a permissionDecision (exit 0 and exit 3 alike): the
# rewritten command goes through Claude Code's native allow/deny/ask
# evaluation. Permission control lives in settings.json, not in rtk.
jq -n \
--argjson updated "$UPDATED_INPUT" \
'{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"updatedInput": $updated
}
}'
+1 -1
View File
@@ -182,7 +182,7 @@ printf "│ 📦 v%-45s│\n" "$CONFIG_VERSION"
# Version check: compare local vs remote (non-blocking)
_remote_ver=""
if [ -n "$REPO_DIR" ] && [ -d "$REPO_DIR/.git" ]; then
_remote_ver=$(cd "$REPO_DIR" 2>/dev/null && git fetch origin --quiet 2>/dev/null && git show origin/master:version.txt 2>/dev/null) || _remote_ver=""
_remote_ver=$(cd "$REPO_DIR" 2>/dev/null && git fetch origin --quiet 2>/dev/null && git show origin/main:version.txt 2>/dev/null) || _remote_ver=""
fi
if [ -n "$_remote_ver" ] && [ "$_remote_ver" != "$CONFIG_VERSION" ]; then
printf "│ 🔄 update available: v%-27s│\n" "$_remote_ver"
+3 -1
View File
@@ -10,7 +10,9 @@
# --- Always-on plugins ---
detect_rtk() {
command -v rtk &>/dev/null
command -v rtk &>/dev/null && return 0
# PATH heal: hook/session PATH may lack the cargo bin dir (LRN-036 class)
[ -x "$HOME/.cargo/bin/rtk" ] || [ -x "$HOME/.local/bin/rtk" ]
}
detect_superpowers() {
+4 -1
View File
@@ -65,7 +65,10 @@ if has "$cand" "--help"; then ok "T5 surfaced --help candidate (BDR-001 ⇄ --he
echo; echo "=== T6 live oracle smoke — oracles QUERY real git/fs (not a name) ==="
if reconcile_oracle_merge_done "$REPO" "prune-memory"; then ok "T6a merge_done(prune-memory) via git log"; else no "T6a merge not found in git"; fi
if reconcile_oracle_sha_exists "$REPO" "be1dcef"; then ok "T6b sha_exists(be1dcef) via cat-file"; else no "T6b sha missing"; fi
dk="$MEM/../skills/darwin-skill"
# $REPO here = lib/ (see line 12) → lib/../skills = the real skills/ dir.
# Was "$MEM/../skills" = .claude/skills/ — the LRN-042 parasite dir, removed
# 2026-06-30 by make plugin Step 8.5: green-for-wrong-reason (LRN-077 class).
dk="$REPO/../skills/darwin-skill"
if reconcile_oracle_path_present "$dk"; then ok "T6c path_present(darwin-skill) via fs"; else no "T6c path absent"; fi
echo; echo "================ $pass GREEN / $fail RED ================"