mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-17 14:42:06 +00:00
unbroker finds where a consenting person's info is exposed across data brokers and people-search sites and files the removals, running as far as each site allows and handing only genuinely human-only steps (hard CAPTCHA, gov-ID, phone, fax) back as an end-of-run digest. - Deterministic stdlib CLI (scripts/pdd.py) owns config, dossiers+consent, the broker DB, tier planning, the ledger, email, and the autonomous action queue; the agent scans/submits with native tools (web_extract, browser_*, delegate_task, cronjob, terminal). - Verify-before-disclose, least-disclosure (never volunteers SSN), consent gate, opaque ids, optional age-at-rest encryption, file-locked ledger. - Jurisdiction-aware (CCPA/CPRA, GDPR, generic); CA DROP one-shot covers the state registry (~545) in a single request; BADBOOL + curated people-search coverage; scheduled re-scan for re-listing. - No CAPTCHA-solving services or anti-bot bypass; browser email mode needs no stored password. - 85 hermetic tests (tests/skills/test_unbroker_skill.py; SMTP/IMAP via injected fakes, registry via CSV fixtures). Ships placeholder data only. Broker dataset adapted from BADBOOL (Yael Grauer, CC BY-NC-SA 4.0).
63 lines
2.3 KiB
Python
63 lines
2.3 KiB
Python
"""Render opt-out / legal request text from templates/ with safe substitution.
|
|
|
|
Templates use {field} placeholders. Missing fields are left literal (never crash,
|
|
never inject blanks that look like real data). Field values come from the
|
|
least-disclosure selection in dossier.select_disclosure.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import paths
|
|
|
|
|
|
class _SafeDict(dict):
|
|
def __missing__(self, key): # leave unknown placeholders untouched
|
|
return "{" + key + "}"
|
|
|
|
|
|
def template_path(name: str) -> Path:
|
|
return paths.templates_dir() / name
|
|
|
|
|
|
def render(template_name: str, fields: dict) -> str:
|
|
text = template_path(template_name).read_text(encoding="utf-8")
|
|
return text.format_map(_SafeDict(fields))
|
|
|
|
|
|
def _join_listings(value) -> str:
|
|
if isinstance(value, (list, tuple)):
|
|
return "\n".join(str(v) for v in value)
|
|
return str(value or "")
|
|
|
|
|
|
def _join_identifiers(value) -> str:
|
|
"""Render the subject's OWN identifiers as a bullet list for an indirect-exposure request."""
|
|
if isinstance(value, (list, tuple)):
|
|
return "\n".join(f" - {v}" for v in value if v)
|
|
return f" - {value}" if value else ""
|
|
|
|
|
|
def render_optout_email(broker: dict, fields: dict) -> str:
|
|
ctx = dict(fields)
|
|
ctx.setdefault("broker_name", broker.get("name", "the data broker"))
|
|
ctx["listing_urls"] = _join_listings(fields.get("listing_urls"))
|
|
ctx.setdefault("full_name", fields.get("full_name", "[your name]"))
|
|
ctx.setdefault("contact_email", fields.get("contact_email", "[your email]"))
|
|
return render("emails/generic-optout.txt", ctx)
|
|
|
|
|
|
def render_request(kind: str, broker: dict, fields: dict) -> str:
|
|
"""kind: generic | ccpa | ccpa_agent | ccpa_indirect | gdpr"""
|
|
template = {
|
|
"generic": "emails/generic-optout.txt",
|
|
"ccpa": "emails/ccpa-deletion.txt",
|
|
"ccpa_agent": "emails/ccpa-authorized-agent.txt",
|
|
"ccpa_indirect": "emails/ccpa-indirect-deletion.txt",
|
|
"gdpr": "emails/gdpr-erasure.txt",
|
|
}.get(kind, "emails/generic-optout.txt")
|
|
ctx = dict(fields)
|
|
ctx.setdefault("broker_name", broker.get("name", "the data broker"))
|
|
ctx["listing_urls"] = _join_listings(fields.get("listing_urls"))
|
|
ctx["my_identifiers"] = _join_identifiers(fields.get("my_identifiers"))
|
|
return render(template, ctx)
|