mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-01 19:25:10 +02:00
64d5a3e424
* fix: drop all anon RLS policies + revoke view access + add cache table Migration 002 locks down the Supabase telemetry backend: - Drops all SELECT, INSERT, UPDATE policies for the anon role - Explicitly revokes SELECT on crash_clusters and skill_sequences views - Drops stale error_message/failed_step columns (exist live but not in migration) - Creates community_pulse_cache table for server-side aggregation caching * feat: extend community-pulse with full dashboard data + server-side cache community-pulse now returns top skills, crash clusters, version distribution, and weekly active count in a single aggregated response. Results are cached in the community_pulse_cache table (1-hour TTL) to prevent DoS via repeated expensive queries. * fix: route all telemetry through edge functions, not PostgREST - gstack-telemetry-sync: POST to /functions/v1/telemetry-ingest instead of /rest/v1/telemetry_events. Removes sed field-renaming (edge function expects raw JSONL names). Parses inserted count — holds cursor if zero inserted. - gstack-update-check: POST to /functions/v1/update-check. - gstack-community-dashboard: calls community-pulse edge function instead of direct PostgREST queries. - config.sh: removes GSTACK_TELEMETRY_ENDPOINT, fixes misleading comment. * test: RLS smoke test + telemetry field name verification - verify-rls.sh: 9-check smoke test (5 reads + 3 inserts + 1 update) verifying anon key is fully locked out after migration. - telemetry.test.ts: verifies JSONL uses raw field names (v, ts, sessions) that the edge function expects, not Postgres column names. - README.md: fixes privacy claim to match actual RLS policy. * chore: bump version and changelog (v0.11.16.0) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: pre-landing review fixes — JSONB field order, version filter, RLS verification - Dashboard JSON parsing: use per-object grep instead of field-order-dependent regex (JSONB doesn't preserve key order) - Version distribution: filter to skill_run events only (was counting all types) - verify-rls.sh: only 401/403 count as PASS (not empty 200 or 5xx); add Authorization header to test as anon role properly - Remove dead empty loop in community-pulse * chore: untrack browse/dist binaries — 116MB of arm64-only Mach-O These compiled Bun binaries only work on arm64 macOS, and ./setup already rebuilds from source for every platform. They were tracked despite .gitignore due to being committed before the ignore rule. Untracking stops them from appearing as modified in every diff. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: tone down changelog — security hardening, not incident report * fix: keep INSERT policies for old client compat, preserve extra columns - Keep anon INSERT policies so pre-v0.11.16 clients can still sync telemetry via PostgREST while new clients use edge functions - Add error_message/failed_step columns to migration (reconcile repo with live schema) instead of dropping them - Security fix still lands: SELECT and UPDATE policies are dropped Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: sync package.json version with VERSION file (0.11.16.0) --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
213 lines
7.7 KiB
Bash
Executable File
213 lines
7.7 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# gstack-update-check — periodic version check for all skills.
|
|
#
|
|
# Output (one line, or nothing):
|
|
# JUST_UPGRADED <old> <new> — marker found from recent upgrade
|
|
# UPGRADE_AVAILABLE <old> <new> — remote VERSION differs from local
|
|
# (nothing) — up to date, snoozed, disabled, or check skipped
|
|
#
|
|
# Env overrides (for testing):
|
|
# GSTACK_DIR — override auto-detected gstack root
|
|
# GSTACK_REMOTE_URL — override remote VERSION URL
|
|
# GSTACK_STATE_DIR — override ~/.gstack state directory
|
|
set -euo pipefail
|
|
|
|
GSTACK_DIR="${GSTACK_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
|
|
STATE_DIR="${GSTACK_STATE_DIR:-$HOME/.gstack}"
|
|
CACHE_FILE="$STATE_DIR/last-update-check"
|
|
MARKER_FILE="$STATE_DIR/just-upgraded-from"
|
|
SNOOZE_FILE="$STATE_DIR/update-snoozed"
|
|
VERSION_FILE="$GSTACK_DIR/VERSION"
|
|
REMOTE_URL="${GSTACK_REMOTE_URL:-https://raw.githubusercontent.com/garrytan/gstack/main/VERSION}"
|
|
|
|
# ─── Force flag (busts cache + snooze for standalone /gstack-upgrade) ──
|
|
if [ "${1:-}" = "--force" ]; then
|
|
rm -f "$CACHE_FILE"
|
|
rm -f "$SNOOZE_FILE"
|
|
fi
|
|
|
|
# ─── Step 0: Check if updates are disabled ────────────────────
|
|
_UC=$("$GSTACK_DIR/bin/gstack-config" get update_check 2>/dev/null || true)
|
|
if [ "$_UC" = "false" ]; then
|
|
exit 0
|
|
fi
|
|
|
|
# ─── Migration: fix stale Codex descriptions (one-time) ───────
|
|
# Existing installs may have .agents/skills/gstack/SKILL.md with oversized
|
|
# descriptions (>1024 chars) that Codex rejects. We can't regenerate from
|
|
# the runtime root (no bun/scripts), so delete oversized files — the next
|
|
# ./setup or /gstack-upgrade will regenerate them properly.
|
|
# Marker file ensures this runs at most once per install.
|
|
if [ ! -f "$STATE_DIR/.codex-desc-healed" ]; then
|
|
for _AGENTS_SKILL in "$GSTACK_DIR"/.agents/skills/*/SKILL.md; do
|
|
[ -f "$_AGENTS_SKILL" ] || continue
|
|
_DESC=$(awk '/^---$/{n++;next}n==1&&/^description:/{d=1;sub(/^description:\s*/,"");if(length>0)print;next}d&&/^ /{sub(/^ /,"");print;next}d{d=0}' "$_AGENTS_SKILL" | wc -c | tr -d ' ')
|
|
if [ "${_DESC:-0}" -gt 1024 ]; then
|
|
rm -f "$_AGENTS_SKILL"
|
|
fi
|
|
done
|
|
mkdir -p "$STATE_DIR"
|
|
touch "$STATE_DIR/.codex-desc-healed"
|
|
fi
|
|
|
|
# ─── Snooze helper ──────────────────────────────────────────
|
|
# check_snooze <remote_version>
|
|
# Returns 0 if snoozed (should stay quiet), 1 if not snoozed (should output).
|
|
#
|
|
# Snooze file format: <version> <level> <epoch>
|
|
# Level durations: 1=24h, 2=48h, 3+=7d
|
|
# New version (version mismatch) resets snooze.
|
|
check_snooze() {
|
|
local remote_ver="$1"
|
|
if [ ! -f "$SNOOZE_FILE" ]; then
|
|
return 1 # no snooze file → not snoozed
|
|
fi
|
|
local snoozed_ver snoozed_level snoozed_epoch
|
|
snoozed_ver="$(awk '{print $1}' "$SNOOZE_FILE" 2>/dev/null || true)"
|
|
snoozed_level="$(awk '{print $2}' "$SNOOZE_FILE" 2>/dev/null || true)"
|
|
snoozed_epoch="$(awk '{print $3}' "$SNOOZE_FILE" 2>/dev/null || true)"
|
|
|
|
# Validate: all three fields must be non-empty
|
|
if [ -z "$snoozed_ver" ] || [ -z "$snoozed_level" ] || [ -z "$snoozed_epoch" ]; then
|
|
return 1 # corrupt file → not snoozed
|
|
fi
|
|
|
|
# Validate: level and epoch must be integers
|
|
case "$snoozed_level" in *[!0-9]*) return 1 ;; esac
|
|
case "$snoozed_epoch" in *[!0-9]*) return 1 ;; esac
|
|
|
|
# New version dropped? Ignore snooze.
|
|
if [ "$snoozed_ver" != "$remote_ver" ]; then
|
|
return 1
|
|
fi
|
|
|
|
# Compute snooze duration based on level
|
|
local duration
|
|
case "$snoozed_level" in
|
|
1) duration=86400 ;; # 24 hours
|
|
2) duration=172800 ;; # 48 hours
|
|
*) duration=604800 ;; # 7 days (level 3+)
|
|
esac
|
|
|
|
local now
|
|
now="$(date +%s)"
|
|
local expires=$(( snoozed_epoch + duration ))
|
|
if [ "$now" -lt "$expires" ]; then
|
|
return 0 # still snoozed
|
|
fi
|
|
|
|
return 1 # snooze expired
|
|
}
|
|
|
|
# ─── Step 1: Read local version ──────────────────────────────
|
|
LOCAL=""
|
|
if [ -f "$VERSION_FILE" ]; then
|
|
LOCAL="$(cat "$VERSION_FILE" 2>/dev/null | tr -d '[:space:]')"
|
|
fi
|
|
if [ -z "$LOCAL" ]; then
|
|
exit 0 # No VERSION file → skip check
|
|
fi
|
|
|
|
# ─── Step 2: Check "just upgraded" marker ─────────────────────
|
|
if [ -f "$MARKER_FILE" ]; then
|
|
OLD="$(cat "$MARKER_FILE" 2>/dev/null | tr -d '[:space:]')"
|
|
rm -f "$MARKER_FILE"
|
|
rm -f "$SNOOZE_FILE"
|
|
mkdir -p "$STATE_DIR"
|
|
echo "UP_TO_DATE $LOCAL" > "$CACHE_FILE"
|
|
if [ -n "$OLD" ]; then
|
|
echo "JUST_UPGRADED $OLD $LOCAL"
|
|
fi
|
|
exit 0
|
|
fi
|
|
|
|
# ─── Step 3: Check cache freshness ──────────────────────────
|
|
# UP_TO_DATE: 60 min TTL (detect new releases quickly)
|
|
# UPGRADE_AVAILABLE: 720 min TTL (keep nagging)
|
|
if [ -f "$CACHE_FILE" ]; then
|
|
CACHED="$(cat "$CACHE_FILE" 2>/dev/null || true)"
|
|
case "$CACHED" in
|
|
UP_TO_DATE*) CACHE_TTL=60 ;;
|
|
UPGRADE_AVAILABLE*) CACHE_TTL=720 ;;
|
|
*) CACHE_TTL=0 ;; # corrupt → force re-fetch
|
|
esac
|
|
|
|
STALE=$(find "$CACHE_FILE" -mmin +$CACHE_TTL 2>/dev/null || true)
|
|
if [ -z "$STALE" ] && [ "$CACHE_TTL" -gt 0 ]; then
|
|
case "$CACHED" in
|
|
UP_TO_DATE*)
|
|
CACHED_VER="$(echo "$CACHED" | awk '{print $2}')"
|
|
if [ "$CACHED_VER" = "$LOCAL" ]; then
|
|
exit 0
|
|
fi
|
|
;;
|
|
UPGRADE_AVAILABLE*)
|
|
CACHED_OLD="$(echo "$CACHED" | awk '{print $2}')"
|
|
if [ "$CACHED_OLD" = "$LOCAL" ]; then
|
|
CACHED_NEW="$(echo "$CACHED" | awk '{print $3}')"
|
|
if check_snooze "$CACHED_NEW"; then
|
|
exit 0 # snoozed — stay quiet
|
|
fi
|
|
echo "$CACHED"
|
|
exit 0
|
|
fi
|
|
;;
|
|
esac
|
|
fi
|
|
fi
|
|
|
|
# ─── Step 4: Slow path — fetch remote version ────────────────
|
|
mkdir -p "$STATE_DIR"
|
|
|
|
# Fire Supabase install ping in background (parallel, non-blocking)
|
|
# This logs an update check event for community health metrics via edge function.
|
|
# If Supabase is not configured or telemetry is off, this is a no-op.
|
|
if [ -z "${GSTACK_SUPABASE_URL:-}" ] && [ -f "$GSTACK_DIR/supabase/config.sh" ]; then
|
|
. "$GSTACK_DIR/supabase/config.sh"
|
|
fi
|
|
_SUPA_URL="${GSTACK_SUPABASE_URL:-}"
|
|
_SUPA_KEY="${GSTACK_SUPABASE_ANON_KEY:-}"
|
|
# Respect telemetry opt-out — don't ping Supabase if user set telemetry: off
|
|
_TEL_TIER="$("$GSTACK_DIR/bin/gstack-config" get telemetry 2>/dev/null || true)"
|
|
if [ -n "$_SUPA_URL" ] && [ -n "$_SUPA_KEY" ] && [ "${_TEL_TIER:-off}" != "off" ]; then
|
|
_OS="$(uname -s | tr '[:upper:]' '[:lower:]')"
|
|
curl -sf --max-time 5 \
|
|
-X POST "${_SUPA_URL}/functions/v1/update-check" \
|
|
-H "Content-Type: application/json" \
|
|
-H "apikey: ${_SUPA_KEY}" \
|
|
-d "{\"version\":\"$LOCAL\",\"os\":\"$_OS\"}" \
|
|
>/dev/null 2>&1 &
|
|
fi
|
|
|
|
# GitHub raw fetch (primary, always reliable)
|
|
REMOTE=""
|
|
REMOTE="$(curl -sf --max-time 5 "$REMOTE_URL" 2>/dev/null || true)"
|
|
REMOTE="$(echo "$REMOTE" | tr -d '[:space:]')"
|
|
|
|
# Validate: must look like a version number (reject HTML error pages)
|
|
if ! echo "$REMOTE" | grep -qE '^[0-9]+\.[0-9.]+$'; then
|
|
# Invalid or empty response — assume up to date
|
|
echo "UP_TO_DATE $LOCAL" > "$CACHE_FILE"
|
|
exit 0
|
|
fi
|
|
|
|
if [ "$LOCAL" = "$REMOTE" ]; then
|
|
echo "UP_TO_DATE $LOCAL" > "$CACHE_FILE"
|
|
exit 0
|
|
fi
|
|
|
|
# Versions differ — upgrade available
|
|
echo "UPGRADE_AVAILABLE $LOCAL $REMOTE" > "$CACHE_FILE"
|
|
if check_snooze "$REMOTE"; then
|
|
exit 0 # snoozed — stay quiet
|
|
fi
|
|
|
|
# Log upgrade_prompted event (only on slow-path fetch, not cached replays)
|
|
TEL_CMD="$GSTACK_DIR/bin/gstack-telemetry-log"
|
|
if [ -x "$TEL_CMD" ]; then
|
|
"$TEL_CMD" --event-type upgrade_prompted --skill "" --duration 0 \
|
|
--outcome success --session-id "update-$$-$(date +%s)" 2>/dev/null &
|
|
fi
|
|
|
|
echo "UPGRADE_AVAILABLE $LOCAL $REMOTE"
|