Merge pull request 'feat(reports): add no-email identity summary and disclosure validator (closes #305)' (#329) from feat/issue-305-report-email-redaction into master

This commit was merged in pull request #329.
This commit is contained in:
2026-07-07 03:03:12 -05:00
2 changed files with 183 additions and 0 deletions
+81
View File
@@ -2610,3 +2610,84 @@ def build_review_mutation_proof(run_log: list[dict]) -> dict:
"missing_fields": [],
"reasons": [],
}
# ---------------------------------------------------------------------------
# Identity disclosure (#305)
# ---------------------------------------------------------------------------
EMAIL_ADDRESS_RE = re.compile(
r"[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}")
EMAIL_JUSTIFICATION_MARKERS = (
"email required",
"email is required",
"email necessary",
"necessary to disambiguate",
"disambiguate identity",
"tool requires the email",
"user explicitly asked",
)
def format_identity_summary(username, profile, role=None, remote=None):
"""Return the no-email identity line for workflow reports (#305).
Standard reports identify actors as ``<username> / <profile>`` (plus
optional role/remote). Personal email is never part of the summary; if
an email lands in the username slot, only its local part is kept.
"""
name = (username or "").strip()
if "@" in name:
name = name.split("@", 1)[0]
summary = f"{name} / {(profile or '').strip()}"
extras = [str(part).strip() for part in (role, remote)
if part and str(part).strip()]
if extras:
summary += f" ({', '.join(extras)})"
return summary
def assess_email_disclosure(
report_text,
*,
justification_markers=EMAIL_JUSTIFICATION_MARKERS,
):
"""Flag unnecessary personal-email disclosure in a report (#305).
Username/profile identity is sufficient for normal workflow reports.
An email address is tolerated only when the report itself explains why
it is necessary (tool proof, explicit request, or disambiguation).
"""
text = report_text or ""
emails = sorted(set(EMAIL_ADDRESS_RE.findall(text)))
if not emails:
return {
"proven": True,
"flagged": False,
"justified": False,
"emails": [],
"reasons": [],
}
lower = text.lower()
justified = any(marker in lower for marker in justification_markers)
if justified:
return {
"proven": True,
"flagged": False,
"justified": True,
"emails": emails,
"reasons": [
"email disclosure present but justified in the report",
],
}
return {
"proven": False,
"flagged": True,
"justified": False,
"emails": emails,
"reasons": [
f"unnecessary personal email disclosure: {email}"
for email in emails
],
}
+102
View File
@@ -0,0 +1,102 @@
"""Identity-disclosure checks for workflow reports (#305).
Workflow reports sometimes included the authenticated user's personal
email even though username/profile identity is sufficient (observed:
``Identity: jcwalker3 / [email protected]`` in a reconciliation
handoff). These tests pin the no-email identity summary helper and the
final-report validator that flags unnecessary email disclosure.
"""
import sys
import unittest
sys.path.insert(0, str(__import__("pathlib").Path(__file__).resolve().parent.parent))
import review_proofs
class TestIdentitySummary(unittest.TestCase):
def test_author_summary_uses_username_and_profile_only(self):
summary = review_proofs.format_identity_summary(
"jcwalker3", "prgs-author")
self.assertEqual(summary, "jcwalker3 / prgs-author")
self.assertNotIn("@", summary)
def test_reviewer_summary_uses_username_and_profile_only(self):
summary = review_proofs.format_identity_summary(
"sysadmin", "prgs-reviewer")
self.assertEqual(summary, "sysadmin / prgs-reviewer")
def test_summary_appends_role_and_remote_without_email(self):
summary = review_proofs.format_identity_summary(
"jcwalker3", "prgs-author", role="author", remote="prgs")
self.assertIn("jcwalker3 / prgs-author", summary)
self.assertIn("author", summary)
self.assertIn("prgs", summary)
self.assertNotIn("@", summary)
def test_summary_never_leaks_email_passed_as_username(self):
"""Defense in depth: an email in the username slot is reduced."""
summary = review_proofs.format_identity_summary(
"[email protected]", "prgs-author")
self.assertNotIn("@", summary)
self.assertIn("jcwalker3", summary)
class TestEmailDisclosureAssessment(unittest.TestCase):
def test_report_without_email_passes(self):
report = "\n".join([
"Controller Handoff",
"Identity: jcwalker3 / prgs-author",
"Role: author",
])
res = review_proofs.assess_email_disclosure(report)
self.assertTrue(res["proven"])
self.assertFalse(res["flagged"])
self.assertEqual(res["emails"], [])
self.assertEqual(res["reasons"], [])
def test_unnecessary_email_is_flagged(self):
report = "\n".join([
"Controller Handoff",
"Identity: jcwalker3 / [email protected]",
])
res = review_proofs.assess_email_disclosure(report)
self.assertFalse(res["proven"])
self.assertTrue(res["flagged"])
self.assertIn("[email protected]", res["emails"])
self.assertTrue(res["reasons"])
self.assertFalse(res["justified"])
def test_multiple_emails_all_reported(self):
report = (
"Identity: [email protected] author\n"
"Reviewer: [email protected]\n"
)
res = review_proofs.assess_email_disclosure(report)
self.assertTrue(res["flagged"])
self.assertEqual(
sorted(res["emails"]),
["[email protected]", "[email protected]"],
)
def test_justified_email_with_explanation_is_not_flagged(self):
report = "\n".join([
"Identity: jcwalker3 / prgs-author",
"Contact email: [email protected]",
"Email required because two accounts share the username and the",
"address is necessary to disambiguate identity.",
])
res = review_proofs.assess_email_disclosure(report)
self.assertFalse(res["flagged"])
self.assertTrue(res["justified"])
self.assertIn("[email protected]", res["emails"])
def test_email_without_justification_language_is_flagged(self):
report = "Contact email: [email protected] just in case.\n"
res = review_proofs.assess_email_disclosure(report)
self.assertTrue(res["flagged"])
self.assertFalse(res["justified"])
if __name__ == "__main__":
unittest.main()