From 31efea28d69dc74b2722d2d91a31430d55c8d7ce Mon Sep 17 00:00:00 2001 From: Fernando Frizzatti Date: Wed, 22 Jul 2026 23:01:44 -0300 Subject: [PATCH] feat(harness): WAF bypass header for browser tools on deco-owned hosts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit take_screenshot / scrape_url / inspect_page run on Browserless — headless Chrome from datacenter egress — which Cloudflare bot management on deco's own zones (decocdn.com / deco.site) scores as a bot and blocks. Agent QA of deco previews (envs-*.decocdn.com) and storefronts consistently 403'd. Both zones already carry a shared-secret skip rule matching the x-deco-probe-bypass header. When DECO_WAF_BYPASS_TOKEN is set, the three tools now send that header — ONLY when the target host is a deco zone, so browser tools pointed at third-party URLs never leak the secret. /screenshot and /content get setExtraHTTPHeaders in the payload; the /function template calls page.setExtraHTTPHeaders before goto. Requires DECO_WAF_BYPASS_TOKEN in the cluster env (value = the existing x-deco-probe-bypass rule secret). Co-Authored-By: Claude Opus 4.8 (1M context) --- .../decopilot/built-in-tools/inspect-page.ts | 7 ++++ .../built-in-tools/portable-media-tools.ts | 6 ++++ .../decopilot/built-in-tools/scrape-url.ts | 5 +++ .../decopilot/built-in-tools/waf-bypass.ts | 34 +++++++++++++++++++ 4 files changed, 52 insertions(+) create mode 100644 packages/harness/src/decopilot/built-in-tools/waf-bypass.ts diff --git a/packages/harness/src/decopilot/built-in-tools/inspect-page.ts b/packages/harness/src/decopilot/built-in-tools/inspect-page.ts index 9b2b7f32e0..1d40251553 100644 --- a/packages/harness/src/decopilot/built-in-tools/inspect-page.ts +++ b/packages/harness/src/decopilot/built-in-tools/inspect-page.ts @@ -19,6 +19,7 @@ import type { ObjectStorageHooks } from "../../harness-deps"; import { createOutputPreview, estimateJsonTokens } from "./read-tool-output"; import { toStudioStorageUri } from "../studio-storage-uri"; import { LARGE_RESULT_TOKEN_THRESHOLD } from "./constants"; +import { wafBypassHeaders } from "./waf-bypass"; const InspectPageInputSchema = z.object({ url: z.string().url().describe("The URL of the web page to inspect."), @@ -67,6 +68,12 @@ function buildFunctionCode( errors.push(err.message || String(err)); }); + // Deco-zone targets get the WAF bypass header (see waf-bypass.ts). + const bypassHeaders = ${JSON.stringify(wafBypassHeaders(url))}; + if (Object.keys(bypassHeaders).length) { + await page.setExtraHTTPHeaders(bypassHeaders); + } + await page.goto(${JSON.stringify(url)}, { waitUntil: ${JSON.stringify(waitUntil)}, timeout: 30000, diff --git a/packages/harness/src/decopilot/built-in-tools/portable-media-tools.ts b/packages/harness/src/decopilot/built-in-tools/portable-media-tools.ts index 176ef79342..8908c020e6 100644 --- a/packages/harness/src/decopilot/built-in-tools/portable-media-tools.ts +++ b/packages/harness/src/decopilot/built-in-tools/portable-media-tools.ts @@ -7,6 +7,7 @@ import { } from "../studio-storage-uri"; import type { PendingImage } from "./vm-tools/types"; import { BROWSERLESS_BASE_URL } from "./constants"; +import { wafBypassHeaders } from "./waf-bypass"; const FILES_URL_PATTERN = /\/api\/[^/]+\/files\/([^?#]+)/; const MAX_REFERENCE_IMAGE_BYTES = 20 * 1024 * 1024; @@ -345,6 +346,11 @@ export function createPortableTakeScreenshotTool( quality: JPEG_QUALITY, }, viewport: DEFAULT_VIEWPORT, + // Deco-zone targets get the WAF bypass header (see waf-bypass.ts) + // so bot management doesn't block the render. + ...(Object.keys(wafBypassHeaders(input.url)).length + ? { setExtraHTTPHeaders: wafBypassHeaders(input.url) } + : {}), }), }, ); diff --git a/packages/harness/src/decopilot/built-in-tools/scrape-url.ts b/packages/harness/src/decopilot/built-in-tools/scrape-url.ts index 2474d34a55..dd384c32b5 100644 --- a/packages/harness/src/decopilot/built-in-tools/scrape-url.ts +++ b/packages/harness/src/decopilot/built-in-tools/scrape-url.ts @@ -16,6 +16,7 @@ import type { ObjectStorageHooks } from "../../harness-deps"; import { createOutputPreview, estimateJsonTokens } from "./read-tool-output"; import { toStudioStorageUri } from "../studio-storage-uri"; import { LARGE_RESULT_TOKEN_THRESHOLD } from "./constants"; +import { wafBypassHeaders } from "./waf-bypass"; const ScrapeUrlInputSchema = z.object({ url: z.string().url().describe("The URL of the web page to scrape."), @@ -55,6 +56,10 @@ export function createScrapeUrlTool( headers: { "Content-Type": "application/json" }, body: JSON.stringify({ url: input.url, + // Deco-zone targets get the WAF bypass header (see waf-bypass.ts). + ...(Object.keys(wafBypassHeaders(input.url)).length + ? { setExtraHTTPHeaders: wafBypassHeaders(input.url) } + : {}), }), }, ); diff --git a/packages/harness/src/decopilot/built-in-tools/waf-bypass.ts b/packages/harness/src/decopilot/built-in-tools/waf-bypass.ts new file mode 100644 index 0000000000..a1d4623a01 --- /dev/null +++ b/packages/harness/src/decopilot/built-in-tools/waf-bypass.ts @@ -0,0 +1,34 @@ +/** + * WAF bypass header for deco-owned hosts. + * + * The browser tools (take_screenshot / scrape_url / inspect_page) run on + * Browserless — headless Chrome from datacenter egress — which Cloudflare + * bot management on deco's own zones scores as a bot and blocks. That breaks + * agent QA/verification of deco previews (envs-*.decocdn.com) and + * storefronts. Both zones carry a shared-secret skip rule matching the + * `x-deco-probe-bypass` header; sending it gives Studio's tooling trusted + * passage without loosening bot protection for anyone else. + * + * SECURITY: the header is attached ONLY when the target host is a deco zone + * (*.decocdn.com / *.deco.site). Browser tools can target arbitrary URLs — + * sending the secret to third-party origins would leak it. + */ +export function wafBypassHeaders(targetUrl: string): Record { + const token = + typeof process !== "undefined" + ? process.env?.DECO_WAF_BYPASS_TOKEN + : undefined; + if (!token) return {}; + try { + const host = new URL(targetUrl).hostname; + const decoHost = + host === "decocdn.com" || + host === "deco.site" || + host.endsWith(".decocdn.com") || + host.endsWith(".deco.site"); + if (decoHost) return { "x-deco-probe-bypass": token }; + } catch { + // Malformed URL — the Browserless call will fail on its own terms. + } + return {}; +}