celik-muhammed commited on
Commit
cb788eb
·
verified ·
1 Parent(s): 6805f0e

Upload 8 files

Browse files
Files changed (4) hide show
  1. _dataset_schema.py +7 -0
  2. _shared_logic.py +215 -2
  3. app.py +17 -12
  4. deduplicate_dataset.py +33 -6
_dataset_schema.py CHANGED
@@ -128,6 +128,13 @@ Schema version history
128
  all v2-only fields default via ``setdefault`` (``feedbackId=None``,
129
  ``prevFeedbackId=None``, ``editCount=0``).
130
 
 
 
 
 
 
 
 
131
  References
132
  ----------
133
  See ``DATASET_COLLECTION_GUIDANCE.md`` for the deduplication contract, the
 
128
  all v2-only fields default via ``setdefault`` (``feedbackId=None``,
129
  ``prevFeedbackId=None``, ``editCount=0``).
130
 
131
+ Security note
132
+ Client IP addresses are **never stored** in dataset records. The proxy
133
+ (``app.py``) passes every ``client_ip`` through ``_mask_ip()`` before any
134
+ log entry is written, and the normalisation functions in this module
135
+ receive only the sanitised JSON payload — the raw IP never enters any
136
+ field accepted or emitted by this module.
137
+
138
  References
139
  ----------
140
  See ``DATASET_COLLECTION_GUIDANCE.md`` for the deduplication contract, the
_shared_logic.py CHANGED
@@ -1,4 +1,4 @@
1
- # _shared_logic.py v6.1.0
2
  #
3
  # Single source of truth for shared constants, pure helper functions, and
4
  # type aliases used by the deployed proxy (_hf_spaces_proxy/app.py) and the
@@ -152,9 +152,11 @@ deployed Spaces and log aggregators can correlate errors to a specific release.
152
 
153
  from __future__ import annotations
154
 
 
155
  import json
156
  import logging
157
  import os
 
158
  from typing import Any, Literal
159
 
160
  logger = logging.getLogger(__name__)
