hermes-agent/optional-skills/security/unbroker/scripts/vectors.py
SHL0MS c2828f2b9b feat(skills): add security/unbroker (autonomous data-broker removal)
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).
2026-07-02 21:16:10 -04:00

53 lines
2.1 KiB
Python

"""Enumerate the search queries to run per broker, across ALL of a subject's identifiers.
People-search sites index a person under every name, phone, email, and address they
have. A subject with two names (maiden/married) and three past cities can have many
distinct listings on one broker, each found via a different search. `search_vectors`
expands the dossier into the concrete searches to run, filtered by what each broker
supports (`broker.search.by`, default ["name"]).
"""
from __future__ import annotations
import dossier as dossier_mod
# What a broker can be searched by; default if a record doesn't declare it.
DEFAULT_BY = ["name"]
def supported_by(broker: dict) -> list[str]:
return list((broker.get("search") or {}).get("by") or DEFAULT_BY)
def search_vectors(subject_dossier: dict, broker: dict) -> list[dict]:
"""List of {by, query} searches to run for this subject on this broker."""
by = set(supported_by(broker))
ident = subject_dossier.get("identity", {})
vectors: list[dict] = []
if "name" in by:
names = dossier_mod.all_names(subject_dossier)
locations = dossier_mod.all_locations(subject_dossier)
if locations:
for name in names:
for loc in locations:
vectors.append({"by": "name",
"query": {"full_name": name, "city": loc.get("city"), "state": loc.get("state")}})
else:
for name in names:
vectors.append({"by": "name", "query": {"full_name": name}})
if "phone" in by:
for phone in ident.get("phones") or []:
vectors.append({"by": "phone", "query": {"phone": phone}})
if "email" in by:
for email in ident.get("emails") or []:
vectors.append({"by": "email", "query": {"email": email}})
if "address" in by:
for a in dossier_mod.all_addresses(subject_dossier):
if a.get("line1"):
vectors.append({"by": "address",
"query": {k: a.get(k) for k in ("line1", "city", "state", "postal")}})
return vectors