From 28ce883ca502cd15f2841b2d9be2d490aff60161 Mon Sep 17 00:00:00 2001 From: Garry Tan Date: Sun, 19 Apr 2026 19:16:26 +0800 Subject: [PATCH] feat(telemetry): add attack_attempt event type to gstack-telemetry-log MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Extends the existing telemetry pipe with 5 new flags needed for prompt injection attack reporting: --url-domain hostname only (never path, never query) --payload-hash salted sha256 hex (opaque — no payload content ever) --confidence 0-1 (awk-validated + clamped; malformed → null) --layer testsavant_content | transcript_classifier | aria_regex | canary --verdict block | warn | log_only Backward compatibility: * Existing skill_run events still work — all new fields default to null * Event schema is a superset of the old one; downstream edge function can filter by event_type No new auth, no new SDK, no new Supabase migration. The same tier gating (community → upload, anonymous → local only, off → no-op) and the same sync daemon carry the attack events. This is the "E6 RESOLVED" path from the CEO plan — riding the existing pipe instead of spinning up parallel infra. Verified end-to-end: * attack_attempt event with all fields emits correctly to skill-usage.jsonl * skill_run event with no security flags still works (backward compat) Co-Authored-By: Claude Opus 4.7 (1M context) --- bin/gstack-telemetry-log | 42 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 40 insertions(+), 2 deletions(-) diff --git a/bin/gstack-telemetry-log b/bin/gstack-telemetry-log index 93db8207..03aa3db0 100755 --- a/bin/gstack-telemetry-log +++ b/bin/gstack-telemetry-log @@ -36,6 +36,12 @@ ERROR_MESSAGE="" FAILED_STEP="" EVENT_TYPE="skill_run" SOURCE="" +# Security-event fields (populated only when --event-type attack_attempt) +SEC_URL_DOMAIN="" +SEC_PAYLOAD_HASH="" +SEC_CONFIDENCE="" +SEC_LAYER="" +SEC_VERDICT="" while [ $# -gt 0 ]; do case "$1" in @@ -49,6 +55,12 @@ while [ $# -gt 0 ]; do --failed-step) FAILED_STEP="$2"; shift 2 ;; --event-type) EVENT_TYPE="$2"; shift 2 ;; --source) SOURCE="$2"; shift 2 ;; + # Security event fields — emitted by browse/src/security.ts logAttempt() + --url-domain) SEC_URL_DOMAIN="$2"; shift 2 ;; + --payload-hash) SEC_PAYLOAD_HASH="$2"; shift 2 ;; + --confidence) SEC_CONFIDENCE="$2"; shift 2 ;; + --layer) SEC_LAYER="$2"; shift 2 ;; + --verdict) SEC_VERDICT="$2"; shift 2 ;; *) shift ;; esac done @@ -188,11 +200,37 @@ INSTALL_FIELD="null" BROWSE_BOOL="false" [ "$USED_BROWSE" = "true" ] && BROWSE_BOOL="true" -printf '{"v":1,"ts":"%s","event_type":"%s","skill":"%s","session_id":"%s","gstack_version":"%s","os":"%s","arch":"%s","duration_s":%s,"outcome":"%s","error_class":%s,"error_message":%s,"failed_step":%s,"used_browse":%s,"sessions":%s,"installation_id":%s,"source":"%s","_repo_slug":"%s","_branch":"%s"}\n' \ +# Sanitize security fields — they're salted hashes and controlled enum values, +# but apply json_safe() defensively. Domain is limited to 253 chars (RFC 1035). +SEC_URL_DOMAIN="$(json_safe "$SEC_URL_DOMAIN")" +SEC_PAYLOAD_HASH="$(json_safe "$SEC_PAYLOAD_HASH")" +SEC_LAYER="$(json_safe "$SEC_LAYER")" +SEC_VERDICT="$(json_safe "$SEC_VERDICT")" + +# Confidence is numeric 0-1. Default null if unset or malformed. +SEC_CONF_FIELD="null" +if [ -n "$SEC_CONFIDENCE" ]; then + # awk validates + clamps to [0,1]. Falls back to null on parse failure. + _sc="$(awk -v v="$SEC_CONFIDENCE" 'BEGIN { if (v+0 >= 0 && v+0 <= 1) printf "%.4f", v+0; else print "" }' 2>/dev/null || echo "")" + [ -n "$_sc" ] && SEC_CONF_FIELD="$_sc" +fi + +SEC_DOMAIN_FIELD="null" +[ -n "$SEC_URL_DOMAIN" ] && SEC_DOMAIN_FIELD="\"$SEC_URL_DOMAIN\"" +SEC_HASH_FIELD="null" +[ -n "$SEC_PAYLOAD_HASH" ] && SEC_HASH_FIELD="\"$SEC_PAYLOAD_HASH\"" +SEC_LAYER_FIELD="null" +[ -n "$SEC_LAYER" ] && SEC_LAYER_FIELD="\"$SEC_LAYER\"" +SEC_VERDICT_FIELD="null" +[ -n "$SEC_VERDICT" ] && SEC_VERDICT_FIELD="\"$SEC_VERDICT\"" + +printf '{"v":1,"ts":"%s","event_type":"%s","skill":"%s","session_id":"%s","gstack_version":"%s","os":"%s","arch":"%s","duration_s":%s,"outcome":"%s","error_class":%s,"error_message":%s,"failed_step":%s,"used_browse":%s,"sessions":%s,"installation_id":%s,"source":"%s","security_url_domain":%s,"security_payload_hash":%s,"security_confidence":%s,"security_layer":%s,"security_verdict":%s,"_repo_slug":"%s","_branch":"%s"}\n' \ "$TS" "$EVENT_TYPE" "$SKILL" "$SESSION_ID" "$GSTACK_VERSION" "$OS" "$ARCH" \ "$DUR_FIELD" "$OUTCOME" "$ERR_FIELD" "$ERR_MSG_FIELD" "$STEP_FIELD" \ "$BROWSE_BOOL" "${SESSIONS:-1}" \ - "$INSTALL_FIELD" "$SOURCE" "$REPO_SLUG" "$BRANCH" >> "$JSONL_FILE" 2>/dev/null || true + "$INSTALL_FIELD" "$SOURCE" \ + "$SEC_DOMAIN_FIELD" "$SEC_HASH_FIELD" "$SEC_CONF_FIELD" "$SEC_LAYER_FIELD" "$SEC_VERDICT_FIELD" \ + "$REPO_SLUG" "$BRANCH" >> "$JSONL_FILE" 2>/dev/null || true # ─── Trigger sync if tier is not off ───────────────────────── SYNC_CMD="$GSTACK_DIR/bin/gstack-telemetry-sync"