upload current sources

This commit is contained in:
2026-09-16 01:47:28 -07:00
commit f8b45ea4a5
109 changed files with 26012 additions and 0 deletions
+153
View File
@@ -0,0 +1,153 @@
#!/usr/bin/env bash
# PreToolUse consultation gate (ADR-003).
#
# Matched tools (see .claude/settings.json): Write, Edit, MultiEdit,
# NotebookEdit, Bash, mcp__filesystem__{write_file,edit_file,move_file}.
# Denies writes to SAP artifacts unless a grounding consultation (one of the
# SAP/UI5/context7 MCP servers, WebSearch, or WebFetch) was recorded in the
# last 30 minutes by mark-consult.sh.
#
# Always exits 0. A deny is expressed ONLY via hookSpecificOutput JSON on
# stdout (permissionDecision: "deny"); silence means "no opinion".
#
# FAIL-CLOSED RULES
# * jq missing -> deny (cannot parse the tool input at all).
# * marker unresolvable -> deny, but only once a gated path is involved.
# * marker missing/stale -> deny for gated paths.
#
# GATED PATHS (after exemptions):
# *.cds *.abap *.asddls *.acds *.bdef *.ddls *.view.xml *.fragment.xml
# */srv/* */app/* basename manifest.json or annotation*.xml
# EXEMPT: *.md */.claude/* */docs/*
# NOT gated: */worker/* (TypeScript worker is governed by context7, not this gate).
#
# BASH HEURISTIC (best effort, documented limits):
# A Bash command is gated only when it BOTH (a) contains a write indicator
# (> or >> redirect, heredoc <<, sed -i/--in-place, tee, cp, mv, install,
# rsync, rm, ln, touch, truncate, dd, patch, or python/node/perl/ruby with
# -c/-e/-i) AND (b) mentions a gated path/extension as a whitespace token.
# Redirects to /dev/null and fd duplications (2>&1) are ignored.
# Limits: tokens are split on whitespace and shell punctuation, so paths
# inside quotes with spaces, paths built from variables ($DIR/x), globs,
# `cd app && ...` relative paths, and writes performed by scripts invoked by
# name are NOT detected. sed expressions that happen to contain "/app/" or
# "/srv/" can cause a false deny when combined with a write indicator.
# Read-only commands (cat, grep, sed -n, head, ls, find) pass.
#
# ENV OVERRIDES
# SAP_CONSULT_MARKER path to the marker file (tests only).
# CLAUDE_PROJECT_DIR set by Claude Code; preferred over the $0-relative
# fallback for locating .claude/.last-mcp-consult.
deny() {
# $1 must not contain double quotes or backslashes (kept JSON-safe by hand
# so that a deny still works when jq itself is unavailable).
printf '{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"%s"}}\n' "$1"
exit 0
}
# --- Dependency check FIRST, using builtins only (fail closed). --------------
if ! command -v jq >/dev/null 2>&1; then
deny "Ground-truth gate: required dependency jq is not on PATH, so the tool input cannot be inspected. Install jq (or fix PATH) and retry."
fi
INPUT="$(cat)"
TOOL="$(printf '%s' "$INPUT" | jq -r '.tool_name // empty' 2>/dev/null)"
# is_gated PATH -> 0 if the path is a gated SAP artifact, 1 otherwise.
is_gated() {
local p="$1" norm base
[ -z "$p" ] && return 1
norm="/${p#/}" # relative paths get a leading slash so */app/* matches
base="${p##*/}"
case "$norm" in
*.md|*/.claude/*|*/docs/*) return 1 ;;
esac
case "$norm" in
*.cds|*.abap|*.asddls|*.acds|*.bdef|*.ddls) return 0 ;;
*.view.xml|*.fragment.xml) return 0 ;;
*/srv/*|*/app/*) return 0 ;;
esac
case "$base" in
manifest.json|annotation*.xml) return 0 ;;
esac
return 1
}
GATED=0
GATED_HINT=""
if [ "$TOOL" = "Bash" ]; then
CMD="$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null)"
[ -z "$CMD" ] && exit 0
# Drop redirects that never write a file: fd dups (2>&1, >&2) and /dev/null.
SAN="$(printf '%s' "$CMD" \
| sed -E 's/[0-9]*>&[0-9]+//g; s/[&0-9]*>>?[[:space:]]*\/dev\/null//g')"
WRITE_RE='(>|<<|(^|[[:space:];&|(])(tee|cp|mv|install|rsync|rm|ln|touch|truncate|dd|patch)([[:space:]]|$)|(^|[[:space:];&|(])sed([[:space:]]+[^[:space:]]+)*[[:space:]]+(-[[:alnum:]]*i[[:alnum:]]*|--in-place)([[:space:]=]|$)|(^|[[:space:];&|(])(python[0-9.]*|node|perl|ruby)([[:space:]]+[^[:space:]]+)*[[:space:]]+-[[:alnum:]]*[cei]([[:space:]]|$))'
if ! printf '%s' "$SAN" | grep -qE "$WRITE_RE"; then
exit 0
fi
# Tokenise on whitespace and shell punctuation; test every token as a path.
TOKENS="$(printf '%s' "$CMD" | tr ';|&()<>"'"'"'`=,' ' ' | tr '\n' ' ')"
for tok in $TOKENS; do
if is_gated "$tok"; then
GATED=1
GATED_HINT="$tok"
break
fi
done
else
PATHS="$(printf '%s' "$INPUT" | jq -r '
.tool_input as $t
| [ $t.file_path, $t.notebook_path, $t.path, $t.destination, $t.source ]
| map(select(type == "string" and length > 0))
| .[]' 2>/dev/null)"
[ -z "$PATHS" ] && exit 0
while IFS= read -r p; do
if is_gated "$p"; then
GATED=1
GATED_HINT="$p"
break
fi
done <<EOP
$PATHS
EOP
fi
[ "$GATED" -eq 0 ] && exit 0
# --- Resolve marker (fail closed if impossible). ------------------------------
if [ -n "${SAP_CONSULT_MARKER:-}" ]; then
MARKER="$SAP_CONSULT_MARKER"
else
if [ -n "${CLAUDE_PROJECT_DIR:-}" ] && [ -d "$CLAUDE_PROJECT_DIR" ]; then
REPO="$CLAUDE_PROJECT_DIR"
else
REPO="$(cd "$(dirname "$0")/../.." 2>/dev/null && pwd)" || REPO=""
fi
if [ -z "$REPO" ]; then
deny "Ground-truth gate: cannot resolve the repository root (CLAUDE_PROJECT_DIR unset and script location unreadable), so the consultation marker cannot be checked. Failing closed."
fi
MARKER="$REPO/.claude/.last-mcp-consult"
fi
# --- Freshness check: marker younger than 30 minutes satisfies the gate. -----
if [ -f "$MARKER" ]; then
LAST="$(cat "$MARKER" 2>/dev/null)"
NOW="$(date +%s)"
case "$LAST" in
''|*[!0-9]*) : ;; # not a number -> treat as stale
*)
if [ $((NOW - LAST)) -lt 1800 ]; then
exit 0
fi
;;
esac
fi
# Keep the hint JSON-safe (strip quotes/backslashes, truncate).
SAFE_HINT="$(printf '%s' "$GATED_HINT" | tr -d '"\\' | cut -c1-160)"
deny "Ground-truth gate: no grounding consultation recorded in the last 30 minutes, and this touches a SAP artifact ($SAFE_HINT). Consult the relevant server first (CDS/RAP/ABAP -> abap-docs or sap-docs; Fiori/annotations -> fiori-mcp-server; UI5 -> ui5-mcp-server; libraries -> context7). Only calls to those servers, WebSearch, or WebFetch refresh the gate; then retry."