ca1bf5a59b
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
244 lines
10 KiB
Bash
Executable File
244 lines
10 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# PostToolUse hook: registra cada invocacion del agente en
|
|
# projects/fn_monitoring/apps/call_monitor/operations.db (issue 0085b).
|
|
#
|
|
# Identifica tool, extrae function_id cuando es posible, clasifica el patron
|
|
# (mcp_*, fn_cli_run, heredoc_py, sqlite_direct, edit_registry, ...) y
|
|
# detecta antipatrones para registrar violations.
|
|
#
|
|
# NUNCA bloquea la herramienta. Falla silenciosamente si la BD no esta lista.
|
|
# Solo guarda args_hash, jamas valores concretos.
|
|
|
|
set -euo pipefail
|
|
|
|
# ---- Resolve registry root (walks up from cwd looking for registry.db) ----
|
|
resolve_root() {
|
|
local d="${PWD}"
|
|
while [ "$d" != "/" ]; do
|
|
if [ -f "$d/registry.db" ]; then
|
|
printf '%s' "$d"
|
|
return 0
|
|
fi
|
|
d=$(dirname "$d")
|
|
done
|
|
return 1
|
|
}
|
|
|
|
ROOT=$(resolve_root) || exit 0
|
|
DB="$ROOT/projects/fn_monitoring/apps/call_monitor/operations.db"
|
|
|
|
# Si la BD aun no existe, el hook no hace nada (esperando init).
|
|
[ -f "$DB" ] || exit 0
|
|
|
|
# ---- Read stdin JSON ----
|
|
INPUT=$(cat)
|
|
if [ -z "$INPUT" ]; then exit 0; fi
|
|
|
|
# Required jq presence
|
|
command -v jq >/dev/null 2>&1 || exit 0
|
|
command -v sqlite3 >/dev/null 2>&1 || exit 0
|
|
|
|
TOOL_NAME=$(printf '%s' "$INPUT" | jq -r '.tool_name // ""')
|
|
SESSION_ID=$(printf '%s' "$INPUT" | jq -r '.session_id // ""')
|
|
TS=$(date -u +%s)
|
|
|
|
# Tool response success/error
|
|
SUCCESS=1
|
|
ERROR_CLASS=""
|
|
ERROR_SNIPPET=""
|
|
RESP_IS_ERROR=$(printf '%s' "$INPUT" | jq -r 'if (.tool_response | type) == "object" then (.tool_response.is_error // false) else false end')
|
|
if [ "$RESP_IS_ERROR" = "true" ]; then
|
|
SUCCESS=0
|
|
ERROR_SNIPPET=$(printf '%s' "$INPUT" | jq -r 'if (.tool_response | type) == "object" then (.tool_response.error // .tool_response.content // "") else "" end' | head -c 240 | tr '\n' ' ')
|
|
fi
|
|
|
|
# args_hash: sha256 truncado del tool_input (sin valores)
|
|
ARGS_HASH=$(printf '%s' "$INPUT" | jq -c '.tool_input // {}' | sha256sum | cut -c1-16)
|
|
|
|
# Helpers SQL
|
|
sql_escape() { printf '%s' "$1" | sed "s/'/''/g"; }
|
|
|
|
insert_call() {
|
|
local fn_id="$1" tool_used="$2" duration_ms="${3:-0}" snippet="${4:-}"
|
|
local fn_esc tu_esc ec_esc es_esc sid_esc ah_esc snip_esc
|
|
# Politica issue 0087: command_snippet solo se rellena cuando function_id
|
|
# esta vacio. Si la call golpea una funcion del registry, su ID y
|
|
# tool_used bastan; no duplicamos el comando.
|
|
if [ -n "$fn_id" ]; then snippet=""; fi
|
|
# Redact common secrets antes de persistir
|
|
snippet=$(printf '%s' "$snippet" \
|
|
| sed -E 's/(password|token|secret|api[_-]?key|bearer)([[:space:]]*[=:][[:space:]]*)[^[:space:]]+/\1\2<REDACTED>/Ig' \
|
|
| head -c 200)
|
|
fn_esc=$(sql_escape "$fn_id")
|
|
tu_esc=$(sql_escape "$tool_used")
|
|
ec_esc=$(sql_escape "$ERROR_CLASS")
|
|
es_esc=$(sql_escape "$ERROR_SNIPPET")
|
|
sid_esc=$(sql_escape "$SESSION_ID")
|
|
ah_esc=$(sql_escape "$ARGS_HASH")
|
|
snip_esc=$(sql_escape "$snippet")
|
|
sqlite3 "$DB" "INSERT INTO calls (session_id, function_id, tool_used, args_hash, duration_ms, success, error_class, error_snippet, command_snippet, ts) VALUES ('$sid_esc','$fn_esc','$tu_esc','$ah_esc',$duration_ms,$SUCCESS,'$ec_esc','$es_esc','$snip_esc',$TS);" 2>/dev/null || true
|
|
}
|
|
|
|
insert_code_write() {
|
|
local fn_id="$1" file_path="$2" added="${3:-0}" removed="${4:-0}"
|
|
local fn_esc fp_esc sid_esc
|
|
fn_esc=$(sql_escape "$fn_id")
|
|
fp_esc=$(sql_escape "$file_path")
|
|
sid_esc=$(sql_escape "$SESSION_ID")
|
|
sqlite3 "$DB" "INSERT INTO code_writes (session_id, function_id, file_path, lines_added, lines_removed, ts) VALUES ('$sid_esc','$fn_esc','$fp_esc',$added,$removed,$TS);" 2>/dev/null || true
|
|
}
|
|
|
|
# Snapshot a function version row when an edit lands on a registry file.
|
|
# Uses sha256 of file bytes as content_hash (separate namespace from index source).
|
|
insert_edit_version() {
|
|
local fn_id="$1" abs_path="$2"
|
|
[ -f "$abs_path" ] || return 0
|
|
command -v sha256sum >/dev/null 2>&1 || return 0
|
|
local hash
|
|
hash=$(sha256sum "$abs_path" 2>/dev/null | awk '{print $1}')
|
|
[ -z "$hash" ] && return 0
|
|
local fn_esc h_esc
|
|
fn_esc=$(sql_escape "$fn_id")
|
|
h_esc=$(sql_escape "$hash")
|
|
sqlite3 "$DB" "INSERT OR IGNORE INTO function_versions (function_id, content_hash, version, snapped_at, source, lines_added, lines_removed) VALUES ('$fn_esc','$h_esc','',$TS,'edit_hook',0,0);" 2>/dev/null || true
|
|
}
|
|
|
|
insert_violation() {
|
|
local rule_id="$1" fn_id="$2" snippet="$3" severity="${4:-warning}"
|
|
local r_esc fn_esc sn_esc sev_esc sid_esc
|
|
r_esc=$(sql_escape "$rule_id")
|
|
fn_esc=$(sql_escape "$fn_id")
|
|
sn_esc=$(sql_escape "$(printf '%s' "$snippet" | head -c 240 | tr '\n' ' ')")
|
|
sev_esc=$(sql_escape "$severity")
|
|
sid_esc=$(sql_escape "$SESSION_ID")
|
|
sqlite3 "$DB" "INSERT INTO violations (session_id, rule_id, function_id, command_snippet, severity, ts) VALUES ('$sid_esc','$r_esc','$fn_esc','$sn_esc','$sev_esc',$TS);" 2>/dev/null || true
|
|
}
|
|
|
|
# ---- Derive function_id from registry file path ----
|
|
# Matches paths under functions/<domain>/<name>.<ext>, python/functions/<domain>/<name>.py,
|
|
# bash/functions/<domain>/<name>.sh, frontend/functions/<domain>/<name>.ts(x)
|
|
derive_fn_id_from_path() {
|
|
local p="$1"
|
|
[ -z "$p" ] && return 1
|
|
case "$p" in
|
|
functions/*/*.go|*/functions/*/*.go)
|
|
local dom name
|
|
dom=$(printf '%s' "$p" | sed -E 's|.*functions/([^/]+)/.*|\1|')
|
|
name=$(printf '%s' "$p" | sed -E 's|.*functions/[^/]+/([^/.]+)\..*|\1|')
|
|
[ -n "$dom" ] && [ -n "$name" ] && printf '%s_go_%s' "$name" "$dom" && return 0 ;;
|
|
python/functions/*/*.py)
|
|
local dom name
|
|
dom=$(printf '%s' "$p" | sed -E 's|python/functions/([^/]+)/.*|\1|')
|
|
name=$(printf '%s' "$p" | sed -E 's|python/functions/[^/]+/([^/.]+)\..*|\1|')
|
|
[ -n "$dom" ] && [ -n "$name" ] && printf '%s_py_%s' "$name" "$dom" && return 0 ;;
|
|
bash/functions/*/*.sh)
|
|
local dom name
|
|
dom=$(printf '%s' "$p" | sed -E 's|bash/functions/([^/]+)/.*|\1|')
|
|
name=$(printf '%s' "$p" | sed -E 's|bash/functions/[^/]+/([^/.]+)\..*|\1|')
|
|
[ -n "$dom" ] && [ -n "$name" ] && printf '%s_bash_%s' "$name" "$dom" && return 0 ;;
|
|
frontend/functions/*/*.ts|frontend/functions/*/*.tsx)
|
|
local dom name
|
|
dom=$(printf '%s' "$p" | sed -E 's|frontend/functions/([^/]+)/.*|\1|')
|
|
name=$(printf '%s' "$p" | sed -E 's|frontend/functions/[^/]+/([^/.]+)\..*|\1|')
|
|
[ -n "$dom" ] && [ -n "$name" ] && printf '%s_ts_%s' "$name" "$dom" && return 0 ;;
|
|
esac
|
|
return 1
|
|
}
|
|
|
|
# ---- Dispatch by tool ----
|
|
case "$TOOL_NAME" in
|
|
mcp__registry__fn_search)
|
|
insert_call "" "mcp_fn_search"
|
|
;;
|
|
mcp__registry__fn_show)
|
|
ID=$(printf '%s' "$INPUT" | jq -r '.tool_input.id // ""')
|
|
insert_call "$ID" "mcp_fn_show"
|
|
;;
|
|
mcp__registry__fn_code)
|
|
ID=$(printf '%s' "$INPUT" | jq -r '.tool_input.id // ""')
|
|
insert_call "$ID" "mcp_fn_code"
|
|
;;
|
|
mcp__registry__fn_uses)
|
|
ID=$(printf '%s' "$INPUT" | jq -r '.tool_input.id // ""')
|
|
insert_call "$ID" "mcp_fn_uses"
|
|
;;
|
|
mcp__registry__fn_run)
|
|
ID=$(printf '%s' "$INPUT" | jq -r '.tool_input.id // ""')
|
|
insert_call "$ID" "mcp_fn_run"
|
|
;;
|
|
mcp__registry__fn_list_domains)
|
|
insert_call "" "mcp_fn_list_domains"
|
|
;;
|
|
mcp__registry__fn_proposal)
|
|
insert_call "" "mcp_fn_proposal"
|
|
;;
|
|
mcp__registry__fn_doctor)
|
|
insert_call "" "mcp_fn_doctor"
|
|
;;
|
|
mcp__registry__fn_create_function)
|
|
insert_call "" "mcp_fn_create_function"
|
|
;;
|
|
|
|
Edit|Write|MultiEdit)
|
|
FILE_PATH=$(printf '%s' "$INPUT" | jq -r '.tool_input.file_path // ""')
|
|
ABS_PATH="$FILE_PATH"
|
|
# Make path relative to root if absolute and inside root
|
|
case "$FILE_PATH" in
|
|
"$ROOT"/*) FILE_PATH="${FILE_PATH#$ROOT/}" ;;
|
|
/*) ABS_PATH="$FILE_PATH" ;;
|
|
*) ABS_PATH="$ROOT/$FILE_PATH" ;;
|
|
esac
|
|
FN_ID=$(derive_fn_id_from_path "$FILE_PATH" || true)
|
|
if [ -n "$FN_ID" ]; then
|
|
insert_code_write "$FN_ID" "$FILE_PATH" 0 0
|
|
insert_call "$FN_ID" "edit_registry"
|
|
insert_edit_version "$FN_ID" "$ABS_PATH"
|
|
fi
|
|
;;
|
|
|
|
Bash)
|
|
CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // ""')
|
|
CMD_HEAD=$(printf '%s' "$CMD" | head -c 200 | tr '\n' ' ')
|
|
|
|
# Classify
|
|
TOOL_USED="bash_other"
|
|
FN_ID=""
|
|
|
|
if printf '%s' "$CMD" | grep -qE '(^|[[:space:]])\./fn[[:space:]]+run[[:space:]]+'; then
|
|
TOOL_USED="fn_cli_run"
|
|
FN_ID=$(printf '%s' "$CMD" | sed -nE 's/.*\.\/fn[[:space:]]+run[[:space:]]+([A-Za-z0-9_]+).*/\1/p' | head -n1)
|
|
elif printf '%s' "$CMD" | grep -qE 'python/\.venv/bin/python3[[:space:]]+-[[:space:]]+<<'; then
|
|
TOOL_USED="heredoc_py"
|
|
elif printf '%s' "$CMD" | grep -qE 'sqlite3[[:space:]][^|]*\bregistry\.db\b'; then
|
|
TOOL_USED="sqlite_direct"
|
|
fi
|
|
|
|
insert_call "$FN_ID" "$TOOL_USED" 0 "$CMD_HEAD"
|
|
|
|
# ---- Violation rules ----
|
|
# 1. sqlite3 directo SELECT sobre registry.db (excepto schema/pragma/count/join)
|
|
if [ "$TOOL_USED" = "sqlite_direct" ]; then
|
|
if ! printf '%s' "$CMD" | grep -qiE '(\.schema|\.tables|PRAGMA[[:space:]]+(table_info|index_list)|COUNT\(|GROUP[[:space:]]+BY|JOIN[[:space:]])'; then
|
|
insert_violation "sqlite3_registry_select" "" "$CMD_HEAD" "warning"
|
|
fi
|
|
fi
|
|
|
|
# 2. python -c "import X; dir(X)"
|
|
if printf '%s' "$CMD" | grep -qE 'python[3]?[[:space:]]+-c[[:space:]]+["'\''].*import.*(dir|help)\('; then
|
|
insert_violation "python_dir_inspect" "" "$CMD_HEAD" "info"
|
|
fi
|
|
|
|
# 3. from <pkg> import * (en heredoc python)
|
|
if [ "$TOOL_USED" = "heredoc_py" ]; then
|
|
if printf '%s' "$CMD" | grep -qE 'from[[:space:]]+[A-Za-z0-9_.]+[[:space:]]+import[[:space:]]+\*'; then
|
|
insert_violation "import_star_in_heredoc" "" "$CMD_HEAD" "warning"
|
|
fi
|
|
if printf '%s' "$CMD" | grep -qE 'client\._http\.request\('; then
|
|
insert_violation "client_http_request_direct" "" "$CMD_HEAD" "warning"
|
|
fi
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
exit 0
|