Suppress own-domain tracker attribution

Patterns like "ethereum-https://letaido.com" embed the scanned site's
own origin, appended either by a browser extension (e.g. a wallet
injecting window.ethereum) or by an owner-set tracker. The mapping
agent read that embedded domain and attributed the site to itself as a
third party ("Letaido"), but a site owner is never a third party of its
own site.

Pass the scanned site's eTLD+1 to the agent as a <scanned_site> signal
and instruct it to ignore a domain embedded in the pattern that matches
the site, returning low confidence when that is the only cue. Add a
deterministic backstop that discards any agent attribution whose vendor
name resolves to the site's own domain, so the pattern falls through to
the unmatched fallback regardless of whether the model honored the
prompt. Deterministic catalog, naming, and sibling signals run before
the agent and are untouched, so an independent strong vendor signal
still attributes normally.

Signed-off-by: Émile Ré <emile@probo.com>
This commit is contained in:
Émile Ré
2026-06-09 19:40:55 +02:00
parent faca86a022
commit e88ad152df
4 changed files with 232 additions and 6 deletions

View File

@@ -18,6 +18,7 @@ import (
"context"
"errors"
"fmt"
"strings"
"time"
"go.gearno.de/kit/log"
@@ -637,7 +638,9 @@ func (h *trackerMappingHandler) identifyWithAgent(
domains = uri.FilterFirstPartyDomains(domains, siteOrigin)
prompt := buildAgentPrompt(tp, domains)
siteDomain := uri.ExtractDomain(siteOrigin)
prompt := buildAgentPrompt(tp, domains, siteDomain)
agentCtx, cancel := context.WithTimeout(ctx, h.agentTimeout)
defer cancel()
@@ -680,11 +683,67 @@ func (h *trackerMappingHandler) identifyWithAgent(
return nil, nil
}
// Backstop for the prompt rule that the scanned site is never a third
// party of itself: a pattern that embeds the site's own domain (e.g.
// an "ethereum-https://example.com" wallet-extension key, or an
// owner-set tracker) can lead the agent to attribute the site's own
// brand. Discard such attributions outright so the pattern falls
// through to the unmatched fallback instead of being mapped to the
// site owner.
if nameMatchesSiteDomain(identification.ThirdPartyName, siteOrigin) {
h.logger.InfoCtx(
ctx,
"agent attributed scanned site as third party, discarding",
log.String("pattern", tp.Pattern),
)
return nil, nil
}
return &agentIdentification{
result: identification,
}, nil
}
// nameMatchesSiteDomain reports whether a candidate vendor name refers to
// the scanned site itself. The site owner is never a third party of its
// own site, so an attribution whose name resolves to the site's own
// domain must be rejected. The comparison is alphanumeric-normalised and
// conservative (equality against the eTLD+1 and its primary label) to
// avoid suppressing unrelated vendors whose name merely overlaps.
func nameMatchesSiteDomain(name, siteOrigin string) bool {
domain := uri.ExtractDomain(siteOrigin)
if domain == "" {
return false
}
normalizedName := normalizeAlnum(name)
if normalizedName == "" {
return false
}
label, _, _ := strings.Cut(domain, ".")
return normalizedName == normalizeAlnum(domain) ||
normalizedName == normalizeAlnum(label)
}
// normalizeAlnum lowercases s and keeps only ASCII letters and digits,
// so vendor names and domains can be compared free of spacing,
// punctuation, and casing differences (e.g. "Letaido" and "letaido.com"
// both reduce to a comparable form).
func normalizeAlnum(s string) string {
var b strings.Builder
for _, r := range strings.ToLower(s) {
if (r >= 'a' && r <= 'z') || (r >= '0' && r <= '9') {
b.WriteRune(r)
}
}
return b.String()
}
// persistAgentIdentification writes a confident agent identification:
// it resolves or creates the catalog third party and upserts the
// catalog pattern row that links to it. It runs inside the caller's