From b5bf63b436eb248ee431fe24c4789140cd7ed955 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Fri, 12 Jun 2026 16:11:17 +0200 Subject: [PATCH] Give tracker agents a browser to read setters MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tracker-mapping and common-pattern enrichment agents only had web search, which returns title/url/snippet, so they could never open a cookie-database or cookie-policy page to read which vendor actually sets a tracker. This mis-attributed setters whose snippet is misleading (e.g. _li_* read as LinkedIn rather than LiveIntent). Wire the read-only headless-browser toolset into both agents, gated on a configured Chrome endpoint, mirroring the common-third-party enrichment worker: agent construction moves into the run path so each run can carry a per-run browser that is closed when the run returns. The prompts now direct the agent to open a promising result and read the named setter from the full page text. Both agents stay unchanged when no Chrome endpoint is configured. Signed-off-by: Émile Ré --- pkg/cookiebanner/common_pattern_enricher.go | 50 +++++++++++++------ .../common_pattern_enrichment_agent.go | 9 ++++ .../prompts/tracker_enrichment.txt.tmpl | 8 +-- .../prompts/tracker_identification.txt.tmpl | 18 ++++--- pkg/cookiebanner/tracker_agents_config.go | 11 +++- pkg/cookiebanner/tracker_mapping_agent.go | 8 +++ pkg/cookiebanner/tracker_mapping_worker.go | 29 ++++++++--- pkg/probod/tracker_agents.go | 2 + 8 files changed, 99 insertions(+), 36 deletions(-) diff --git a/pkg/cookiebanner/common_pattern_enricher.go b/pkg/cookiebanner/common_pattern_enricher.go index 5b4bf34ce..9d1ba5d33 100644 --- a/pkg/cookiebanner/common_pattern_enricher.go +++ b/pkg/cookiebanner/common_pattern_enricher.go @@ -23,6 +23,7 @@ import ( "go.gearno.de/kit/log" "go.gearno.de/kit/pg" "go.probo.inc/probo/pkg/agent" + "go.probo.inc/probo/pkg/agent/tools/browser" "go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/llm" @@ -39,8 +40,10 @@ import ( type CommonPatternEnricher struct { pg *pg.Client logger *log.Logger - enrichmentAgent *agent.Agent - mappingAgent *agent.Agent + enrichmentCfg TrackerEnrichmentAgentConfig + mappingCfg TrackerMappingAgentConfig + enrichmentEnabled bool + mappingEnabled bool enrichmentTimeout time.Duration mappingTimeout time.Duration } @@ -70,24 +73,20 @@ func NewCommonPatternEnricher( e := &CommonPatternEnricher{ pg: pgClient, logger: logger, + enrichmentCfg: enrichmentCfg, + mappingCfg: mappingCfg, + enrichmentEnabled: enrichmentCfg.LLMClient != nil, + mappingEnabled: mappingCfg.LLMClient != nil, enrichmentTimeout: enrichmentTimeout, mappingTimeout: mappingTimeout, } - if enrichmentCfg.LLMClient != nil { - e.enrichmentAgent = buildCommonPatternEnrichmentAgent(enrichmentCfg, pgClient, logger) - } - - if mappingCfg.LLMClient != nil { - e.mappingAgent = buildTrackerMappingAgent(mappingCfg, pgClient, logger) - } - return e } // Enabled reports whether an LLM-backed enrichment agent is configured. func (e *CommonPatternEnricher) Enabled() bool { - return e.enrichmentAgent != nil + return e.enrichmentEnabled } // EnrichPattern researches a description for one common tracker pattern @@ -105,6 +104,19 @@ func (e *CommonPatternEnricher) EnrichPattern(ctx context.Context, cp coredata.C return err } + // Build one per-run browser shared by both agent sub-runs when a + // Chrome endpoint is configured. The browser lets the agents open + // cookie-database and cookie-policy pages to read the true setter and + // ground a description; it is closed when this run returns. + var browserTools []agent.Tool + + if e.enrichmentCfg.ChromeAddr != "" { + webBrowser := browser.NewBrowser(ctx, e.enrichmentCfg.ChromeAddr) + defer webBrowser.Close() + + browserTools = browser.NewReadOnlyToolset(webBrowser).Tools() + } + // Map before enriching: an unlinked pattern is run through the // mapping agent first so a confident vendor both seeds the enrichment // prompt and gets linked. Attribution stays the mapping pipeline's @@ -113,7 +125,7 @@ func (e *CommonPatternEnricher) EnrichPattern(ctx context.Context, cp coredata.C var attribution *TrackerMappingAgentResult if cp.CommonThirdPartyID == nil { - attribution, err = e.identifyThirdParty(ctx, cp) + attribution, err = e.identifyThirdParty(ctx, cp, browserTools) if err != nil { return err } @@ -123,7 +135,7 @@ func (e *CommonPatternEnricher) EnrichPattern(ctx context.Context, cp coredata.C } } - description, err := e.research(ctx, cp, thirdPartyName) + description, err := e.research(ctx, cp, thirdPartyName, browserTools) if err != nil { return fmt.Errorf("cannot research tracker description: %w", err) } @@ -207,15 +219,18 @@ func (e *CommonPatternEnricher) research( ctx context.Context, cp coredata.CommonTrackerPattern, thirdPartyName string, + browserTools []agent.Tool, ) (string, error) { prompt := buildEnrichmentPrompt(cp, thirdPartyName) + enrichmentAgent := buildCommonPatternEnrichmentAgent(e.enrichmentCfg, e.pg, e.logger, browserTools) + agentCtx, cancel := context.WithTimeout(ctx, e.enrichmentTimeout) defer cancel() result, err := agent.RunTyped[CommonPatternEnrichmentResult]( agentCtx, - e.enrichmentAgent, + enrichmentAgent, []llm.Message{ { Role: llm.RoleUser, @@ -239,19 +254,22 @@ func (e *CommonPatternEnricher) research( func (e *CommonPatternEnricher) identifyThirdParty( ctx context.Context, cp coredata.CommonTrackerPattern, + browserTools []agent.Tool, ) (*TrackerMappingAgentResult, error) { - if e.mappingAgent == nil { + if !e.mappingEnabled { return nil, nil } prompt := buildCommonPatternIdentificationPrompt(cp) + mappingAgent := buildTrackerMappingAgent(e.mappingCfg, e.pg, e.logger, browserTools) + agentCtx, cancel := context.WithTimeout(ctx, e.mappingTimeout) defer cancel() result, err := agent.RunTyped[TrackerMappingAgentResult]( agentCtx, - e.mappingAgent, + mappingAgent, []llm.Message{ { Role: llm.RoleUser, diff --git a/pkg/cookiebanner/common_pattern_enrichment_agent.go b/pkg/cookiebanner/common_pattern_enrichment_agent.go index 3c61c2c3b..09875017c 100644 --- a/pkg/cookiebanner/common_pattern_enrichment_agent.go +++ b/pkg/cookiebanner/common_pattern_enrichment_agent.go @@ -35,15 +35,24 @@ type CommonPatternEnrichmentResult struct { Description string `json:"description" jsonschema:"A concise, factual, compliance-grade description of what this tracker stores or does and its purpose. One or two sentences. Name the operating company when known. Empty when the purpose cannot be substantiated from evidence."` } +// buildCommonPatternEnrichmentAgent builds the common-pattern enrichment +// agent. extraTools carries the browser read-only toolset when a headless +// Chrome endpoint is configured; it is empty otherwise, in which case the +// agent relies on the DB search tool and web search alone. The browser +// lets it open authoritative vendor and cookie-database pages to ground a +// description. func buildCommonPatternEnrichmentAgent( cfg TrackerEnrichmentAgentConfig, pgClient *pg.Client, logger *log.Logger, + extraTools []agent.Tool, ) *agent.Agent { tools := []agent.Tool{ searchThirdPartiesTool(pgClient), } + tools = append(tools, extraTools...) + if cfg.FirecrawlAPIKey != "" { tools = append(tools, search.FirecrawlSearchTool(cfg.FirecrawlAPIKey)) } diff --git a/pkg/cookiebanner/prompts/tracker_enrichment.txt.tmpl b/pkg/cookiebanner/prompts/tracker_enrichment.txt.tmpl index d8eed15ae..51152cac1 100644 --- a/pkg/cookiebanner/prompts/tracker_enrichment.txt.tmpl +++ b/pkg/cookiebanner/prompts/tracker_enrichment.txt.tmpl @@ -21,9 +21,11 @@ Return a structured JSON response with: - Stop once you have a confident, well-sourced answer; do not exhaust all queries if the first succeeds. - Verify that any result discusses a tracker whose name shares a meaningful prefix with the pattern being described. Discard results about a differently-named tracker. A generic token shared with a vendor's terminology (e.g. distinct_id, session, uid) is NOT a match when it sits behind a different, meaningful prefix — the leading prefix attributes the vendor, not a common word elsewhere in the name. -4. Be factual and conservative. Describe only what the evidence supports. Do not speculate about data flows or purposes you cannot substantiate. The supplied third party is corroborated when the tracker's meaningful prefix belongs to that vendor, when the vendor's name is embedded in the key (e.g. "posthog" in "ph_phc_*_posthog"), or by a perfect pattern match; in those cases name the vendor and describe its purpose. Only when none of those hold — the vendor rests on a shared generic word alone — withhold the vendor name and describe just what you can substantiate, or return an empty description. +4. When browser tools are available (navigate_to_url, extract_page_text, extract_links), use them to open an authoritative result returned by web_search — the vendor's own cookie/privacy documentation or a cookie-database entry — and read its full content rather than relying on the title/snippet. The precise, source-grounded purpose statement usually lives in the page body, not the snippet. Apply the same prefix-matching rule to the page text, and never describe the cookie-database directory operator itself as the vendor. Open at most a few pages and stop once one substantiates the purpose. -5. Keep the description concise (one to two sentences) and free of marketing language. It should read as a neutral, compliance-grade statement of purpose. +5. Be factual and conservative. Describe only what the evidence supports. Do not speculate about data flows or purposes you cannot substantiate. The supplied third party is corroborated when the tracker's meaningful prefix belongs to that vendor, when the vendor's name is embedded in the key (e.g. "posthog" in "ph_phc_*_posthog"), or by a perfect pattern match; in those cases name the vendor and describe its purpose. Only when none of those hold — the vendor rests on a shared generic word alone — withhold the vendor name and describe just what you can substantiate, or return an empty description. -6. If you genuinely cannot substantiate the tracker's purpose from evidence, return an empty description. Do not write a fallback such as "purpose could not be determined", and do not guess a purpose from the name or max-age alone (e.g. do not claim a key is "used for session" just because it has no expiry). An empty description is preferable to an unverified one. +6. Keep the description concise (one to two sentences) and free of marketing language. It should read as a neutral, compliance-grade statement of purpose. + +7. If you genuinely cannot substantiate the tracker's purpose from evidence, return an empty description. Do not write a fallback such as "purpose could not be determined", and do not guess a purpose from the name or max-age alone (e.g. do not claim a key is "used for session" just because it has no expiry). An empty description is preferable to an unverified one. diff --git a/pkg/cookiebanner/prompts/tracker_identification.txt.tmpl b/pkg/cookiebanner/prompts/tracker_identification.txt.tmpl index 7f31b2e82..d07a74260 100644 --- a/pkg/cookiebanner/prompts/tracker_identification.txt.tmpl +++ b/pkg/cookiebanner/prompts/tracker_identification.txt.tmpl @@ -24,10 +24,12 @@ Return a structured JSON response with: - Stop searching once you get a confident match; do not exhaust all query slots if the first one succeeds. - When evaluating web search results, verify that the tracker name discussed in the result shares a meaningful prefix with the pattern you are identifying. Trackers with different prefixes are distinct — for example, _hjCookieTest (Hotjar's _hj prefix) must not be confused with a pattern named cookietest (no _hj prefix). If the search result discusses a tracker whose prefix does not match, discard it and continue searching or lower your confidence. - A generic token shared with a vendor's terminology is NOT a match when it appears as a suffix or substring behind a different, meaningful prefix. The leading prefix is what attributes a vendor, not a common word elsewhere in the name. For example, probo_distinct_id carries the custom prefix probo_, so it must NOT be attributed to Mixpanel merely because Mixpanel uses a distinct_id key — the prefix probo_ does not belong to Mixpanel. Likewise, a key ending in _session or _uid is not attributable to a vendor just because that vendor also uses such a word. - - Some web search results come from cookie-database or cookie-banner directory sites (e.g. cookifi.com, cookiepedia.co.uk, cookiedatabase.org, cookie-script.com, cookieserve.com, and similar "cookie database" / "cookie scanner" directories). These rank highly only because they catalog cookies, not because they set them. Treat such a result ONLY as a reference directory: read which actual vendor the page names as the setter of the tracker, and attribute to THAT vendor. NEVER set third_party_name to the directory operator itself (e.g. "Cookifi", "Cookiepedia", "Cookie-Script", "CookieDatabase", "CookieServe") — they are never the third party that set the tracker. If such a page names no concrete vendor for the tracker, ignore it and continue searching or return an empty third_party_name with third_party_confidence below 0.3. + - Some web search results come from cookie-database or cookie-banner directory sites (e.g. cookifi.com, cookiepedia.co.uk, cookiedatabase.org, cookie-script.com, cookieserve.com, and similar "cookie database" / "cookie scanner" directories). These rank highly only because they catalog cookies, not because they set them. Treat such a result ONLY as a reference directory: open the page with the browser tools (navigate_to_url, then extract_page_text) and read which actual vendor the page names as the setter of the tracker, and attribute to THAT vendor. The snippet alone is often insufficient — the database table that names the true setter usually only appears in the full page text. NEVER set third_party_name to the directory operator itself (e.g. "Cookifi", "Cookiepedia", "Cookie-Script", "CookieDatabase", "CookieServe") — they are never the third party that set the tracker. If such a page names no concrete vendor for the tracker, ignore it and continue searching or return an empty third_party_name with third_party_confidence below 0.3. - Exception: a consent-management vendor's OWN product cookie is still attributable to that vendor on the strength of its naming convention, independent of where the search result was hosted — e.g. OptanonConsent / OptanonAlertBoxClosed -> OneTrust, CookieConsent -> Cookiebot, cookieyes-consent -> CookieYes. Judge that on the naming convention alone, the same way you would any other vendor. -4. Common cookie naming conventions to recognize: +4. When browser tools are available (navigate_to_url, extract_page_text, extract_links, find_links_matching), use them to open a promising web_search result and read its full content rather than relying on the title/snippet. This is decisive for two cases: a cookie-database entry whose table names the true setter (e.g. a "_li_*" entry that names LiveIntent, not LinkedIn), and a site's cookie-policy page that lists the vendor behind a cookie. Read the page, identify the concrete vendor the page attributes the tracker to, and apply the same prefix-matching and directory-operator rules above to that text. Do not navigate to more than a few pages; stop once a page gives a confident attribution. + +5. Common cookie naming conventions to recognize: - _ga*, _gid, _gat*: Google Analytics - _fbp, _fbc, fr: Meta / Facebook - _pk_*: Matomo (formerly Piwik) @@ -38,11 +40,11 @@ Return a structured JSON response with: - hubspot*: HubSpot - _cls_*: Clarity (Microsoft) -5. The observed domains are useful signals but not conclusive on their own. Many sites load third-party tracker scripts through a first-party reverse proxy (e.g. t.example.com proxying PostHog). When a domain matches the scanned site, it reveals nothing about which third party set the tracker — rely on the naming convention or a database/web search instead. First-party proxy domains are filtered before they reach you, but if you still see the scanned site's own domain, ignore it as evidence. Well-known third-party tracking domains (e.g. doubleclick.net, facebook.com, analytics.google.com) remain strong evidence. +6. The observed domains are useful signals but not conclusive on their own. Many sites load third-party tracker scripts through a first-party reverse proxy (e.g. t.example.com proxying PostHog). When a domain matches the scanned site, it reveals nothing about which third party set the tracker — rely on the naming convention or a database/web search instead. First-party proxy domains are filtered before they reach you, but if you still see the scanned site's own domain, ignore it as evidence. Well-known third-party tracking domains (e.g. doubleclick.net, facebook.com, analytics.google.com) remain strong evidence. -6. A domain or full URL appearing INSIDE the pattern name is NOT a third-party signal when it matches . It is the site's own first-party origin — commonly a browser extension that appended the page URL to its key (e.g. "ethereum-https://example.com", where the wallet extension suffixes the site origin), or a tracker the site owner set on their own site. The site owner is never a third party of its own site, so you must NOT attribute the scanned site's own brand or domain as a third party. If the embedded own-domain is the ONLY vendor cue, return an empty third_party_name with third_party_confidence below 0.3. A genuine naming convention elsewhere in the key (e.g. _ga, _fbp, _hj) still attributes normally — judge that on its own merits, independent of the embedded site domain. +7. A domain or full URL appearing INSIDE the pattern name is NOT a third-party signal when it matches . It is the site's own first-party origin — commonly a browser extension that appended the page URL to its key (e.g. "ethereum-https://example.com", where the wallet extension suffixes the site origin), or a tracker the site owner set on their own site. The site owner is never a third party of its own site, so you must NOT attribute the scanned site's own brand or domain as a third party. If the embedded own-domain is the ONLY vendor cue, return an empty third_party_name with third_party_confidence below 0.3. A genuine naming convention elsewhere in the key (e.g. _ga, _fbp, _hj) still attributes normally — judge that on its own merits, independent of the embedded site domain. -7. Be conservative with third_party_confidence. It measures certainty about the attribution (who set the tracker), nothing else: +8. Be conservative with third_party_confidence. It measures certainty about the attribution (who set the tracker), nothing else: - 0.9-1.0: exact pattern match found in database, or unmistakable naming convention + matching domain, or a name that embeds the vendor (e.g. "__darkreader__*" -> Dark Reader) - 0.7-0.8: strong signal from naming convention or domain, but not a database match - 0.5-0.6: reasonable guess based on partial naming patterns @@ -50,10 +52,10 @@ Return a structured JSON response with: Do not lower third_party_confidence just because the artifact is a browser-extension key, localStorage entry, or otherwise not a classic web tracker. The goal is to attribute the vendor, not to judge how "tracker-worthy" the artifact is — if the name unambiguously names its source, attribute it with high confidence. -8. If you truly cannot identify who set the tracker, set third_party_name to an empty string and third_party_confidence below 0.3. +9. If you truly cannot identify who set the tracker, set third_party_name to an empty string and third_party_confidence below 0.3. -9. Only attribute a tracker to a company or service when you have concrete evidence: an exact (perfect) match on the pattern in the database, an unmistakable naming convention where the tracker's meaningful prefix belongs to that vendor (including a vendor name embedded in the key), or a clear web search result whose tracker name shares that meaningful prefix. Absent a shared meaningful prefix or a perfect pattern match, do NOT imagine a vendor — never guess or invent attributions based on vague similarity, a shared generic word, or general knowledge. If no evidence supports a match, return an empty third_party_name with third_party_confidence below 0.3. +10. Only attribute a tracker to a company or service when you have concrete evidence: an exact (perfect) match on the pattern in the database, an unmistakable naming convention where the tracker's meaningful prefix belongs to that vendor (including a vendor name embedded in the key), or a clear web search result whose tracker name shares that meaningful prefix. Absent a shared meaningful prefix or a perfect pattern match, do NOT imagine a vendor — never guess or invent attributions based on vague similarity, a shared generic word, or general knowledge. If no evidence supports a match, return an empty third_party_name with third_party_confidence below 0.3. -10. For the category field, use one of: {{.Categories}}. +11. For the category field, use one of: {{.Categories}}. Most cookies fall under ANALYTICS or MARKETING. diff --git a/pkg/cookiebanner/tracker_agents_config.go b/pkg/cookiebanner/tracker_agents_config.go index 22e00f582..9cf874d5f 100644 --- a/pkg/cookiebanner/tracker_agents_config.go +++ b/pkg/cookiebanner/tracker_agents_config.go @@ -27,11 +27,14 @@ import ( // MaxTokens and Temperature bound and steer the LLM call (the output is // tiny structured JSON). Timeout caps a single agent run and MaxTurns // bounds the agent reasoning loop. Zero-valued tuning fields fall back -// to package defaults. +// to package defaults. ChromeAddr enables the read-only browser toolset +// (so the agent can open cookie-database and cookie-policy pages to read +// the true setter); when empty the agent relies on web search alone. type TrackerMappingAgentConfig struct { LLMClient *llm.Client Model string FirecrawlAPIKey string + ChromeAddr string MaxTokens *int Temperature *float64 Timeout time.Duration @@ -45,11 +48,15 @@ type TrackerMappingAgentConfig struct { // MaxTokens and Temperature bound and steer the LLM call (the output is // tiny structured JSON). Timeout caps a single agent run and MaxTurns // bounds the agent reasoning loop. Zero-valued tuning fields fall back -// to package defaults. +// to package defaults. ChromeAddr enables the read-only browser toolset +// (so the agent can open authoritative vendor and cookie-database pages +// to ground a description); when empty the agent relies on web search +// alone. type TrackerEnrichmentAgentConfig struct { LLMClient *llm.Client Model string FirecrawlAPIKey string + ChromeAddr string MaxTokens *int Temperature *float64 Timeout time.Duration diff --git a/pkg/cookiebanner/tracker_mapping_agent.go b/pkg/cookiebanner/tracker_mapping_agent.go index fa616c201..4028dc30b 100644 --- a/pkg/cookiebanner/tracker_mapping_agent.go +++ b/pkg/cookiebanner/tracker_mapping_agent.go @@ -75,16 +75,24 @@ type TrackerMappingAgentResult struct { ThirdPartyConfidence float64 `json:"third_party_confidence" jsonschema:"Confidence (0.0 to 1.0) in which company or service set this tracker, independent of whether the artifact is a classic web tracker. Set below 0.5 if unsure who set it."` } +// buildTrackerMappingAgent builds the tracker-mapping agent. extraTools +// carries the browser read-only toolset when a headless Chrome endpoint +// is configured; it is empty otherwise, in which case the agent relies on +// the DB search tools and web search alone. The browser lets it open +// cookie-database and cookie-policy pages to read the true setter. func buildTrackerMappingAgent( cfg TrackerMappingAgentConfig, pgClient *pg.Client, logger *log.Logger, + extraTools []agent.Tool, ) *agent.Agent { tools := []agent.Tool{ searchTrackerPatternsTool(pgClient), searchThirdPartiesTool(pgClient), } + tools = append(tools, extraTools...) + if cfg.FirecrawlAPIKey != "" { tools = append(tools, search.FirecrawlSearchTool(cfg.FirecrawlAPIKey)) } diff --git a/pkg/cookiebanner/tracker_mapping_worker.go b/pkg/cookiebanner/tracker_mapping_worker.go index 1052335d3..83f5eaed3 100644 --- a/pkg/cookiebanner/tracker_mapping_worker.go +++ b/pkg/cookiebanner/tracker_mapping_worker.go @@ -25,6 +25,7 @@ import ( "go.gearno.de/kit/pg" "go.gearno.de/kit/worker" "go.probo.inc/probo/pkg/agent" + "go.probo.inc/probo/pkg/agent/tools/browser" "go.probo.inc/probo/pkg/coredata" "go.probo.inc/probo/pkg/gid" "go.probo.inc/probo/pkg/llm" @@ -42,7 +43,8 @@ const defaultMappingStaleAfter = 10 * time.Minute type trackerMappingHandler struct { pg *pg.Client logger *log.Logger - mappingAgent *agent.Agent + mappingCfg TrackerMappingAgentConfig + mappingEnabled bool disambiguationAgent *agent.Agent agentTimeout time.Duration disambiguationTimeout time.Duration @@ -69,15 +71,13 @@ func NewTrackerMappingWorker( h := &trackerMappingHandler{ pg: pgClient, logger: logger, + mappingCfg: mappingCfg, + mappingEnabled: mappingCfg.LLMClient != nil, agentTimeout: agentTimeout, disambiguationTimeout: disambiguationCfg.Timeout, staleAfter: staleAfter, } - if mappingCfg.LLMClient != nil { - h.mappingAgent = buildTrackerMappingAgent(mappingCfg, pgClient, logger) - } - if disambiguationCfg.LLMClient != nil { h.disambiguationAgent = thirdparty.BuildDisambiguationAgent(disambiguationCfg, logger) } @@ -191,7 +191,7 @@ func (h *trackerMappingHandler) Process(ctx context.Context, tp coredata.Tracker // the deterministic signals could not resolve a catalog third party. // The LLM and web-search calls happen outside any transaction; the // result is persisted in its own short transaction. - if commonThirdPartyID == nil && h.mappingAgent != nil { + if commonThirdPartyID == nil && h.mappingEnabled { ident, err := h.identifyWithAgent(ctx, tp, det.origin) if err != nil { return fmt.Errorf("cannot identify with agent: %w", err) @@ -622,12 +622,27 @@ func (h *trackerMappingHandler) identifyWithAgent( prompt := buildAgentPrompt(tp, domains, siteDomain) + // Build the mapping agent per run so it can carry a per-run browser + // when a Chrome endpoint is configured. The browser lets the agent + // open cookie-database and cookie-policy pages to read the true + // setter; it is closed when this run returns. + var browserTools []agent.Tool + + if h.mappingCfg.ChromeAddr != "" { + webBrowser := browser.NewBrowser(ctx, h.mappingCfg.ChromeAddr) + defer webBrowser.Close() + + browserTools = browser.NewReadOnlyToolset(webBrowser).Tools() + } + + mappingAgent := buildTrackerMappingAgent(h.mappingCfg, h.pg, h.logger, browserTools) + agentCtx, cancel := context.WithTimeout(ctx, h.agentTimeout) defer cancel() result, err := agent.RunTyped[TrackerMappingAgentResult]( agentCtx, - h.mappingAgent, + mappingAgent, []llm.Message{ { Role: llm.RoleUser, diff --git a/pkg/probod/tracker_agents.go b/pkg/probod/tracker_agents.go index ce8b1ab63..e400f221d 100644 --- a/pkg/probod/tracker_agents.go +++ b/pkg/probod/tracker_agents.go @@ -62,6 +62,7 @@ func (impl *Implm) buildTrackerAgents( LLMClient: mappingClient, Model: mappingAgentCfg.ModelName, FirecrawlAPIKey: firecrawlAPIKey, + ChromeAddr: impl.cfg.ChromeDPAddr, MaxTokens: mappingAgentCfg.MaxTokens, Temperature: mappingAgentCfg.Temperature, Timeout: time.Duration(impl.cfg.TrackerMappingWorker.AgentTimeout) * time.Second, @@ -88,6 +89,7 @@ func (impl *Implm) buildTrackerAgents( LLMClient: enrichmentClient, Model: enrichmentAgentCfg.ModelName, FirecrawlAPIKey: firecrawlAPIKey, + ChromeAddr: impl.cfg.ChromeDPAddr, MaxTokens: enrichmentAgentCfg.MaxTokens, Temperature: enrichmentAgentCfg.Temperature, Timeout: time.Duration(impl.cfg.CommonPatternEnrichmentWorker.AgentTimeout) * time.Second,