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
11 changes: 9 additions & 2 deletions .github/workflows/sanitization.yml
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,20 @@ jobs:
with:
python-version: "3.12"

- name: Install semantic-layer dep
run: pip install anthropic
- name: Install deps (semantic layer + self-tests)
run: pip install anthropic pytest

- name: Scanner self-tests (deterministic, no key needed)
run: python -m pytest tests/test_check_sanitization.py -q

- name: Sanitization gate (diff)
# The semantic (LLM) layer runs automatically when ANTHROPIC_API_KEY is set
# as a repo Actions secret — strongly recommended, since it's the layer that
# catches engagement particulars the deterministic regexes can't. Without the
# key it skips with a loud warning (deterministic layer still hard-fails on
# secrets/PII). To make the semantic layer MANDATORY once your key is wired,
# append `--require-semantic` below: the gate then fails closed if the key is
# ever missing instead of silently passing deterministic-only.
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
Expand Down
11 changes: 8 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,14 @@ flag is **not** an automatic rejection — it routes the PR to a human maintaine
makes the call. If your PR is flagged and you believe it's clean, say so in the PR
description; a maintainer reviews the flagged content and decides.

The semantic layer runs when `ANTHROPIC_API_KEY` is set in CI secrets. Without the
key it skips with a warning (and the `--require-semantic` flag turns that skip into a
hard failure, for maintainers who need the full gate in CI).
The semantic layer needs `ANTHROPIC_API_KEY`. Set it as a repo Actions secret (this is
on the onboarding checklist) so the layer runs in CI — it's the part that catches
engagement particulars the regexes can't. Without the key it skips with a loud warning
(the deterministic layer still hard-fails on secrets/PII), so local/offline runs work.
**Once your key is wired, make the semantic layer mandatory** by appending
`--require-semantic` to the gate step in `.github/workflows/sanitization.yml`: the gate
then fails closed if the key is ever missing, instead of silently passing
deterministic-only.

To tune what counts as a particular for this domain, edit `sanitize.config.json` —
that is the single configuration surface for the gate. See the inline comments in that
Expand Down
1 change: 1 addition & 0 deletions sanitize.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
"personal names of subjects, clients, targets, or end-users tied to a specific engagement",
"specific organization/company names tied to one active engagement (not generic/public reference)",
"document IDs, file names, dataset names, account handles, or URLs specific to one engagement",
"a messaging or account identifier tied to a real person — a Signal/Telegram UUID, a contact phone, an allowlisted user ID (these belong in deployment config/secrets, never in the package)",
"a codename or label that identifies one particular engagement (e.g. used as a skill name or heading)",
"dates, locations, monetary figures, or case details that only make sense for one engagement"
],
Expand Down
3 changes: 3 additions & 0 deletions scripts/check_sanitization.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,9 @@ def load_config(root="."):
("private-key-block", re.compile(r"-----BEGIN (?:RSA |EC |OPENSSH |DSA |PGP )?PRIVATE KEY-----")),
("email", re.compile(r"\b[A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}\b")),
("us-phone", re.compile(r"(?<!\d)(?:\+?1[\s.\-]?)?\(?\d{3}\)?[\s.\-]\d{3}[\s.\-]\d{4}(?!\d)")),
# E.164 international (e.g. a contact wired into config/SOUL): "+" + 7–15 digits,
# no separators. The us-phone pattern only covers North-American formatting.
("intl-phone", re.compile(r"(?<!\d)\+\d{7,15}(?!\d)")),
("ipv4", re.compile(r"(?<!\d)(?:\d{1,3}\.){3}\d{1,3}(?!\d)")),
]

Expand Down
8 changes: 8 additions & 0 deletions tests/test_check_sanitization.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ def test_detects_ipv4_but_allows_loopback():
assert not any(l == "ipv4" for l, _ in cs.scan_deterministic("bind 127.0.0.1", ["127.0.0.1"]))


def test_detects_international_phone():
# E.164 form with no separators — the us-phone pattern misses these entirely.
assert any(l == "intl-phone" for l, _ in cs.scan_deterministic("contact +442079460958 anytime", []))
assert any(l == "intl-phone" for l, _ in cs.scan_deterministic("MATILDE_CONTACT=+4915123456789", []))
# Not a phone: a short +N token (version, diff count) must not trip it.
assert not any(l == "intl-phone" for l, _ in cs.scan_deterministic("rebased +1234 ahead", []))


def test_generic_methodology_is_clean():
assert cs.scan_deterministic("Structure a handoff: decisions, threads, next step.", []) == []

Expand Down
Loading