ScuttleBot

scuttlebot / skills / openai-relay / hooks / scuttlebot-check.sh
Source Blame History 136 lines
50baf1a… lmata 1 #!/bin/bash
50baf1a… lmata 2 # Pre-action hook for Codex. Checks scuttlebot for operator instructions before
50baf1a… lmata 3 # each tool call and returns a blocking decision when the session nick is
50baf1a… lmata 4 # explicitly mentioned.
50baf1a… lmata 5
50baf1a… lmata 6 SCUTTLEBOT_CONFIG_FILE="${SCUTTLEBOT_CONFIG_FILE:-$HOME/.config/scuttlebot-relay.env}"
50baf1a… lmata 7 if [ -f "$SCUTTLEBOT_CONFIG_FILE" ]; then
50baf1a… lmata 8 set -a
50baf1a… lmata 9 . "$SCUTTLEBOT_CONFIG_FILE"
50baf1a… lmata 10 set +a
50baf1a… lmata 11 fi
1d3caa2… lmata 12 if [ -n "${SCUTTLEBOT_CHANNEL_STATE_FILE:-}" ] && [ -f "$SCUTTLEBOT_CHANNEL_STATE_FILE" ]; then
1d3caa2… lmata 13 set -a
1d3caa2… lmata 14 . "$SCUTTLEBOT_CHANNEL_STATE_FILE"
1d3caa2… lmata 15 set +a
1d3caa2… lmata 16 fi
50baf1a… lmata 17
50baf1a… lmata 18 SCUTTLEBOT_URL="${SCUTTLEBOT_URL:-http://localhost:8080}"
50baf1a… lmata 19 SCUTTLEBOT_TOKEN="${SCUTTLEBOT_TOKEN}"
50baf1a… lmata 20 SCUTTLEBOT_CHANNEL="${SCUTTLEBOT_CHANNEL:-general}"
50baf1a… lmata 21 SCUTTLEBOT_HOOKS_ENABLED="${SCUTTLEBOT_HOOKS_ENABLED:-1}"
50baf1a… lmata 22
50baf1a… lmata 23 sanitize() {
50baf1a… lmata 24 printf '%s' "$1" | tr -cs '[:alnum:]_-' '-'
1d3caa2… lmata 25 }
1d3caa2… lmata 26
1d3caa2… lmata 27 normalize_channel() {
1d3caa2… lmata 28 local channel="$1"
1d3caa2… lmata 29 channel="${channel//[$' \t\r\n']/}"
1d3caa2… lmata 30 channel="${channel#\#}"
1d3caa2… lmata 31 printf '%s' "$channel"
1d3caa2… lmata 32 }
1d3caa2… lmata 33
1d3caa2… lmata 34 relay_channels() {
1d3caa2… lmata 35 local raw="${SCUTTLEBOT_CHANNELS:-$SCUTTLEBOT_CHANNEL}"
1d3caa2… lmata 36 local IFS=','
1d3caa2… lmata 37 local item channel seen=""
1d3caa2… lmata 38 read -r -a items <<< "$raw"
1d3caa2… lmata 39 for item in "${items[@]}"; do
1d3caa2… lmata 40 channel=$(normalize_channel "$item")
1d3caa2… lmata 41 [ -n "$channel" ] || continue
1d3caa2… lmata 42 case ",$seen," in
1d3caa2… lmata 43 *,"$channel",*) ;;
1d3caa2… lmata 44 *)
1d3caa2… lmata 45 seen="${seen:+$seen,}$channel"
1d3caa2… lmata 46 printf '%s\n' "$channel"
1d3caa2… lmata 47 ;;
1d3caa2… lmata 48 esac
1d3caa2… lmata 49 done
1d3caa2… lmata 50 }
1d3caa2… lmata 51
1d3caa2… lmata 52 contains_mention() {
1d3caa2… lmata 53 local text="$1"
1d3caa2… lmata 54 [[ "$text" =~ (^|[^[:alnum:]_./\\-])$SCUTTLEBOT_NICK($|[^[:alnum:]_./\\-]) ]]
1d3caa2… lmata 55 }
1d3caa2… lmata 56
87e6978… lmata 57 epoch_millis() {
87e6978… lmata 58 local at="$1" ts_secs ts_frac ts_clean frac
87e6978… lmata 59 ts_frac=$(printf '%s' "$at" | grep -oE '\.[0-9]+' | head -1)
87e6978… lmata 60 ts_clean=$(printf '%s' "$at" | sed 's/\.[0-9]*//' | sed 's/\([+-][0-9][0-9]\):\([0-9][0-9]\)$/\1\2/')
87e6978… lmata 61 ts_secs=$(date -j -f "%Y-%m-%dT%H:%M:%S%z" "$ts_clean" "+%s" 2>/dev/null || \
87e6978… lmata 62 date -d "$ts_clean" "+%s" 2>/dev/null)
87e6978… lmata 63 [ -n "$ts_secs" ] || return
87e6978… lmata 64 if [ -n "$ts_frac" ]; then
87e6978… lmata 65 frac="${ts_frac#.}000"
87e6978… lmata 66 printf '%s%s' "$ts_secs" "${frac:0:3}"
87e6978… lmata 67 else
87e6978… lmata 68 printf '%s000' "$ts_secs"
87e6978… lmata 69 fi
87e6978… lmata 70 }
87e6978… lmata 71
87e6978… lmata 72 now_millis() {
87e6978… lmata 73 python3 -c "import time; print(int(time.time()*1000))" 2>/dev/null || \
87e6978… lmata 74 date +%s%3N 2>/dev/null || \
87e6978… lmata 75 printf '%s000' "$(date +%s)"
50baf1a… lmata 76 }
50baf1a… lmata 77
50baf1a… lmata 78 base_name=$(basename "$(pwd)")
50baf1a… lmata 79 base_name=$(sanitize "$base_name")
50baf1a… lmata 80 session_suffix="${SCUTTLEBOT_SESSION_ID:-${CODEX_SESSION_ID:-$PPID}}"
50baf1a… lmata 81 session_suffix=$(sanitize "$session_suffix")
50baf1a… lmata 82 default_nick="codex-${base_name}-${session_suffix}"
50baf1a… lmata 83 SCUTTLEBOT_NICK="${SCUTTLEBOT_NICK:-$default_nick}"
50baf1a… lmata 84
50baf1a… lmata 85 [ "$SCUTTLEBOT_HOOKS_ENABLED" = "0" ] && exit 0
50baf1a… lmata 86 [ "$SCUTTLEBOT_HOOKS_ENABLED" = "false" ] && exit 0
50baf1a… lmata 87 [ -z "$SCUTTLEBOT_TOKEN" ] && exit 0
50baf1a… lmata 88
1d3caa2… lmata 89 state_key=$(printf '%s' "$SCUTTLEBOT_NICK|$(pwd)" | cksum | awk '{print $1}')
50baf1a… lmata 90 LAST_CHECK_FILE="/tmp/.scuttlebot-last-check-$state_key"
50baf1a… lmata 91
50baf1a… lmata 92 last_check=0
50baf1a… lmata 93 if [ -f "$LAST_CHECK_FILE" ]; then
50baf1a… lmata 94 last_check=$(cat "$LAST_CHECK_FILE")
87e6978… lmata 95 # Migrate from second-precision to millisecond-precision on first upgrade.
87e6978… lmata 96 if [ "$last_check" -lt 100000000000 ] 2>/dev/null; then
87e6978… lmata 97 last_check=$((last_check * 1000))
87e6978… lmata 98 fi
50baf1a… lmata 99 fi
87e6978… lmata 100 now=$(now_millis)
50baf1a… lmata 101 echo "$now" > "$LAST_CHECK_FILE"
50baf1a… lmata 102
50baf1a… lmata 103 BOTS='["bridge","oracle","sentinel","steward","scribe","warden","snitch","herald","scroll","systembot","auditbot","claude"]'
50baf1a… lmata 104
50baf1a… lmata 105 instruction=$(
1d3caa2… lmata 106 for channel in $(relay_channels); do
1d3caa2… lmata 107 messages=$(curl -sf \
1d3caa2… lmata 108 --connect-timeout 1 \
1d3caa2… lmata 109 --max-time 2 \
1d3caa2… lmata 110 -H "Authorization: Bearer $SCUTTLEBOT_TOKEN" \
1d3caa2… lmata 111 "$SCUTTLEBOT_URL/v1/channels/$channel/messages" 2>/dev/null) || continue
1d3caa2… lmata 112 [ -n "$messages" ] || continue
1d3caa2… lmata 113 echo "$messages" | jq -r --argjson bots "$BOTS" --arg self "$SCUTTLEBOT_NICK" --arg channel "$channel" '
1d3caa2… lmata 114 .messages[]
1d3caa2… lmata 115 | select(.nick as $n |
1d3caa2… lmata 116 ($bots | index($n) | not) and
1d3caa2… lmata 117 ($n | startswith("claude-") | not) and
1d3caa2… lmata 118 ($n | startswith("codex-") | not) and
1d3caa2… lmata 119 ($n | startswith("gemini-") | not) and
1d3caa2… lmata 120 $n != $self
1d3caa2… lmata 121 )
1d3caa2… lmata 122 | "\(.at)\t\($channel)\t\(.nick)\t\(.text)"
1d3caa2… lmata 123 ' 2>/dev/null
1d3caa2… lmata 124 done | while IFS=$'\t' read -r at channel nick text; do
87e6978… lmata 125 ts=$(epoch_millis "$at")
50baf1a… lmata 126 [ -n "$ts" ] || continue
50baf1a… lmata 127 [ "$ts" -gt "$last_check" ] || continue
50baf1a… lmata 128 contains_mention "$text" || continue
1d3caa2… lmata 129 printf '%s\t[#%s] %s: %s\n' "$ts" "$channel" "$nick" "$text"
1d3caa2… lmata 130 done | sort -n | tail -1 | cut -f2-
50baf1a… lmata 131 )
50baf1a… lmata 132
50baf1a… lmata 133 [ -z "$instruction" ] && exit 0
50baf1a… lmata 134
50baf1a… lmata 135 echo "{\"decision\": \"block\", \"reason\": \"[IRC instruction from operator] $instruction\"}"
50baf1a… lmata 136 exit 0

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button