Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Changelog

## 0.4.1

### Fixed

- Card-number redaction and the card-number check skip digit runs inside hex, base64 and ids, and `x402.payment_payload` evidence is no longer redacted. Parts of x402 signatures could be replaced with `[redacted:pan]`, which then failed verification and raised a false `evidence_invalid` alert.

## 0.4.0

### Added
Expand Down
7 changes: 7 additions & 0 deletions halvorai_ingest/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,10 @@ def events_url(endpoint: str) -> str:
return base if base.endswith("/v1/events") else base + "/v1/events"


# JSON evidence that carries a signature over its own fields, so redaction would break it.
SIGNED_JSON_EVIDENCE = frozenset({"x402.payment_payload"})


@dataclass
class ClientStats:
emitted: int = 0
Expand Down Expand Up @@ -414,6 +418,9 @@ def _redact_event(self, event: dict[str, Any]) -> None:
evidence = event.get("evidence")
if isinstance(evidence, list):
for item in evidence:
# A signed x402 payload is evidence only while byte-identical.
if isinstance(item, dict) and item.get("kind") in SIGNED_JSON_EVIDENCE:
continue
if isinstance(item, dict) and item.get("format") == "json" and "value" in item:
item["value"] = redact_deep(item["value"])
item["sha256"] = evidence_sha256(item["value"])
Expand Down
3 changes: 2 additions & 1 deletion halvorai_ingest/redact.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@
r"\b(cvc|cvv2?|security code)(\"\s*:\s*\"|\s*[:=]\s*|\s+)(\d{3,4})(\")?",
re.IGNORECASE,
)
PAN_RUN = re.compile(r"\d(?:[ -]?\d)*")
# Digits glued to letters or more digits (hex signatures, base64, ids) are not a card number.
PAN_RUN = re.compile(r"(?<![0-9A-Za-z])\d(?:[ -]?\d)*(?![0-9A-Za-z])")

MAX_DEPTH = 64

Expand Down
3 changes: 2 additions & 1 deletion halvorai_ingest/validate.py
Original file line number Diff line number Diff line change
Expand Up @@ -302,7 +302,8 @@ class Related(_Strict):

def contains_pan(text: str) -> bool:
"""True when text holds a Luhn-valid run of 13 to 19 digits (spaces or dashes allowed)."""
for run in re.findall(r"\d(?:[ -]?\d)*", text):
# Same boundaries as the redaction: digits glued to letters (hex, base64, ids) are not a card.
for run in re.findall(r"(?<![0-9A-Za-z])\d(?:[ -]?\d)*(?![0-9A-Za-z])", text):
digits = re.sub(r"[ -]", "", run)
if 13 <= len(digits) <= 19 and _luhn(digits):
return True
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "halvorai-ingest"
version = "0.4.0"
version = "0.4.1"
description = "Signed, attributed payment events for AI agents. Never in the payment path."
readme = "README.md"
requires-python = ">=3.10"
Expand Down
13 changes: 13 additions & 0 deletions tests/test_redact.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,16 @@ def test_redacts_secret_key() -> None:
def test_redacts_cvc() -> None:
assert "[redacted:cvc]" in redact_text("cvc: 123")
assert "[redacted:cvc]" in redact_text('"cvc":"999"')


def test_leaves_digit_runs_inside_hex_base64_and_ids_alone() -> None:
for text in (
"0x9632e1311996ebcb38d7a4242424242424242b5beb14f513",
"c2lnbmF0dXJl4242424242424242AAA=",
"ord_4242424242424242x",
):
assert redact_text(text) == text


def test_still_finds_a_card_number_in_a_url_path() -> None:
assert redact_text("https://api.example/r/4242424242424242") == "https://api.example/r/[redacted:pan]"
2 changes: 1 addition & 1 deletion uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading