@webpieces/ai-hook-rules 0.4.558 → 0.4.560

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -80,6 +80,58 @@ fi
80
80
  # Read the tool payload ONCE, up front. The shim no longer exec's the bin (see RUN_BIN_SH), so it must
81
81
  # forward stdin to the bin itself — and it needs the payload again on the fail-closed path below.
82
82
  PAYLOAD="$(cat)"
83
+ CMD="$(printf '%s' "$PAYLOAD" | sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\([^"\\]*\)".*/\1/p')"
84
+ TOOL="$(printf '%s' "$PAYLOAD" | sed -n 's/.*"tool_name"[[:space:]]*:[[:space:]]*"\([^"\\]*\)".*/\1/p')"
85
+ FILE="$(printf '%s' "$PAYLOAD" | sed -n 's/.*"file_path"[[:space:]]*:[[:space:]]*"\([^"\\]*\)".*/\1/p')"
86
+ WP_CWD="$(printf '%s' "$PAYLOAD" | sed -n 's/.*"cwd"[[:space:]]*:[[:space:]]*"\([^"\\]*\)".*/\1/p')"
87
+ [ -n "$WP_CWD" ] || WP_CWD="$ROOT" # no cwd in the payload (older client, or a hand-run) → the shim's own tree
88
+ # Best-effort AUDIT TRAIL of what L0 did with this call — every call, not just the broken ones. One
89
+ # tab-separated line per invocation into this TREE's own logs/ai-hook-shim.log (gitignored), so the
90
+ # observed behaviour can be diffed against the matrix in guards/L0-tooling.md. NEVER breaks or blocks the
91
+ # hook: every write is swallowed, and nothing ever goes to stdout (stdout is the PreToolUse decision
92
+ # channel — a stray byte there would corrupt allow/deny).
93
+ WP_TREE=""
94
+ WP_LOG_DIR=""
95
+ wp_resolve_log_dir() {
96
+ _wp_rp="$(git -C "$WP_CWD" rev-parse --git-dir --git-common-dir 2>/dev/null)"
97
+ _wp_gd="$(printf '%s\n' "$_wp_rp" | sed -n 1p)"
98
+ _wp_cd="$(printf '%s\n' "$_wp_rp" | sed -n 2p)"
99
+ if [ -z "$_wp_gd" ] || [ -z "$_wp_cd" ]; then
100
+ WP_TREE=primary; WP_LOG_DIR="$WP_CWD/.webpieces/logs"; return 0
101
+ fi
102
+ # git prints a BARE .git from the primary clone and an absolute path from a linked worktree; the TS
103
+ # twin runs path.resolve(cwd, printed), so do the same before comparing or taking a basename.
104
+ case "$_wp_gd" in /*) : ;; *) _wp_gd="$WP_CWD/$_wp_gd" ;; esac
105
+ case "$_wp_cd" in /*) : ;; *) _wp_cd="$WP_CWD/$_wp_cd" ;; esac
106
+ # The primary clone's root is the parent of the SHARED git dir — declining any layout whose shared
107
+ # dir is not named .git (a bare repo, --separate-git-dir), same test as primaryRoot().
108
+ _wp_primary="$WP_CWD"
109
+ case "$_wp_cd" in
110
+ */.git) [ -d "${_wp_cd%/*}" ] && _wp_primary="${_wp_cd%/*}" ;;
111
+ esac
112
+ if [ "$_wp_gd" = "$_wp_cd" ]; then
113
+ WP_TREE=primary
114
+ WP_LOG_DIR="$_wp_primary/.webpieces/logs"
115
+ else
116
+ # git's OWN name for the worktree (the basename of <primary>/.git/worktrees/<name>), not the
117
+ # directory's basename — two worktrees under different parents may share a directory name.
118
+ WP_TREE="${_wp_gd##*/}"
119
+ WP_LOG_DIR="$_wp_primary/.webpieces/worktrees/$WP_TREE/logs"
120
+ fi
121
+ }
122
+ wp_log() { # $1 = L0 fault code (D|X|K|-), $2 = verdict label
123
+ {
124
+ [ -n "$WP_LOG_DIR" ] || wp_resolve_log_dir
125
+ mkdir -p "$WP_LOG_DIR" 2>/dev/null || return 0
126
+ _wp_f="$WP_LOG_DIR/ai-hook-shim.log"
127
+ # Rotate at the SAME 512 KB into the SAME .1.log sibling as every JS-side webpieces log. This runs
128
+ # on every tool call, so it is one wc and no more; a size we cannot read counts as 0 (no rotation).
129
+ _wp_sz="$(wc -c < "$_wp_f" 2>/dev/null | tr -d ' ')"
130
+ case "$_wp_sz" in ''|*[!0-9]*) _wp_sz=0 ;; esac
131
+ [ "$_wp_sz" -gt 524288 ] && mv -f "$_wp_f" "$WP_LOG_DIR/ai-hook-shim.1.log" 2>/dev/null
132
+ printf '%s\t%s\t%s\t%s\t%s\t%s\t%s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z' 2>/dev/null)" "$BIN_NAME" "$TOOL" "tree=$WP_TREE" "fault=$1" "$2" "$CMD" >> "$_wp_f"
133
+ } 2>/dev/null || true
134
+ }
83
135
  BROKEN_BIN=""
84
136
  CRASH_MSG=""
85
137
  if [ -x "$BIN" ] && [ -z "$DRIFT_PKG" ]; then
@@ -91,6 +143,14 @@ if [ -x "$BIN" ] && [ -z "$DRIFT_PKG" ]; then
91
143
  cat "$OUT_FILE" # the guard's real decision — verbatim
92
144
  cat "$ERR_FILE" >&2
93
145
  rm -f "$OUT_FILE" "$ERR_FILE" 2>/dev/null
146
+ # THE HEALTHY CALL, which this log used to be silent about (see WP_LOG_SH's header). No sh-side
147
+ # fault, the bin ran, and its own exit code says how it ended — 0 allow, 2 block. Logged AFTER the
148
+ # decision bytes are already on stdout, so the audit trail never sits between the guard and Claude
149
+ # Code. Without this line, "no entry" meant either healthy or never-ran, and those are the two
150
+ # answers a reader most needs to tell apart.
151
+ WP_VERDICT=PASS-BIN-ALLOW
152
+ [ "$RC" = 2 ] && WP_VERDICT=PASS-BIN-BLOCK
153
+ wp_log - "$WP_VERDICT"
94
154
  exit "$RC"
95
155
  fi
96
156
  # Crashed. Keep the most useful stderr line for the human. Strip " and backslash so the text stays a
@@ -107,18 +167,11 @@ fi
107
167
  # through: the assistant's own Bash tool routes through this hook too, so blocking everything would
108
168
  # deadlock the very commands (pnpm install / rm -rf node_modules && pnpm install) that re-enable the
109
169
  # guards. A silent exit 0 = "allow" in the PreToolUse protocol; the guards resume once the tree is sane.
110
- CMD="$(printf '%s' "$PAYLOAD" | sed -n 's/.*"command"[[:space:]]*:[[:space:]]*"\([^"\\]*\)".*/\1/p')"
111
- TOOL="$(printf '%s' "$PAYLOAD" | sed -n 's/.*"tool_name"[[:space:]]*:[[:space:]]*"\([^"\\]*\)".*/\1/p')"
112
- FILE="$(printf '%s' "$PAYLOAD" | sed -n 's/.*"file_path"[[:space:]]*:[[:space:]]*"\([^"\\]*\)".*/\1/p')"
113
- # Best-effort audit trail of every decision the fail-closed shim makes WHILE THE GUARDS ARE DOWN, so a
114
- # human can inspect after something odd (an install that was denied, or one that slipped through). One
115
- # tab-separated line per call → <root>/.webpieces/logs/ai-hook-shim.log (gitignored). NEVER breaks or
116
- # blocks the hook: all writes are best-effort (|| true) and go to a file, never to stdout (stdout is
117
- # the PreToolUse decision channel — a stray byte there would corrupt allow/deny).
118
- LOG_DIR="$ROOT/.webpieces/logs"
119
- wp_log() { # $1 = label (ALLOW-CURE|ALLOW-READ|ALLOW-CONFIG|DENY|DENY-STALE|DENY-BROKEN)
120
- { mkdir -p "$LOG_DIR" 2>/dev/null && printf '%s\t%s\t%s\t%s\t%s\n' "$(date '+%Y-%m-%dT%H:%M:%S%z' 2>/dev/null)" "$BIN_NAME" "$TOOL" "$1" "$CMD" >> "$LOG_DIR/ai-hook-shim.log"; } 2>/dev/null || true
121
- }
170
+ # WHICH of the six guards/L0-tooling.md faults fired, in the doc's own letters. Only the three sh-side
171
+ # codes can be decided here; S/C/Y live in the binary, which never got to run on this path.
172
+ WP_FAULT=X # X bin missing (fresh clone, new worktree)
173
+ [ -n "$DRIFT_PKG" ] && WP_FAULT=D # D version drift; D and K are mutually exclusive
174
+ [ -n "$BROKEN_BIN" ] && WP_FAULT=K # K bin present but CRASHED (corrupt node_modules)
122
175
  DENY_LABEL="DENY"
123
176
  [ -n "$DRIFT_PKG" ] && DENY_LABEL="DENY-STALE" # version drift, not a missing bin
124
177
  [ -n "$BROKEN_BIN" ] && DENY_LABEL="DENY-BROKEN" # bin present but CRASHED (corrupt node_modules)
@@ -126,19 +179,19 @@ DENY_LABEL="DENY"
126
179
  # help a given fault also cannot hurt it, and gating each entry on a fault is what produced the four
127
180
  # defects recorded above L0_ALLOW_ERE.
128
181
  if [ "$TOOL" = "Read" ]; then
129
- wp_log ALLOW-READ # you must be able to read to work out how to fix this
182
+ wp_log "$WP_FAULT" ALLOW-READ # you must be able to read to work out how to fix this
130
183
  exit 0
131
184
  fi
132
185
  case "$FILE" in
133
186
  */webpieces.config.json|webpieces.config.json)
134
- wp_log ALLOW-CONFIG # the always-allowed recovery target — every guard is configured from it
187
+ wp_log "$WP_FAULT" ALLOW-CONFIG # the always-allowed recovery target — every guard is configured from it
135
188
  exit 0 ;;
136
189
  esac
137
190
  if printf '%s' "$CMD" | grep -Eq '^(cd[[:space:]]+([A-Za-z0-9._/@~+-]+|'\''[^'\'']+'\'')[[:space:]]*&&[[:space:]]*)?((pnpm|npm)[[:space:]]+(install|i)([[:space:]]+--[A-Za-z][A-Za-z0-9=._/@:-]*)*|rm[[:space:]]+-rf[[:space:]]+(\./)?node_modules/?([[:space:]]*&&[[:space:]]*(pnpm|npm)[[:space:]]+(install|i)([[:space:]]+--[A-Za-z][A-Za-z0-9=._/@:-]*)*)?|git[[:space:]]+(pull|fetch)([[:space:]]+(--)?[A-Za-z0-9][A-Za-z0-9=._/@:-]*)*|(pnpm|npm|npx)([[:space:]]+(exec|run))?[[:space:]]+wp-upgrade-shim|cp[[:space:]]+(\./)?node_modules/@webpieces/ai-hook-rules/templates/ai-hook\.sh[[:space:]]+(\./)?\.claude/webpieces/ai-hook\.sh|(pnpm|npm|npx)([[:space:]]+(exec|run))?[[:space:]]+wp-install-ai-hooks([[:space:]]+--[A-Za-z][A-Za-z0-9=._/@:-]*)*|(pwd|git[[:space:]]+(status|log|diff|show|branch|rev-parse)|git[[:space:]]+worktree[[:space:]]+list)([[:space:]]+(--)?[A-Za-z0-9][A-Za-z0-9=._/@:-]*)*)([[:space:]]+2>(&1|/dev/null))?([[:space:]]*\|[[:space:]]*(tail|head)([[:space:]]+-(n[[:space:]]+)?[0-9]+)?)?[[:space:]]*$'; then
138
- wp_log ALLOW-CURE # record the self-heal we let through (re-enables the guards)
191
+ wp_log "$WP_FAULT" ALLOW-CURE # record the self-heal we let through (re-enables the guards)
139
192
  exit 0 # allow the cure so the assistant can break the deadlock
140
193
  fi
141
- wp_log "$DENY_LABEL" # every fail-closed block (…-STALE = drift, …-BROKEN = crash) for inspection
194
+ wp_log "$WP_FAULT" "$DENY_LABEL" # every fail-closed block, with the fault that caused it
142
195
  if [ -n "$BROKEN_BIN" ]; then
143
196
  # Report (do NOT auto-clean) the orphaned pnpm staging dirs — a package pnpm was mid-way through
144
197
  # writing is left behind as <name>_<pid>_<hash>. Their presence is the fingerprint of an install that