ScuttleBot

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

Keyboard Shortcuts

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