mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-01 19:25:10 +02:00
03973c2fab
* fix(bin): pass search params via env vars (RCE fix) (#819) Replace shell string interpolation with process.env in gstack-learnings-search to prevent arbitrary code execution via crafted learnings entries. Also fixes the CROSS_PROJECT interpolation that the original PR missed. Adds 3 regression tests verifying no shell interpolation remains in the bun -e block. Co-authored-by: garagon <garagon@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(browse): add path validation to upload command (#821) Add isPathWithin() and path traversal checks to the upload command, blocking file exfiltration via crafted upload paths. Uses existing SAFE_DIRECTORIES constant instead of a local copy. Adds 3 regression tests. Co-authored-by: garagon <garagon@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(browse): symlink resolution in meta-commands validateOutputPath (#820) Add realpathSync to validateOutputPath in meta-commands.ts to catch symlink-based directory escapes in screenshot, pdf, and responsive commands. Resolves SAFE_DIRECTORIES through realpathSync to handle macOS /tmp -> /private/tmp symlinks. Existing path validation tests pass with the hardened implementation. Co-authored-by: garagon <garagon@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * docs: add uninstall instructions to README (#812) Community PR #812 by @0531Kim. Adds two uninstall paths: the gstack-uninstall script (handles everything) and manual removal steps for when the repo isn't cloned. Includes CLAUDE.md cleanup note and Playwright cache guidance. Co-Authored-By: 0531Kim <0531Kim@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(browse): Windows launcher extraEnv + headed-mode token (#822) Community PR #822 by @pieterklue. Three fixes: 1. Windows launcher now merges extraEnv into spawned server env (was only passing BROWSE_STATE_FILE, dropping all other env vars) 2. Welcome page fallback serves inline HTML instead of about:blank redirect (avoids ERR_UNSAFE_REDIRECT on Windows) 3. /health returns auth token in headed mode even without Origin header (fixes Playwright Chromium extensions that don't send it) Also adds HOME/USERPROFILE fallback for cross-platform compatibility. Co-Authored-By: pieterklue <pieterklue@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(browse): terminate orphan server when parent process exits (#808) Community PR #808 by @mmporong. Passes BROWSE_PARENT_PID to the spawned server process. The server polls every 15s with signal 0 and calls shutdown() if the parent is gone. Prevents orphaned chrome-headless-shell processes when Claude Code sessions exit abnormally. Co-Authored-By: mmporong <mmporong@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(security): IPv6 ULA blocking, cookie redaction, per-tab cancel, targeted token (#664) Community PR #664 by @mr-k-man (security audit round 1, new parts only). - IPv6 ULA prefix blocking (fc00::/7) in url-validation.ts with false-positive guard for hostnames like fd.example.com - Cookie value redaction for tokens, API keys, JWTs in browse cookies command - Per-tab cancel files in killAgent() replacing broken global kill-signal - design/serve.ts: realpathSync upgrade prevents symlink bypass in /api/reload - extension: targeted getToken handler replaces token-in-health-broadcast - Supabase migration 003: column-level GRANT restricts anon UPDATE scope - Telemetry sync: upsert error logging - 10 new tests for IPv6, cookie redaction, DNS rebinding, path traversal Co-Authored-By: mr-k-man <mr-k-man@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * fix(security): CSS injection guard, timeout clamping, session validation, tests (#806) Community PR #806 by @mr-k-man (security audit round 2, new parts only). - CSS value validation (DANGEROUS_CSS) in cdp-inspector, write-commands, extension inspector - Queue file permissions (0o700/0o600) in cli, server, sidebar-agent - escapeRegExp for frame --url ReDoS fix - Responsive screenshot path validation with validateOutputPath - State load cookie filtering (reject localhost/.internal/metadata cookies) - Session ID format validation in loadSession - /health endpoint: remove currentUrl and currentMessage fields - QueueEntry interface + isValidQueueEntry validator for sidebar-agent - SIGTERM->SIGKILL escalation in timeout handler - Viewport dimension clamping (1-16384), wait timeout clamping (1s-300s) - Cookie domain validation in cookie-import and cookie-import-browser - DocumentFragment-based tab switching (XSS fix in sidepanel) - pollInProgress reentrancy guard for pollChat - toggleClass/injectCSS input validation in extension inspector - Snapshot annotated path validation with realpathSync - 714-line security-audit-r2.test.ts + 33-line learnings-injection.test.ts Co-Authored-By: mr-k-man <mr-k-man@users.noreply.github.com> Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * chore: bump version and changelog (v0.15.13.0) Community security wave: 8 PRs from 4 contributors (@garagon, @mr-k-man, @mmporong, @0531Kim, @pieterklue). IPv6 ULA blocking, cookie redaction, per-tab cancel signaling, CSS injection guards, timeout clamping, session validation, DocumentFragment XSS fix, parent process watchdog, uninstall docs, Windows fixes, and 750+ lines of security regression tests. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: garagon <garagon@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Co-authored-by: 0531Kim <0531Kim@users.noreply.github.com> Co-authored-by: pieterklue <pieterklue@users.noreply.github.com> Co-authored-by: mmporong <mmporong@users.noreply.github.com> Co-authored-by: mr-k-man <mr-k-man@users.noreply.github.com>
143 lines
5.3 KiB
Bash
Executable File
143 lines
5.3 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# gstack-telemetry-sync — sync local JSONL events to Supabase
|
|
#
|
|
# Fire-and-forget, backgrounded, rate-limited to once per 5 minutes.
|
|
# Strips local-only fields before sending. Respects privacy tiers.
|
|
# Posts to the telemetry-ingest edge function (not PostgREST directly).
|
|
#
|
|
# Env overrides (for testing):
|
|
# GSTACK_STATE_DIR — override ~/.gstack state directory
|
|
# GSTACK_DIR — override auto-detected gstack root
|
|
# GSTACK_SUPABASE_URL — override Supabase project URL
|
|
set -uo pipefail
|
|
|
|
GSTACK_DIR="${GSTACK_DIR:-$(cd "$(dirname "$0")/.." && pwd)}"
|
|
STATE_DIR="${GSTACK_STATE_DIR:-$HOME/.gstack}"
|
|
ANALYTICS_DIR="$STATE_DIR/analytics"
|
|
JSONL_FILE="$ANALYTICS_DIR/skill-usage.jsonl"
|
|
CURSOR_FILE="$ANALYTICS_DIR/.last-sync-line"
|
|
RATE_FILE="$ANALYTICS_DIR/.last-sync-time"
|
|
CONFIG_CMD="$GSTACK_DIR/bin/gstack-config"
|
|
|
|
# Source Supabase config if not overridden by env
|
|
if [ -z "${GSTACK_SUPABASE_URL:-}" ] && [ -f "$GSTACK_DIR/supabase/config.sh" ]; then
|
|
. "$GSTACK_DIR/supabase/config.sh"
|
|
fi
|
|
SUPABASE_URL="${GSTACK_SUPABASE_URL:-}"
|
|
ANON_KEY="${GSTACK_SUPABASE_ANON_KEY:-}"
|
|
|
|
# ─── Pre-checks ──────────────────────────────────────────────
|
|
# No Supabase URL configured yet → exit silently
|
|
[ -z "$SUPABASE_URL" ] && exit 0
|
|
|
|
# No JSONL file → nothing to sync
|
|
[ -f "$JSONL_FILE" ] || exit 0
|
|
|
|
# Rate limit: once per 5 minutes
|
|
if [ -f "$RATE_FILE" ]; then
|
|
STALE=$(find "$RATE_FILE" -mmin +5 2>/dev/null || true)
|
|
[ -z "$STALE" ] && exit 0
|
|
fi
|
|
|
|
# ─── Read tier ───────────────────────────────────────────────
|
|
TIER="$("$CONFIG_CMD" get telemetry 2>/dev/null || true)"
|
|
TIER="${TIER:-off}"
|
|
[ "$TIER" = "off" ] && exit 0
|
|
|
|
# ─── Read cursor ─────────────────────────────────────────────
|
|
CURSOR=0
|
|
if [ -f "$CURSOR_FILE" ]; then
|
|
CURSOR="$(cat "$CURSOR_FILE" 2>/dev/null | tr -d ' \n\r\t')"
|
|
# Validate: must be a non-negative integer
|
|
case "$CURSOR" in *[!0-9]*) CURSOR=0 ;; esac
|
|
fi
|
|
|
|
# Safety: if cursor exceeds file length, reset
|
|
TOTAL_LINES="$(wc -l < "$JSONL_FILE" | tr -d ' \n\r\t')"
|
|
if [ "$CURSOR" -gt "$TOTAL_LINES" ] 2>/dev/null; then
|
|
CURSOR=0
|
|
fi
|
|
|
|
# Nothing new to sync
|
|
[ "$CURSOR" -ge "$TOTAL_LINES" ] 2>/dev/null && exit 0
|
|
|
|
# ─── Read unsent lines ───────────────────────────────────────
|
|
SKIP=$(( CURSOR + 1 ))
|
|
UNSENT="$(tail -n "+$SKIP" "$JSONL_FILE" 2>/dev/null || true)"
|
|
[ -z "$UNSENT" ] && exit 0
|
|
|
|
# ─── Strip local-only fields and build batch ─────────────────
|
|
# Edge function expects raw JSONL field names (v, ts, sessions) —
|
|
# no column renaming needed (the function maps them internally).
|
|
BATCH="["
|
|
FIRST=true
|
|
COUNT=0
|
|
|
|
while IFS= read -r LINE; do
|
|
# Skip empty or malformed lines
|
|
[ -z "$LINE" ] && continue
|
|
echo "$LINE" | grep -q '^{' || continue
|
|
|
|
# Strip local-only fields (keep v, ts, sessions as-is for edge function)
|
|
CLEAN="$(echo "$LINE" | sed \
|
|
-e 's/,"_repo_slug":"[^"]*"//g' \
|
|
-e 's/,"_branch":"[^"]*"//g' \
|
|
-e 's/,"repo":"[^"]*"//g')"
|
|
|
|
# If anonymous tier, strip installation_id
|
|
if [ "$TIER" = "anonymous" ]; then
|
|
CLEAN="$(echo "$CLEAN" | sed 's/,"installation_id":"[^"]*"//g; s/,"installation_id":null//g')"
|
|
fi
|
|
|
|
if [ "$FIRST" = "true" ]; then
|
|
FIRST=false
|
|
else
|
|
BATCH="$BATCH,"
|
|
fi
|
|
BATCH="$BATCH$CLEAN"
|
|
COUNT=$(( COUNT + 1 ))
|
|
|
|
# Batch size limit
|
|
[ "$COUNT" -ge 100 ] && break
|
|
done <<< "$UNSENT"
|
|
|
|
BATCH="$BATCH]"
|
|
|
|
# Nothing to send after filtering
|
|
[ "$COUNT" -eq 0 ] && exit 0
|
|
|
|
# ─── POST to edge function ───────────────────────────────────
|
|
RESP_FILE="$(mktemp /tmp/gstack-sync-XXXXXX 2>/dev/null || echo "/tmp/gstack-sync-$$")"
|
|
HTTP_CODE="$(curl -s -w '%{http_code}' --max-time 10 \
|
|
-X POST "${SUPABASE_URL}/functions/v1/telemetry-ingest" \
|
|
-H "Content-Type: application/json" \
|
|
-H "apikey: ${ANON_KEY}" \
|
|
-o "$RESP_FILE" \
|
|
-d "$BATCH" 2>/dev/null || echo "000")"
|
|
|
|
# ─── Update cursor on success (2xx) ─────────────────────────
|
|
case "$HTTP_CODE" in
|
|
2*)
|
|
# Parse inserted count from response — only advance if events were actually inserted.
|
|
# Advance by SENT count (not inserted count) because we can't map inserted back to
|
|
# source lines. If inserted==0, something is systemically wrong — don't advance.
|
|
INSERTED="$(grep -o '"inserted":[0-9]*' "$RESP_FILE" 2>/dev/null | grep -o '[0-9]*' || echo "0")"
|
|
# Check for upsert errors (installation tracking failures) — log but don't block cursor advance
|
|
UPSERT_ERRORS="$(grep -o '"upsertErrors"' "$RESP_FILE" 2>/dev/null || true)"
|
|
if [ -n "$UPSERT_ERRORS" ]; then
|
|
echo "[gstack-telemetry-sync] Warning: installation upsert errors in response" >&2
|
|
fi
|
|
if [ "${INSERTED:-0}" -gt 0 ] 2>/dev/null; then
|
|
NEW_CURSOR=$(( CURSOR + COUNT ))
|
|
echo "$NEW_CURSOR" > "$CURSOR_FILE" 2>/dev/null || true
|
|
fi
|
|
;;
|
|
esac
|
|
|
|
rm -f "$RESP_FILE" 2>/dev/null || true
|
|
|
|
# Update rate limit marker
|
|
touch "$RATE_FILE" 2>/dev/null || true
|
|
|
|
exit 0
|