Session 4 step 16 — post-hoc enforcement via PostToolUse hook with PAUSED flag, budget-report.sh aggregates spend against window limit. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
90 lines
2.8 KiB
Bash
90 lines
2.8 KiB
Bash
#!/bin/bash
|
|
# PostToolUse hook: Log cost events and enforce budget.
|
|
# Bash 3.2 compatible. Uses python3 for JSON parsing.
|
|
#
|
|
# Follows Paperclip's post-hoc enforcement pattern:
|
|
# 1. Log cost event after each tool call
|
|
# 2. Check cumulative cost against budget policy
|
|
# 3. Warn at soft threshold, pause at hard threshold
|
|
#
|
|
# Placeholders:
|
|
# {{WORKING_DIR}} - absolute path to project directory
|
|
|
|
WORKING_DIR="{{WORKING_DIR}}"
|
|
BUDGET_DIR="$WORKING_DIR/budget"
|
|
COST_LOG="$BUDGET_DIR/cost-events.jsonl"
|
|
BUDGET_FILE="$WORKING_DIR/BUDGET.md"
|
|
PAUSED_FLAG="$BUDGET_DIR/PAUSED"
|
|
|
|
mkdir -p "$BUDGET_DIR"
|
|
|
|
# Read hook input
|
|
INPUT=$(cat)
|
|
TOOL_NAME=$(echo "$INPUT" | python3 -c "import sys,json; print(json.load(sys.stdin).get('tool_name',''))" 2>/dev/null)
|
|
|
|
# Log cost event
|
|
python3 -c "
|
|
import json, sys, time, os
|
|
|
|
try:
|
|
data = json.loads('''$INPUT''')
|
|
except:
|
|
sys.exit(0)
|
|
|
|
tool_name = data.get('tool_name', '')
|
|
event = {
|
|
'timestamp': time.strftime('%Y-%m-%dT%H:%M:%SZ', time.gmtime()),
|
|
'tool_name': tool_name,
|
|
'agent': os.environ.get('AGENT_NAME', 'unknown'),
|
|
'estimated_tokens': 0
|
|
}
|
|
|
|
cost_log = '$COST_LOG'
|
|
with open(cost_log, 'a') as f:
|
|
f.write(json.dumps(event) + '\n')
|
|
" 2>/dev/null
|
|
|
|
# Check budget if BUDGET.md exists
|
|
if [ -f "$BUDGET_FILE" ] && [ -f "$COST_LOG" ]; then
|
|
BUDGET_RESULT=$(BUDGET_FILE="$BUDGET_FILE" COST_LOG="$COST_LOG" PAUSED_FLAG="$PAUSED_FLAG" python3 -c "
|
|
import re, json, os
|
|
budget_file = os.environ.get('BUDGET_FILE', '')
|
|
cost_log = os.environ.get('COST_LOG', '')
|
|
paused_flag = os.environ.get('PAUSED_FLAG', '')
|
|
try:
|
|
content = open(budget_file).read()
|
|
limit_m = re.search(r'limit:\s*(\d+)\s*cents', content)
|
|
if not limit_m: print('ok'); exit(0)
|
|
limit = int(limit_m.group(1))
|
|
warn_m = re.search(r'warn_percent:\s*(\d+)', content)
|
|
warn_pct = int(warn_m.group(1)) if warn_m else 80
|
|
hard_m = re.search(r'hard_stop:\s*(\w+)', content)
|
|
hard_stop = hard_m.group(1).lower() == 'true' if hard_m else True
|
|
event_count = sum(1 for _ in open(cost_log))
|
|
estimated_cents = event_count
|
|
pct = (estimated_cents / limit * 100) if limit > 0 else 0
|
|
if pct >= 100 and hard_stop:
|
|
open(paused_flag, 'w').write('Budget exceeded: ' + str(estimated_cents) + '/' + str(limit) + ' cents')
|
|
print('hard_stop')
|
|
elif pct >= warn_pct:
|
|
print('warn')
|
|
else:
|
|
print('ok')
|
|
except Exception as e:
|
|
print('ok')
|
|
" 2>/dev/null)
|
|
|
|
if [ "$BUDGET_RESULT" = "hard_stop" ]; then
|
|
echo "BUDGET EXCEEDED — agent paused. Check $PAUSED_FLAG" >&2
|
|
elif [ "$BUDGET_RESULT" = "warn" ]; then
|
|
echo "BUDGET WARNING — approaching limit" >&2
|
|
fi
|
|
fi
|
|
|
|
# Check if agent is paused
|
|
if [ -f "$PAUSED_FLAG" ]; then
|
|
echo '{"decision": "block", "reason": "Agent paused: budget exceeded. Remove '"$PAUSED_FLAG"' to resume."}'
|
|
exit 2
|
|
fi
|
|
|
|
exit 0
|