@@ -186,6 +188,10 @@ __all__ = [ # noqa: RUF022
186
  "_safe_float",
187
  "_safe_int",
188
  "_token_log_fragment",
 
 
 
 
189
  # Helpers — token type system (v6.1.0)
190
  "_classify_token_type",
191
  "_token_suitable_for_inference",
@@ -203,7 +209,7 @@ __all__ = [ # noqa: RUF022
203
  # ─────────────────────────────────────────────────────────────────────────────
204
 
205
  #: Proxy release version — bump on every breaking change.
206
- PROXY_VERSION: str = "6.1.0"
207
 
208
  #: HuggingFace Inference Providers router base URL (no trailing slash).
209
  #: Only used for Path 3 (standard provider models) when ``BACKEND_URL`` is
@@ -611,6 +617,213 @@ def _token_log_fragment(token: str, token_type: str = "") -> str:
611
  return fragment
612
 
613
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
614
  def _classify_token_type(
615
  token: str,
616
  declared_type: str | None = None,
 
1
+ # _shared_logic.py v6.2.0
2
  #
3
  # Single source of truth for shared constants, pure helper functions, and
4
  # type aliases used by the deployed proxy (_hf_spaces_proxy/app.py) and the
 
152
 
153
  from __future__ import annotations
154
 
155
+ import ipaddress
156
  import json
157
  import logging
158
  import os
159
+ import re
160
  from typing import Any, Literal
161
 
162
  logger = logging.getLogger(__name__)
 
188
  "_safe_float",
189
  "_safe_int",
190
  "_token_log_fragment",
191
+ # Privacy / log-redaction (v6.2.0)
192
+ "_REDACT_PATTERNS",
193
+ "_RedactingFilter",
194
+ "_mask_ip",
195
  # Helpers — token type system (v6.1.0)
196
  "_classify_token_type",
197
  "_token_suitable_for_inference",
 
209
  # ─────────────────────────────────────────────────────────────────────────────
210
 
211
  #: Proxy release version — bump on every breaking change.
212
+ PROXY_VERSION: str = "6.2.0"
213
 
214
  #: HuggingFace Inference Providers router base URL (no trailing slash).
215
  #: Only used for Path 3 (standard provider models) when ``BACKEND_URL`` is
 
617
  return fragment
618
 
619
 
620
+ # ─────────────────────────────────────────────────────────────────────────────
621
+ # Privacy / log-redaction helpers (v6.2.0)
622
+ # ─────────────────────────────────────────────────────────────────────────────
623
+ #
624
+ # Design rationale
625
+ # ----------------
626
+ # Two complementary layers protect PII in log output:
627
+ #
628
+ # Layer 1 — call-site masking via :func:`_mask_ip`
629
+ # Every ``json.dumps({..., "ip": ...})`` call in ``app.py`` passes
630
+ # ``client_ip`` through :func:`_mask_ip` before it is serialised.
631
+ # This is the PRIMARY control: the raw IP never enters the log string.
632
+ #
633
+ # Layer 2 — defence-in-depth via :class:`_RedactingFilter`
634
+ # Attached to the root logging handler. Applies :data:`_REDACT_PATTERNS`
635
+ # to the fully formatted message BEFORE it is emitted. Catches:
636
+ # • HF token strings leaked via exception messages from
637
+ # ``huggingface_hub`` (e.g. ``snapshot_download`` auth failures).
638
+ # • IPv4 addresses emitted by third-party library loggers (httpx,
639
+ # uvicorn) that bypass the call-site masking.
640
+ # • Any future code that forgets to call :func:`_mask_ip` first.
641
+ #
642
+ # IPv6 is handled exclusively at Layer 1 (:func:`_mask_ip`). A generic
643
+ # IPv6 regex in Layer 2 has unacceptable false-positive rates (e.g. it
644
+ # would match ``12:34:56:78`` in log timestamps or MAC addresses).
645
+ # ─────────────────────────────────────────────────────────────────────────────
646
+
647
+
648
+ def _mask_ip(ip: str) -> str:
649
+ """Mask a client IP address for privacy-safe log output.
650
+
651
+ Preserves enough network context for rate-limit and abuse analysis while
652
+ zeroing the host portion that identifies the individual user.
653
+
654
+ * **IPv4** — zero the last octet, retaining the /24 subnet.
655
+ ``"192.168.1.100"`` → ``"192.168.1.0"``
656
+ * **IPv6** — zero the interface identifier (last 64 bits), retaining
657
+ the /64 prefix. ``"2001:db8:85a3::8a2e:370:7334"`` → ``"2001:db8:85a3::"``
658
+ * **IPv6 scope suffix** (e.g. ``"fe80::1%eth0"``) — stripped before
659
+ parsing (Python's :mod:`ipaddress` does not accept scope identifiers).
660
+ * **Non-IP strings** — returned as ``"<ip-redacted>"``.
661
+ * **Sentinel** ``"unknown"`` — returned unchanged (already non-identifying).
662
+
663
+ Parameters
664
+ ----------
665
+ ip : str
666
+ Client IP string extracted from the HTTP request headers.
667
+ May be ``"unknown"`` when the proxy header is absent.
668
+
669
+ Returns
670
+ -------
671
+ str
672
+ Masked IP suitable for structured log output. This function is
673
+ intentionally never-raise — any :exc:`ValueError` from
674
+ :mod:`ipaddress` is caught and replaced by the safe fallback.
675
+
676
+ Notes
677
+ -----
678
+ **Security note** — This is the canonical privacy gate for all IP values
679
+ written to log records in ``app.py``. Every ``json.dumps({..., "ip": …})``
680
+ call must pass ``client_ip`` through :func:`_mask_ip` before serialising.
681
+ Callers must **not** write raw ``client_ip`` values to any log record.
682
+
683
+ **Developer note** — Uses :mod:`ipaddress` from the Python standard
684
+ library; no third-party dependencies are introduced.
685
+
686
+ Examples
687
+ --------
688
+ >>> _mask_ip("192.168.1.100")
689
+ '192.168.1.0'
690
+ >>> _mask_ip("10.0.0.255")
691
+ '10.0.0.0'
692
+ >>> _mask_ip("2001:db8:85a3::8a2e:370:7334")
693
+ '2001:db8:85a3::'
694
+ >>> _mask_ip("fe80::1%eth0")
695
+ 'fe80::'
696
+ >>> _mask_ip("unknown")
697
+ 'unknown'
698
+ >>> _mask_ip("not-an-ip")
699
+ '<ip-redacted>'
700
+ """
701
+ if ip in ("unknown", ""):
702
+ return ip
703
+ try:
704
+ # Strip IPv6 zone/scope identifier (e.g. "%eth0") — ipaddress rejects it.
705
+ clean: str = ip.split("%", 1)[0].strip()
706
+ addr = ipaddress.ip_address(clean)
707
+ if isinstance(addr, ipaddress.IPv4Address):
708
+ # Retain /24 (first three octets); zero the host octet.
709
+ return str(
710
+ ipaddress.ip_network(f"{addr}/24", strict=False).network_address
711
+ )
712
+ # IPv6: retain /64 prefix; zero the 64-bit interface identifier.
713
+ return str(
714
+ ipaddress.ip_network(f"{addr}/64", strict=False).network_address
715
+ )
716
+ except ValueError:
717
+ return "<ip-redacted>"
718
+
719
+
720
+ #: Ordered list of ``(compiled_pattern, replacement)`` tuples applied by
721
+ #: :class:`_RedactingFilter` to every log record before emission.
722
+ #:
723
+ #: **Pattern order matters** — patterns are applied left-to-right; more
724
+ #: specific patterns must precede catch-all patterns. There is no overlap
725
+ #: between the current patterns, but this convention must be maintained when
726
+ #: extending this list.
727
+ #:
728
+ #: IPv6 addresses are intentionally **absent** — they are handled at the
729
+ #: call-site by :func:`_mask_ip` (Layer 1). A generic IPv6 regex in a
730
+ #: global filter produces too many false positives (hex timestamps, MAC
731
+ #: addresses, Docker overlay IDs) to be safe in a production log stream.
732
+ _REDACT_PATTERNS: list[tuple[re.Pattern[str], str]] = [
733
+ # HuggingFace API tokens — ``hf_`` prefix followed by ≥ 4 alphanumeric
734
+ # characters. Classic tokens are ~34 chars; fine-grained tokens are ≥ 52.
735
+ # The {4,} lower bound avoids matching ``hf_`` in legitimate identifiers
736
+ # (e.g. Python identifiers that start with ``hf_``) while still catching
737
+ # any partial token fragment that huggingface_hub may embed in an error
738
+ # message.
739
+ (re.compile(r"\bhf_[a-zA-Z0-9]{4,}\b"), "<token-redacted>"),
740
+ # IPv4 addresses — strict dotted-decimal notation with per-octet range
741
+ # validation (0–255). Word boundaries prevent partial matches inside
742
+ # longer numeric strings. This pattern catches IPv4 strings emitted by
743
+ # third-party loggers (httpx, uvicorn) that bypass :func:`_mask_ip`.
744
+ (
745
+ re.compile(
746
+ r"\b(?:(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}"
747
+ r"(?:25[0-5]|2[0-4]\d|[01]?\d\d?)\b"
748
+ ),
749
+ "<ipv4-redacted>",
750
+ ),
751
+ ]
752
+
753
+
754
+ class _RedactingFilter(logging.Filter):
755
+ """Scrub sensitive values from log records before emission.
756
+
757
+ Applies the regex patterns in :data:`_REDACT_PATTERNS` to the fully
758
+ formatted log message, replacing HuggingFace API tokens and raw IPv4
759
+ addresses with opaque placeholders.
760
+
761
+ This class is the **defence-in-depth layer** (Layer 2). The primary
762
+ control is :func:`_mask_ip` at each call site (Layer 1). The filter
763
+ catches values that slip through Layer 1 — most importantly, HF token
764
+ strings embedded in exception messages from ``huggingface_hub``.
765
+
766
+ Parameters
767
+ ----------
768
+ name : str, optional
769
+ Filter name forwarded to :class:`logging.Filter`. Default ``""``.
770
+
771
+ Notes
772
+ -----
773
+ **Security note** — This filter materialises the fully formatted message
774
+ via :meth:`logging.LogRecord.getMessage`, applies every pattern in
775
+ :data:`_REDACT_PATTERNS`, then replaces :attr:`~logging.LogRecord.msg`
776
+ with the scrubbed result and clears :attr:`~logging.LogRecord.args`.
777
+ Clearing ``args`` prevents downstream handlers from re-applying ``%``
778
+ formatting to a string that no longer contains positional placeholders.
779
+
780
+ **Developer note** — Attach to the root handler immediately after
781
+ construction so **every** handler in the process benefits::
782
+
783
+ handler = logging.StreamHandler()
784
+ handler.addFilter(_RedactingFilter())
785
+ logging.root.handlers = [handler]
786
+
787
+ To extend the redaction vocabulary, append a ``(pattern, replacement)``
788
+ tuple to :data:`_REDACT_PATTERNS`.
789
+
790
+ Examples
791
+ --------
792
+ >>> import logging
793
+ >>> f = _RedactingFilter()
794
+ >>> rec = logging.makeLogRecord({"msg": "token=hf_abc1234defg5678 ip=10.0.1.99", "args": ()})
795
+ >>> f.filter(rec)
796
+ True
797
+ >>> rec.msg
798
+ 'token=<token-redacted> ip=<ipv4-redacted>'
799
+ """
800
+
801
+ def filter(self, record: logging.LogRecord) -> bool: # noqa: A003
802
+ """Redact sensitive patterns from *record*'s formatted message.
803
+
804
+ Parameters
805
+ ----------
806
+ record : logging.LogRecord
807
+ Log record to inspect and mutate in-place.
808
+
809
+ Returns
810
+ -------
811
+ bool
812
+ Always ``True`` — this filter never suppresses records, only
813
+ scrubs their message content.
814
+ """
815
+ # Materialise the full %-formatted string first, then scrub it.
816
+ msg: str = record.getMessage()
817
+ for pattern, replacement in _REDACT_PATTERNS:
818
+ msg = pattern.sub(replacement, msg)
819
+ # Write the scrubbed text back and clear args so that any subsequent
820
+ # call to getMessage() returns the already-scrubbed string without
821
+ # attempting to re-apply % formatting.
822
+ record.msg = msg
823
+ record.args = ()
824
+ return True
825
+
826
+
827
  def _classify_token_type(
828
  token: str,
829
  declared_type: str | None = None,
app.py CHANGED
@@ -5,7 +5,7 @@
5
  # Authors: The scikit-plots developers
6
  # SPDX-License-Identifier: BSD-3-Clause
7
  #
8
- # scikit-plots/ai · _hf_spaces_proxy/app.py v6.1.0
9
  #
10
  # Thin OpenAI-compatible reverse proxy for sphinx-ai-assistant.
11
  #
@@ -159,7 +159,9 @@ try:
159
  DEFAULT_PATH3_READ_TIMEOUT,
160
  DEFAULT_PROXY_TIMEOUT,
161
  PROXY_VERSION,
 
162
  _classify_token_type,
 
163
  _resolve_upstream_url,
164
  _safe_float,
165
  _safe_int,
@@ -167,7 +169,7 @@ try:
167
  _validate_env,
168
  _validate_token_config,
169
  )
170
- except Exception as e:
171
  from _shared_logic import ( # type: ignore[import]
172
  DEFAULT_HF_BASE,
173
  DEFAULT_HF_SPACES_MODEL_NAMESPACES,
@@ -178,7 +180,9 @@ except Exception as e:
178
  DEFAULT_PATH3_READ_TIMEOUT,
179
  DEFAULT_PROXY_TIMEOUT,
180
  PROXY_VERSION,
 
181
  _classify_token_type,
 
182
  _resolve_upstream_url,
183
  _safe_float,
184
  _safe_int,
@@ -257,6 +261,7 @@ class _StructuredFormatter(logging.Formatter):
257
 
258
  _handler = logging.StreamHandler()
259
  _handler.setFormatter(_StructuredFormatter())
 
260
  logging.root.handlers = [_handler]
261
  logging.root.setLevel(logging.INFO)
262
  logger = logging.getLogger(__name__)
@@ -1246,7 +1251,7 @@ async def contribute(request: Request) -> JSONResponse: # noqa: PLR0912
1246
  _contrib_rl[client_ip] = (count, window_start)
1247
  if count > 5: # noqa: PLR2004
1248
  logger.warning(
1249
- json.dumps({"event": "contribute.ratelimit", "ip": client_ip})
1250
  )
1251
  raise HTTPException(
1252
  status_code=429,
@@ -1355,7 +1360,7 @@ async def contribute(request: Request) -> JSONResponse: # noqa: PLR0912
1355
  ) from exc
1356
 
1357
  logger.info(
1358
- json.dumps({"event": "contribute.write", "rows": len(records), "ip": client_ip})
1359
  )
1360
  return JSONResponse({"contributed": True, "rows": len(records)})
1361
 
@@ -1436,7 +1441,7 @@ async def share(request: Request) -> JSONResponse:
1436
  count += 1
1437
  _share_rl[client_ip] = (count, window_start)
1438
  if count > 10: # noqa: PLR2004
1439
- logger.warning(json.dumps({"event": "share.ratelimit", "ip": client_ip}))
1440
  raise HTTPException(
1441
  status_code=429,
1442
  detail="Rate limit exceeded. Maximum 10 shares per hour.",
@@ -1469,7 +1474,7 @@ async def share(request: Request) -> JSONResponse:
1469
  {
1470
  "event": "share.create",
1471
  "id": share_id,
1472
- "ip": client_ip,
1473
  "ttl_days": ttl_days,
1474
  }
1475
  )
@@ -1613,7 +1618,7 @@ async def share_patch(share_id: str, request: Request) -> JSONResponse:
1613
  count += 1
1614
  _share_rl[client_ip] = (count, window_start)
1615
  if count > 10: # noqa: PLR2004
1616
- logger.warning(json.dumps({"event": "share.ratelimit", "ip": client_ip}))
1617
  raise HTTPException(
1618
  status_code=429,
1619
  detail="Rate limit exceeded. Maximum 10 shares per hour.",
@@ -1660,7 +1665,7 @@ async def share_patch(share_id: str, request: Request) -> JSONResponse:
1660
  {
1661
  "event": "share.update",
1662
  "id": share_id,
1663
- "ip": client_ip,
1664
  "ttl_days": ttl_days,
1665
  }
1666
  )
@@ -1778,7 +1783,7 @@ async def feedback(request: Request) -> JSONResponse:
1778
  json.dumps(
1779
  {
1780
  "event": "feedback.retract",
1781
- "ip": client_ip,
1782
  "prevSessionId": payload.get("prevSessionId"),
1783
  "conversationId": payload.get("conversationId"),
1784
  "answerIndex": payload.get("answerIndex"),
@@ -1807,7 +1812,7 @@ async def feedback(request: Request) -> JSONResponse:
1807
  _feedback_rl[client_ip] = (count, window_start)
1808
  if count > 30: # noqa: PLR2004
1809
  logger.warning(
1810
- json.dumps({"event": "feedback.ratelimit", "ip": client_ip})
1811
  )
1812
  raise HTTPException(
1813
  status_code=429,
@@ -1819,7 +1824,7 @@ async def feedback(request: Request) -> JSONResponse:
1819
  json.dumps(
1820
  {
1821
  "event": "feedback.receive",
1822
- "ip": client_ip,
1823
  "ratingValue": payload.get("ratingValue"),
1824
  "model": payload.get("model"),
1825
  "conversationId": payload.get("conversationId"),
@@ -1898,7 +1903,7 @@ async def feedback(request: Request) -> JSONResponse:
1898
  "event": "feedback.persist_ok",
1899
  "retract": is_retract,
1900
  "dedup_key": f"{conversation_id}:{answer_index}",
1901
- "ip": client_ip,
1902
  }
1903
  )
1904
  )
 
5
  # Authors: The scikit-plots developers
6
  # SPDX-License-Identifier: BSD-3-Clause
7
  #
8
+ # scikit-plots/ai · _hf_spaces_proxy/app.py v6.2.0
9
  #
10
  # Thin OpenAI-compatible reverse proxy for sphinx-ai-assistant.
11
  #
 
159
  DEFAULT_PATH3_READ_TIMEOUT,
160
  DEFAULT_PROXY_TIMEOUT,
161
  PROXY_VERSION,
162
+ _RedactingFilter,
163
  _classify_token_type,
164
+ _mask_ip,
165
  _resolve_upstream_url,
166
  _safe_float,
167
  _safe_int,
 
169
  _validate_env,
170
  _validate_token_config,
171
  )
172
+ except Exception:
173
  from _shared_logic import ( # type: ignore[import]
174
  DEFAULT_HF_BASE,
175
  DEFAULT_HF_SPACES_MODEL_NAMESPACES,
 
180
  DEFAULT_PATH3_READ_TIMEOUT,
181
  DEFAULT_PROXY_TIMEOUT,
182
  PROXY_VERSION,
183
+ _RedactingFilter,
184
  _classify_token_type,
185
+ _mask_ip,
186
  _resolve_upstream_url,
187
  _safe_float,
188
  _safe_int,
 
261
 
262
  _handler = logging.StreamHandler()
263
  _handler.setFormatter(_StructuredFormatter())
264
+ _handler.addFilter(_RedactingFilter()) # scrub tokens/IPs before emission
265
  logging.root.handlers = [_handler]
266
  logging.root.setLevel(logging.INFO)
267
  logger = logging.getLogger(__name__)
 
1251
  _contrib_rl[client_ip] = (count, window_start)
1252
  if count > 5: # noqa: PLR2004
1253
  logger.warning(
1254
+ json.dumps({"event": "contribute.ratelimit", "ip": _mask_ip(client_ip)})
1255
  )
1256
  raise HTTPException(
1257
  status_code=429,
 
1360
  ) from exc
1361
 
1362
  logger.info(
1363
+ json.dumps({"event": "contribute.write", "rows": len(records), "ip": _mask_ip(client_ip)})
1364
  )
1365
  return JSONResponse({"contributed": True, "rows": len(records)})
1366
 
 
1441
  count += 1
1442
  _share_rl[client_ip] = (count, window_start)
1443
  if count > 10: # noqa: PLR2004
1444
+ logger.warning(json.dumps({"event": "share.ratelimit", "ip": _mask_ip(client_ip)}))
1445
  raise HTTPException(
1446
  status_code=429,
1447
  detail="Rate limit exceeded. Maximum 10 shares per hour.",
 
1474
  {
1475
  "event": "share.create",
1476
  "id": share_id,
1477
+ "ip": _mask_ip(client_ip),
1478
  "ttl_days": ttl_days,
1479
  }
1480
  )
 
1618
  count += 1
1619
  _share_rl[client_ip] = (count, window_start)
1620
  if count > 10: # noqa: PLR2004
1621
+ logger.warning(json.dumps({"event": "share.ratelimit", "ip": _mask_ip(client_ip)}))
1622
  raise HTTPException(
1623
  status_code=429,
1624
  detail="Rate limit exceeded. Maximum 10 shares per hour.",
 
1665
  {
1666
  "event": "share.update",
1667
  "id": share_id,
1668
+ "ip": _mask_ip(client_ip),
1669
  "ttl_days": ttl_days,
1670
  }
1671
  )
 
1783
  json.dumps(
1784
  {
1785
  "event": "feedback.retract",
1786
+ "ip": _mask_ip(client_ip),
1787
  "prevSessionId": payload.get("prevSessionId"),
1788
  "conversationId": payload.get("conversationId"),
1789
  "answerIndex": payload.get("answerIndex"),
 
1812
  _feedback_rl[client_ip] = (count, window_start)
1813
  if count > 30: # noqa: PLR2004
1814
  logger.warning(
1815
+ json.dumps({"event": "feedback.ratelimit", "ip": _mask_ip(client_ip)})
1816
  )
1817
  raise HTTPException(
1818
  status_code=429,
 
1824
  json.dumps(
1825
  {
1826
  "event": "feedback.receive",
1827
+ "ip": _mask_ip(client_ip),
1828
  "ratingValue": payload.get("ratingValue"),
1829
  "model": payload.get("model"),
1830
  "conversationId": payload.get("conversationId"),
 
1903
  "event": "feedback.persist_ok",
1904
  "retract": is_retract,
1905
  "dedup_key": f"{conversation_id}:{answer_index}",
1906
+ "ip": _mask_ip(client_ip),
1907
  }
1908
  )
1909
  )
deduplicate_dataset.py CHANGED
@@ -59,12 +59,22 @@ from __future__ import annotations
59
  import argparse
60
  import json
61
  import logging
 
62
  import sys
63
  from pathlib import Path
64
  from typing import Any
65
 
66
  logger = logging.getLogger(__name__)
67
 
 
 
 
 
 
 
 
 
 
68
  # Optional: normalize records from v1 to v2 schema when _dataset_schema is
69
  # available alongside this script (standard _hf_spaces_proxy/ deployment).
70
  # Falls back to identity function with a warning for standalone usage.
@@ -359,6 +369,14 @@ def _configure_logging() -> None:
359
  err_handler.setFormatter(level_fmt)
360
  err_handler.setLevel(logging.WARNING)
361
 
 
 
 
 
 
 
 
 
362
  root = logging.getLogger()
363
  root.handlers = [out_handler, err_handler]
364
  root.setLevel(logging.DEBUG)
@@ -416,13 +434,22 @@ def main(argv: list[str] | None = None) -> int:
416
  )
417
  return 1
418
  logger.info("Downloading %s ...", args.repo_id)
419
- local_dir = Path(
420
- snapshot_download(
421
- repo_id=args.repo_id,
422
- repo_type="dataset",
423
- token=args.token,
 
 
424
  )
425
- )
 
 
 
 
 
 
 
426
 
427
  logger.info("Reading records from %s ...", local_dir)
428
  all_records = load_all_records(local_dir)
 
59
  import argparse
60
  import json
61
  import logging
62
+ import re
63
  import sys
64
  from pathlib import Path
65
  from typing import Any
66
 
67
  logger = logging.getLogger(__name__)
68
 
69
+ # Optional: import _RedactingFilter from _shared_logic when available so that
70
+ # HF token strings embedded in exception messages (e.g. snapshot_download auth
71
+ # failures) are scrubbed from CLI log output. Safe no-op fallback for
72
+ # standalone usage where _shared_logic.py is absent.
73
+ try:
74
+ from _shared_logic import _RedactingFilter as _REDACTING_FILTER_CLS
75
+ except ImportError:
76
+ _REDACTING_FILTER_CLS = None # type: ignore[assignment,misc]
77
+
78
  # Optional: normalize records from v1 to v2 schema when _dataset_schema is
79
  # available alongside this script (standard _hf_spaces_proxy/ deployment).
80
  # Falls back to identity function with a warning for standalone usage.
 
369
  err_handler.setFormatter(level_fmt)
370
  err_handler.setLevel(logging.WARNING)
371
 
372
+ # Attach defence-in-depth redaction filter when _shared_logic is available.
373
+ # Scrubs HF token strings that huggingface_hub may embed in auth-error
374
+ # messages before they are emitted to stderr. No-op when absent.
375
+ if _REDACTING_FILTER_CLS is not None:
376
+ _rf = _REDACTING_FILTER_CLS()
377
+ out_handler.addFilter(_rf)
378
+ err_handler.addFilter(_rf)
379
+
380
  root = logging.getLogger()
381
  root.handlers = [out_handler, err_handler]
382
  root.setLevel(logging.DEBUG)
 
434
  )
435
  return 1
436
  logger.info("Downloading %s ...", args.repo_id)
437
+ try:
438
+ local_dir = Path(
439
+ snapshot_download(
440
+ repo_id=args.repo_id,
441
+ repo_type="dataset",
442
+ token=args.token,
443
+ )
444
  )
445
+ except Exception as exc: # noqa: BLE001
446
+ logger.error(
447
+ "Failed to download %s: %s\n"
448
+ "Hint: pass --token <HF_READ_TOKEN> or set HF_TOKEN in your environment.",
449
+ args.repo_id,
450
+ exc,
451
+ )
452
+ return 1
453
 
454
  logger.info("Reading records from %s ...", local_dir)
455
  all_records = load_all_records(local_dir)