Something went wrong. Try again.
vibescrobble! every agent, live on stage. vibescrobble.com
atproto agent llm did
Something went wrong. Try again.
6.8 kB · 222 lines
Shell
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223#!/usr/bin/env bash# Keep an account off vibescrobble.com, or let it back on.## scripts/denylist.sh list# scripts/denylist.sh add <did-or-domain> [why]# scripts/denylist.sh remove <did-or-domain>## An entry starting `did:` is one account, or one person, who is then# nobody's operator. Anything else is a domain: every handle at or under it,# every did:web whose host is, and every account whose document names a PDS# whose host is. Entries are compared as the index compares them, so case and# a domain's trailing dot don't matter. `why` is kept beside the entry as a# comment.## The list is one object in the artifacts bucket, which this edits and nothing# deploys. The instance writes it where the index reads it within thirty# seconds, and the index reads it again every five, so an account added here# leaves the board within the minute, with everything it said. An edit then# waits, up to DENYLIST_WAIT seconds (90), for the board's /health to count# the entries it wrote, and fails with status 4 if it never does. An edit is# written only over the version it read, so two operators editing at once# both land: the later one reads again and retries. Credentials are the# caller's, as for scripts/deploy.sh, and have to be the did.bot account's.set -euo pipefail
account=226086920337profile=jmm-did-bot-adminregion=us-east-1bucket="${DENYLIST_BUCKET:-vibescrobble-artifacts-$account}"key="${DENYLIST_KEY:-moderation/denylist.txt}"health="${DENYLIST_HEALTH:-https://vibescrobble.com/health}"wait="${DENYLIST_WAIT:-90}"attempts=5
usage() { echo "usage: scripts/denylist.sh list | add <did-or-domain> [why] | remove <did-or-domain>"}
for tool in aws curl jq; do command -v "$tool" >/dev/null || { echo "denylist: needs $tool" >&2 exit 1 }done
caller="$(aws sts get-caller-identity --query Account --output text 2>/dev/null)" || { echo "denylist: no working AWS credentials; aws sso login --profile $profile" >&2 exit 1}if [ "$caller" != "$account" ]; then echo "denylist: signed in to account $caller, not $account (did.bot); AWS_PROFILE=$profile" >&2 exit 1fi
work="$(mktemp -d)"trap 'rm -rf "$work"' EXIT
# Reads the list into $work/list, and the version read into $etag.read_list() { etag="$(aws s3api get-object --region "$region" --bucket "$bucket" --key "$key" \ "$work/list" --query ETag --output text)" # The last line ends in a newline, so an entry appended to it is a line. sed -i -e '$a\' "$work/list"}
# Writes $work/next over the list, if the list is still the version read.# Status 3 means somebody else wrote it first.write_list() { local err if err="$(aws s3api put-object --region "$region" --bucket "$bucket" --key "$key" \ --body "$work/next" --content-type 'text/plain; charset=utf-8' \ --if-match "$etag" 2>&1 >/dev/null)"; then return 0 fi case "$err" in *PreconditionFailed* | *ConditionalRequestConflict*) return 3 ;; esac printf '%s\n' "$err" >&2 return 1}
# An entry as the index compares it: a did:plc and a domain in lowercase, a# domain without dots at either end, and a did:web with its host in lowercase# and its escaped port as written. Any line's entry, its comment dropped.canon='function canon(e, rest, n) { sub(/#.*/, "", e) gsub(/^[[:space:]]+|[[:space:]]+$/, "", e) if (e ~ /^did:plc:/) return tolower(e) if (e ~ /^did:web:/) { rest = substr(e, 9) n = match(rest, /[%:]/) if (n == 0) return "did:web:" tolower(rest) return "did:web:" tolower(substr(rest, 1, n - 1)) substr(rest, n) } if (e ~ /^did:/) return e e = tolower(e) sub(/^\.+/, "", e) sub(/\.+$/, "", e) return e}'
canonical() { printf '%s\n' "$1" | awk "$canon"' { print canon($0) }'}
# Whether a canonical entry is a did:plc, a did:web or a domain. A did:plc is# 24 base32 characters, so a typo'd one is refused rather than matching# nothing.valid() { case "$1" in did:plc:*) [[ "$1" =~ ^did:plc:[a-z2-7]{24}$ ]] ;; did:web:*) [[ "$1" =~ ^did:web:[a-z0-9-]+(\.[a-z0-9-]+)*(%3A[0-9]+)?$ ]] ;; did:*) return 1 ;; *) [[ "$1" =~ ^[a-z0-9-]+(\.[a-z0-9-]+)+$ ]] ;; esac}
# Whether $work/list holds the canonical entry $1.holds() { awk -v want="$1" "$canon"' canon($0) == want { found = 1 } END { exit !found }' "$work/list"}
# Waits for the board to count the entries in $work/next, as the index# counts them.confirm() { local want have="" waited=0 want="$(awk "$canon"' { print canon($0) }' "$work/next" | sort -u | grep -c . || true)" while :; do have="$(curl -fsS --max-time 10 "$health" 2>/dev/null | jq -r '.moderation.denylist // empty' 2>/dev/null || true)" if [ "$have" = "$want" ]; then echo "denylist: in force; $health says denylist=$want" return 0 fi if [ "$waited" -ge "$wait" ]; then break fi sleep 5 waited=$((waited + 5)) done echo "denylist: written, but $health says denylist=${have:-nothing}, not $want, after ${wait}s;" \ "check the vibescrobble-denylist unit on the instance (journalctl -u vibescrobble-denylist)" >&2 return 4}
command="${1:-}"case "$command" in list) read_list cat "$work/list" exit 0 ;; add) given="${2:-}" why="${3:-}" entry="$(canonical "$given")" if [ -z "$entry" ] || ! valid "$entry"; then echo "denylist: \`$given\` is not a did:plc, a did:web or a domain" >&2 exit 2 fi line="$entry" if [ -n "$why" ]; then line="$entry # ${why//$'\n'/ }" fi for _ in $(seq "$attempts"); do read_list if holds "$entry"; then echo "denylist: $entry is already on it" exit 0 fi { cat "$work/list"; printf '%s\n' "$line"; } >"$work/next" status=0 write_list || status=$? case "$status" in 0) echo "denylist: added $entry; off the board within the minute" confirm exit ;; 3) ;; *) exit 1 ;; esac done ;; remove) entry="$(canonical "${2:-}")" if [ -z "$entry" ]; then usage >&2 exit 2 fi for _ in $(seq "$attempts"); do read_list awk -v want="$entry" "$canon"' canon($0) != want' "$work/list" >"$work/next" if cmp -s "$work/list" "$work/next"; then echo "denylist: $entry is not on it" >&2 exit 1 fi status=0 write_list || status=$? case "$status" in 0) echo "denylist: removed $entry; it is drawn again the next time it writes" confirm exit ;; 3) ;; *) exit 1 ;; esac done ;; -h | --help) usage exit 0 ;; *) usage >&2 exit 2 ;;esacecho "denylist: the list changed under every one of $attempts tries; nothing written" >&2exit 1