diff --git a/contrib/helm/charts/probo/templates/deployment.yaml b/contrib/helm/charts/probo/templates/deployment.yaml index c57ae873c..134ef68ff 100644 --- a/contrib/helm/charts/probo/templates/deployment.yaml +++ b/contrib/helm/charts/probo/templates/deployment.yaml @@ -416,7 +416,7 @@ spec: - name: COMMON_THIRD_PARTY_ENRICHMENT_AGENT_MAX_TURNS value: {{ .Values.probo.commonThirdPartyEnrichmentWorker.agentMaxTurns | quote }} {{- end }} - {{- if .Values.probo.commonThirdPartyEnrichmentWorker.confidenceThreshold }} + {{- if ne .Values.probo.commonThirdPartyEnrichmentWorker.confidenceThreshold nil }} - name: COMMON_THIRD_PARTY_ENRICHMENT_CONFIDENCE_THRESHOLD value: {{ .Values.probo.commonThirdPartyEnrichmentWorker.confidenceThreshold | quote }} {{- end }} diff --git a/pkg/thirdparty/common_third_party_enrichment_worker.go b/pkg/thirdparty/common_third_party_enrichment_worker.go index 7600391f3..a2cd7d86b 100644 --- a/pkg/thirdparty/common_third_party_enrichment_worker.go +++ b/pkg/thirdparty/common_third_party_enrichment_worker.go @@ -73,8 +73,27 @@ const ( defaultEnrichmentMaxAttempts = 3 enrichmentLogoUserAgent = "Probo-Enricher/1.0" + + // maxEnrichmentErrorLen bounds the per-agent error text persisted in + // enrichment metadata so a verbose or hostile agent/tool error cannot + // leak unbounded internal detail into the column. + maxEnrichmentErrorLen = 500 ) +// sanitizeAgentError reduces a raw agent or tool error to a single bounded +// line safe to persist in enrichment metadata: whitespace runs (including +// embedded newlines) collapse to single spaces and the result is truncated +// to maxEnrichmentErrorLen runes. +func sanitizeAgentError(err error) string { + msg := strings.Join(strings.Fields(err.Error()), " ") + + if runes := []rune(msg); len(runes) > maxEnrichmentErrorLen { + msg = string(runes[:maxEnrichmentErrorLen]) + "…" + } + + return msg +} + // EnrichmentConfig configures the common-third-party enrichment worker // and the two agents it runs. The worker no-ops when LLMClient is nil; // callers gate registration on config presence. Browser tools for Agent @@ -219,7 +238,7 @@ func (h *enrichmentHandler) Process(ctx context.Context, party coredata.CommonTh company, err := h.runCompanyProfile(ctx, party) if err != nil { h.logger.WarnCtx(ctx, "company profile agent failed", log.Error(err), log.String("common_third_party_id", party.ID.String())) - runErrors = append(runErrors, "company_profile: "+err.Error()) + runErrors = append(runErrors, "company_profile: "+sanitizeAgentError(err)) } else { anySuccess = true } @@ -260,7 +279,7 @@ func (h *enrichmentHandler) Process(ctx context.Context, party coredata.CommonTh compliance, err := h.runComplianceDocs(ctx, party.Name, website, legalName) if err != nil { h.logger.WarnCtx(ctx, "compliance docs agent failed", log.Error(err), log.String("common_third_party_id", party.ID.String())) - runErrors = append(runErrors, "compliance_docs: "+err.Error()) + runErrors = append(runErrors, "compliance_docs: "+sanitizeAgentError(err)) } else { anySuccess = true } @@ -272,7 +291,7 @@ func (h *enrichmentHandler) Process(ctx context.Context, party coredata.CommonTh domainsResult, err := h.runDomains(ctx, party.Name, website) if err != nil { h.logger.WarnCtx(ctx, "domains agent failed", log.Error(err), log.String("common_third_party_id", party.ID.String())) - runErrors = append(runErrors, "domains: "+err.Error()) + runErrors = append(runErrors, "domains: "+sanitizeAgentError(err)) } else { anySuccess = true owned = resolveOwnedDomains(party.Name, website, domainsResult, defaultEnrichmentDomainConfidenceThreshold) diff --git a/pkg/thirdparty/common_third_party_logo.go b/pkg/thirdparty/common_third_party_logo.go index c11eb68b5..8cd4d88f2 100644 --- a/pkg/thirdparty/common_third_party_logo.go +++ b/pkg/thirdparty/common_third_party_logo.go @@ -196,11 +196,15 @@ func downloadImage( return nil, "", fmt.Errorf("logo response is not an image: %q", contentType) } - body, err := io.ReadAll(io.LimitReader(resp.Body, maxLogoSize)) + body, err := io.ReadAll(io.LimitReader(resp.Body, maxLogoSize+1)) if err != nil { return nil, "", fmt.Errorf("cannot read logo body: %w", err) } + if len(body) > maxLogoSize { + return nil, "", fmt.Errorf("logo response exceeds max size %d bytes", maxLogoSize) + } + if len(body) == 0 { return nil, "", fmt.Errorf("logo response is empty") } diff --git a/pkg/thirdparty/common_third_party_owned_domains.go b/pkg/thirdparty/common_third_party_owned_domains.go index 570eef335..a984ad1af 100644 --- a/pkg/thirdparty/common_third_party_owned_domains.go +++ b/pkg/thirdparty/common_third_party_owned_domains.go @@ -167,10 +167,11 @@ func exactLabelMatch(label string, vendorLabels []string) bool { return slices.Contains(vendorLabels, label) } -// relatedLabelMatch reports whether label is the same as, contains, or is -// contained by any vendor label. Substring matches require the shorter -// string to be at least minLabelOverlap characters to avoid spurious hits -// on very short labels. +// relatedLabelMatch reports whether label is the same as, or shares a +// dominant root with, any vendor label. A substring match requires the +// shorter label to be at least minLabelOverlap characters AND to cover at +// least half of the longer label, so a short root (e.g. "meta") no longer +// attributes an unrelated domain (e.g. "metallica") to the vendor. func relatedLabelMatch(label string, vendorLabels []string) bool { for _, vl := range vendorLabels { if label == vl { @@ -182,7 +183,15 @@ func relatedLabelMatch(label string, vendorLabels []string) bool { shorter, longer = longer, shorter } - if len(shorter) >= minLabelOverlap && strings.Contains(longer, shorter) { + if len(shorter) < minLabelOverlap { + continue + } + + if len(shorter)*2 < len(longer) { + continue + } + + if strings.Contains(longer, shorter) { return true } }