upload current sources
This commit is contained in:
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/usr/bin/env bash
|
||||
# PostToolUse hook for the grounding servers (sap-docs, abap-docs,
|
||||
# sap-released-objects, fiori/ui5 servers, context7, capire) and
|
||||
# WebSearch/WebFetch: records the time of the last successful consultation.
|
||||
# The matcher in .claude/settings.json decides WHICH tools count; this script
|
||||
# only stamps the marker. Drains stdin so the pipe never breaks. Always exits 0.
|
||||
#
|
||||
# Marker location, in priority order:
|
||||
# 1. $SAP_CONSULT_MARKER (test/override hook; never set in normal use)
|
||||
# 2. $CLAUDE_PROJECT_DIR/.claude/.last-mcp-consult (set by Claude Code)
|
||||
# 3. <this script>/../../.claude/.last-mcp-consult (fallback)
|
||||
|
||||
cat > /dev/null
|
||||
|
||||
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
|
||||
[ -z "$REPO" ] && exit 0
|
||||
MARKER="$REPO/.claude/.last-mcp-consult"
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$MARKER")" 2>/dev/null
|
||||
date +%s > "$MARKER" 2>/dev/null
|
||||
|
||||
exit 0
|
||||
Executable
+153
@@ -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."
|
||||
Executable
+32
@@ -0,0 +1,32 @@
|
||||
#!/usr/bin/env bash
|
||||
# Prints ground-truth grounding context for SessionStart ("session") or
|
||||
# UserPromptSubmit ("prompt"). Stdout becomes model context. Always exits 0.
|
||||
|
||||
case "${1:-}" in
|
||||
session)
|
||||
cat <<'EOF'
|
||||
[SAP GROUND-TRUTH CHARTER — this repo enforces MCP-first grounding]
|
||||
Forget what you think you know about SAP, CAP, RAP, ABAP Cloud, CDS, Fiori, and UI5
|
||||
APIs — training knowledge is stale by default and is NOT a citable source here.
|
||||
Authoritative sources, in order: this repo's MCP servers (abap-docs, sap-docs,
|
||||
sap-released-objects, fiori-mcp-server, ui5-mcp-server), context7 for non-SAP
|
||||
libraries/SDKs, live system checks. If MCP tools show as deferred, load them via
|
||||
ToolSearch as your FIRST action — do not defer the load until "needed".
|
||||
Every SAP behavioral claim you record in PRD/ADR/KNOWLEDGE must carry a citation:
|
||||
[source: <server> <doc-id-or-URL>, verified YYYY-MM-DD]. Uncited = hypothesis.
|
||||
A consultation gate is active: writes to SAP artifacts (Write/Edit/Bash/filesystem MCP) are DENIED unless a
|
||||
grounding-server (SAP/UI5/context7 MCP), WebSearch, or WebFetch consultation happened in the last 30 minutes.
|
||||
EOF
|
||||
;;
|
||||
prompt)
|
||||
cat <<'EOF'
|
||||
[SAP-REFLEX] Do not classify whether this prompt "needs" an MCP consult — that
|
||||
classification is the step that fails. CDS/RAP/ABAP/Fiori/UI5/annotation/OData
|
||||
work ⇒ consult abap-docs/sap-docs/fiori-mcp-server/ui5-mcp-server BEFORE writing
|
||||
anything. Library/SDK questions ⇒ context7. Record SAP claims only with
|
||||
[source: <server> <id/URL>, verified YYYY-MM-DD].
|
||||
EOF
|
||||
;;
|
||||
esac
|
||||
|
||||
exit 0
|
||||
Executable
+118
@@ -0,0 +1,118 @@
|
||||
#!/usr/bin/env bash
|
||||
# Test harness for the ADR-003 consultation gate hooks.
|
||||
# Pipes simulated Claude Code hook JSON into sap-consult-gate.sh and
|
||||
# mark-consult.sh with a temp marker (SAP_CONSULT_MARKER) so the real
|
||||
# .claude/.last-mcp-consult is never touched.
|
||||
#
|
||||
# Usage: bash .claude/hooks/tests/run.sh (exit 0 = all pass)
|
||||
|
||||
set -u
|
||||
|
||||
HERE="$(cd "$(dirname "$0")" && pwd)"
|
||||
HOOKS="$(dirname "$HERE")"
|
||||
REPO="$(cd "$HOOKS/../.." && pwd)"
|
||||
GATE="$HOOKS/sap-consult-gate.sh"
|
||||
MARK="$HOOKS/mark-consult.sh"
|
||||
|
||||
TMP="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
STALE="$TMP/stale-marker"
|
||||
FRESH="$TMP/fresh-marker"
|
||||
echo $(( $(date +%s) - 7200 )) > "$STALE"
|
||||
date +%s > "$FRESH"
|
||||
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
# gate MARKER JSON -> prints "deny" or "allow"; also asserts exit 0 + valid JSON.
|
||||
gate() {
|
||||
local marker="$1" json="$2" out rc
|
||||
out="$(printf '%s' "$json" | SAP_CONSULT_MARKER="$marker" CLAUDE_PROJECT_DIR="$REPO" bash "$GATE")"
|
||||
rc=$?
|
||||
if [ "$rc" -ne 0 ]; then echo "rc=$rc"; return; fi
|
||||
if [ -n "$out" ] && ! printf '%s' "$out" | jq -e . >/dev/null 2>&1; then echo "badjson"; return; fi
|
||||
if printf '%s' "$out" | jq -e '.hookSpecificOutput.permissionDecision == "deny"' >/dev/null 2>&1; then
|
||||
echo deny
|
||||
else
|
||||
echo allow
|
||||
fi
|
||||
}
|
||||
|
||||
expect() {
|
||||
local name="$1" want="$2" got="$3"
|
||||
if [ "$want" = "$got" ]; then
|
||||
PASS=$((PASS + 1)); echo "PASS $name [$got]"
|
||||
else
|
||||
FAIL=$((FAIL + 1)); echo "FAIL $name [want $want, got $got]"
|
||||
fi
|
||||
}
|
||||
|
||||
j_edit() { printf '{"tool_name":"Edit","tool_input":{"file_path":"%s","old_string":"a","new_string":"b"}}' "$1"; }
|
||||
j_bash() { printf '{"tool_name":"Bash","tool_input":{"command":%s}}' "$(printf '%s' "$1" | jq -Rs .)"; }
|
||||
j_fswr() { printf '{"tool_name":"mcp__filesystem__write_file","tool_input":{"path":"%s","content":"x"}}' "$1"; }
|
||||
j_fsmv() { printf '{"tool_name":"mcp__filesystem__move_file","tool_input":{"source":"%s","destination":"%s"}}' "$1" "$2"; }
|
||||
j_nb() { printf '{"tool_name":"NotebookEdit","tool_input":{"notebook_path":"%s","new_source":"x"}}' "$1"; }
|
||||
|
||||
MANIFEST="$REPO/app/webapp/manifest.json"
|
||||
ANNOT="$REPO/app/webapp/annotations/annotation.xml"
|
||||
|
||||
echo "== sap-consult-gate.sh =="
|
||||
# --- Cases required by the brief ---------------------------------------------
|
||||
expect "Edit manifest.json, stale marker => deny" deny "$(gate "$STALE" "$(j_edit "$MANIFEST")")"
|
||||
expect "Edit manifest.json, fresh marker => allow" allow "$(gate "$FRESH" "$(j_edit "$MANIFEST")")"
|
||||
expect "Bash sed -i manifest.json, stale => deny" deny "$(gate "$STALE" "$(j_bash "sed -i 's/a/b/' app/webapp/manifest.json")")"
|
||||
expect "Bash cat manifest.json, stale => allow" allow "$(gate "$STALE" "$(j_bash "cat app/webapp/manifest.json")")"
|
||||
expect "Edit worker/src/x.ts, stale => allow" allow "$(gate "$STALE" "$(j_edit "$REPO/worker/src/x.ts")")"
|
||||
expect "Edit PRD.md, stale => allow" allow "$(gate "$STALE" "$(j_edit "$REPO/PRD.md")")"
|
||||
expect "fs write_file annotation.xml, stale => deny" deny "$(gate "$STALE" "$(j_fswr "$ANNOT")")"
|
||||
|
||||
# missing jq: empty PATH, fresh marker, gated file -> must still deny, naming jq.
|
||||
out="$(printf '%s' "$(j_edit "$MANIFEST")" | env -i PATH=/nonexistent SAP_CONSULT_MARKER="$FRESH" /bin/bash "$GATE")"; rc=$?
|
||||
if [ "$rc" -eq 0 ] && printf '%s' "$out" | jq -e '.hookSpecificOutput.permissionDecision == "deny" and (.hookSpecificOutput.permissionDecisionReason | test("jq"))' >/dev/null 2>&1; then
|
||||
got=deny; else got="allow(rc=$rc)"; fi
|
||||
expect "missing jq (PATH stripped) => deny naming jq" deny "$got"
|
||||
|
||||
# --- Additional coverage --------------------------------------------------------
|
||||
expect "Bash sed -i manifest.json, fresh => allow" allow "$(gate "$FRESH" "$(j_bash "sed -i 's/a/b/' app/webapp/manifest.json")")"
|
||||
expect "Bash sed -n (read-only) on manifest, stale => allow" allow "$(gate "$STALE" "$(j_bash "sed -n '1,20p' app/webapp/manifest.json")")"
|
||||
expect "Bash grep on app/ path, stale => allow" allow "$(gate "$STALE" "$(j_bash "grep -rn foo app/webapp/")")"
|
||||
expect "Bash cat with 2>/dev/null, stale => allow" allow "$(gate "$STALE" "$(j_bash "cat app/webapp/manifest.json 2>/dev/null")")"
|
||||
expect "Bash cat 2>&1 | head, stale => allow" allow "$(gate "$STALE" "$(j_bash "cat app/webapp/manifest.json 2>&1 | head -5")")"
|
||||
expect "Bash heredoc > .cds, stale => deny" deny "$(gate "$STALE" "$(j_bash "cat > srv/schema.cds <<'X'
|
||||
entity A {}
|
||||
X")")"
|
||||
expect "Bash echo >> manifest.json, stale => deny" deny "$(gate "$STALE" "$(j_bash "echo '{}' >> app/webapp/manifest.json")")"
|
||||
expect "Bash tee annotation.xml, stale => deny" deny "$(gate "$STALE" "$(j_bash "printf x | tee app/webapp/annotations/annotation.xml")")"
|
||||
expect "Bash cp into app/, stale => deny" deny "$(gate "$STALE" "$(j_bash "cp /tmp/x.json app/webapp/manifest.json")")"
|
||||
expect "Bash mv within app/, stale => deny" deny "$(gate "$STALE" "$(j_bash "mv app/webapp/a.xml app/webapp/b.xml")")"
|
||||
expect "Bash python -c writing .abap, stale => deny" deny "$(gate "$STALE" "$(j_bash "python3 -c 'open(\"src/zcl_x.clas.abap\",\"w\").write(\"x\")'")")"
|
||||
expect "Bash node -e writing manifest, stale => deny" deny "$(gate "$STALE" "$(j_bash "node -e 'require(\"fs\").writeFileSync(\"app/webapp/manifest.json\",\"{}\")'")")"
|
||||
expect "Bash echo > worker file, stale => allow" allow "$(gate "$STALE" "$(j_bash "echo x > worker/src/x.ts")")"
|
||||
expect "Bash echo > docs file, stale => allow" allow "$(gate "$STALE" "$(j_bash "echo x > docs/adr/ADR-009.md")")"
|
||||
expect "Bash npm test (no write), stale => allow" allow "$(gate "$STALE" "$(j_bash "cd worker && npm test")")"
|
||||
expect "Edit .bdef, stale => deny" deny "$(gate "$STALE" "$(j_edit "$REPO/src/zi_x.bdef")")"
|
||||
expect "Edit fragment.xml, stale => deny" deny "$(gate "$STALE" "$(j_edit "$REPO/app/webapp/ext/X.fragment.xml")")"
|
||||
expect "Edit app/README.md (exempt ext), stale => allow" allow "$(gate "$STALE" "$(j_edit "$REPO/app/README.md")")"
|
||||
expect "Edit .claude/hooks file, stale => allow" allow "$(gate "$STALE" "$(j_edit "$REPO/.claude/hooks/x.sh")")"
|
||||
expect "NotebookEdit under app/, stale => deny" deny "$(gate "$STALE" "$(j_nb "$REPO/app/x.ipynb")")"
|
||||
expect "fs move_file src gated, stale => deny" deny "$(gate "$STALE" "$(j_fsmv "$MANIFEST" "/tmp/m.json")")"
|
||||
expect "fs move_file dest gated, stale => deny" deny "$(gate "$STALE" "$(j_fsmv "/tmp/m.json" "$MANIFEST")")"
|
||||
expect "Edit with no path field => allow" allow "$(gate "$STALE" '{"tool_name":"Edit","tool_input":{}}')"
|
||||
expect "Corrupt marker content => deny" deny "$(printf 'garbage' > "$TMP/bad"; gate "$TMP/bad" "$(j_edit "$MANIFEST")")"
|
||||
expect "Missing marker file => deny" deny "$(gate "$TMP/does-not-exist" "$(j_edit "$MANIFEST")")"
|
||||
|
||||
echo "== mark-consult.sh =="
|
||||
M="$TMP/marked"
|
||||
printf '{"tool_name":"mcp__sap-docs__search","tool_response":{}}' | SAP_CONSULT_MARKER="$M" CLAUDE_PROJECT_DIR="$REPO" bash "$MARK"; rc=$?
|
||||
expect "mark-consult exits 0" 0 "$rc"
|
||||
now=$(date +%s); v="$(cat "$M" 2>/dev/null || echo none)"
|
||||
case "$v" in *[!0-9]*|'') got=bad ;; *) [ $((now - v)) -le 5 ] && got=fresh || got=old ;; esac
|
||||
expect "mark-consult writes epoch to override marker" fresh "$got"
|
||||
expect "gate allows after mark-consult stamped marker" allow "$(gate "$M" "$(j_edit "$MANIFEST")")"
|
||||
[ -f "$REPO/.claude/.last-mcp-consult" ] && before="$(cat "$REPO/.claude/.last-mcp-consult")" || before="absent"
|
||||
[ -f "$REPO/.claude/.last-mcp-consult" ] && after="$(cat "$REPO/.claude/.last-mcp-consult")" || after="absent"
|
||||
expect "real marker untouched by harness" "$before" "$after"
|
||||
|
||||
echo
|
||||
echo "passed=$PASS failed=$FAIL"
|
||||
[ "$FAIL" -eq 0 ]
|
||||
Reference in New Issue
Block a user