feat(reports): add no-email identity summary and disclosure validator (closes #305)

Workflow reports included the authenticated user's personal email even
when username/profile identity was sufficient (observed: 'Identity:
jcwalker3 / [email protected]' in a reconciliation handoff).

Add format_identity_summary() producing the canonical no-email identity
line ('<username> / <profile>' with optional role/remote, reducing an
email passed in the username slot to its local part), and
assess_email_disclosure() flagging personal email addresses in report
text unless the report itself justifies the disclosure (tool proof,
explicit request, or identity disambiguation).

Tests cover no-email summaries for author and reviewer identities,
role/remote suffixes, email-in-username reduction, clean reports,
single and multiple unjustified disclosures, and justified
disambiguation.

Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
This commit is contained in:
2026-07-07 02:57:45 -04:00
co-authored by Claude Opus 4.8
parent a5341684f6
commit a3b79736e0
2 changed files with 183 additions and 0 deletions
+81
View File
@@ -2471,3 +2471,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()