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:
@@ -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
|
||||
],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user