From a84a6e233d6b47f1a99e16eeea901461919d8155 Mon Sep 17 00:00:00 2001 From: Sinabina Date: Tue, 21 Jul 2026 12:13:33 -0700 Subject: [PATCH] fix: harden browser provider activation --- .github/docker/Dockerfile.ci | 8 +- .github/workflows/make-pdf-gate.yml | 2 +- browse/src/browser-manager.ts | 2 + browse/src/cli.ts | 15 +- browse/src/proxy-config.ts | 13 +- browse/src/write-commands.ts | 17 +- browse/test/commands.test.ts | 14 ++ browse/test/daemon-mismatch-refuse.test.ts | 41 ++++ browse/test/proxy-config.test.ts | 16 ++ docs/gstack-2/JUDGMENT-PROVENANCE.json | 30 +-- docs/gstack-2/RELEASE-INTEGRITY.md | 2 +- docs/gstack-2/STATUS.md | 20 +- docs/gstack-2/TEST-EVIDENCE.md | 5 +- evals/parity/contracts/benchmark.json | 2 +- evals/parity/contracts/browse.json | 2 +- evals/parity/contracts/canary.json | 2 +- .../parity/contracts/design-consultation.json | 2 +- evals/parity/contracts/design-html.json | 2 +- evals/parity/contracts/design-review.json | 2 +- evals/parity/contracts/devex-review.json | 2 +- evals/parity/contracts/land-and-deploy.json | 2 +- evals/parity/contracts/office-hours.json | 2 +- .../parity/contracts/open-gstack-browser.json | 2 +- evals/parity/contracts/pair-agent.json | 2 +- evals/parity/contracts/qa-only.json | 2 +- evals/parity/contracts/qa.json | 2 +- .../contracts/setup-browser-cookies.json | 2 +- evals/parity/manifest.json | 30 +-- .../deterministic/design-consultation.json | 8 +- .../deterministic/design-review.json | 8 +- .../deterministic/office-hours.json | 8 +- .../deterministic/qa-fix-verify.json | 8 +- .../deterministic/qa-report-only.json | 8 +- runtime/cli.js | 8 +- runtime/doctor.js | 31 ++- runtime/install.js | 21 +- runtime/managed-home.js | 2 +- runtime/runtime-bootstrap.mjs | 87 +++++++- runtime/upgrade.js | 70 +++++- scripts/build-app.sh | 2 +- scripts/gstack2/render-legacy.ts | 3 +- .../references/support/runtime-bootstrap.mjs | 87 +++++++- .../references/legacy/design-consultation.md | 4 +- .../design/references/legacy/design-html.md | 4 +- .../design/references/legacy/design-review.md | 4 +- .../references/support/runtime-bootstrap.mjs | 87 +++++++- skills/plan/references/legacy/office-hours.md | 4 +- .../references/support/runtime-bootstrap.mjs | 87 +++++++- skills/qa/references/legacy/benchmark.md | 4 +- skills/qa/references/legacy/browse.md | 4 +- skills/qa/references/legacy/canary.md | 4 +- skills/qa/references/legacy/devex-review.md | 4 +- .../references/legacy/open-gstack-browser.md | 4 +- skills/qa/references/legacy/pair-agent.md | 4 +- skills/qa/references/legacy/qa-only.md | 4 +- skills/qa/references/legacy/qa.md | 4 +- .../legacy/setup-browser-cookies.md | 4 +- .../references/support/runtime-bootstrap.mjs | 87 +++++++- .../references/support/runtime-bootstrap.mjs | 87 +++++++- skills/ship/references/legacy/canary.md | 4 +- .../ship/references/legacy/land-and-deploy.md | 4 +- .../references/support/runtime-bootstrap.mjs | 87 +++++++- test/gstack2-runtime-install.test.ts | 61 ++++++ test/gstack2-runtime-safety-config.test.ts | 19 ++ test/gstack2-runtime-setup-ux.test.ts | 205 ++++++++++++++++++ 65 files changed, 1203 insertions(+), 171 deletions(-) diff --git a/.github/docker/Dockerfile.ci b/.github/docker/Dockerfile.ci index ebf4a4d13..2be739d4d 100644 --- a/.github/docker/Dockerfile.ci +++ b/.github/docker/Dockerfile.ci @@ -70,11 +70,11 @@ RUN curl --retry 5 --retry-delay 5 --retry-connrefused -fsSL https://bun.sh/inst RUN npm i -g @anthropic-ai/claude-code # Playwright system deps (Chromium) — needed for browse E2E tests -RUN npx playwright install-deps chromium +RUN npx playwright-core install-deps chromium # Linux has neither Helvetica nor Arial. make-pdf's print CSS stacks fall back # to Liberation Sans (metric-compatible Arial clone, SIL OFL 1.1) so PDFs don't -# render in DejaVu Sans. playwright install-deps happens to pull this in today, +# render in DejaVu Sans. playwright-core install-deps happens to pull this in today, # but the dep is implicit and could change — install explicitly so upgrades # can't silently regress rendering. # @@ -100,12 +100,12 @@ RUN bun install --frozen-lockfile && rm -rf /tmp/* # Install Playwright Chromium to a shared location accessible by all users ENV PLAYWRIGHT_BROWSERS_PATH=/opt/playwright-browsers -RUN npx playwright install chromium \ +RUN npx playwright-core install chromium \ && chmod -R a+rX /opt/playwright-browsers # Verify everything works RUN bun --version && node --version && claude --version && jq --version && gh --version \ - && npx playwright --version \ + && npx playwright-core --version \ && fc-match "Liberation Sans" | grep -qi "Liberation" \ || (echo "ERROR: fonts-liberation not installed — make-pdf PDFs will render in DejaVu Sans" && exit 1) diff --git a/.github/workflows/make-pdf-gate.yml b/.github/workflows/make-pdf-gate.yml index 9a3d13538..69c4d7eb1 100644 --- a/.github/workflows/make-pdf-gate.yml +++ b/.github/workflows/make-pdf-gate.yml @@ -66,7 +66,7 @@ jobs: fc-match -f '%{family[0]}\t%{color}\n' ':lang=und-zsye:charset=1F600' || true - name: Install Playwright Chromium - run: bunx playwright install chromium + run: bunx playwright-core install chromium - name: Build binaries run: bun run build diff --git a/browse/src/browser-manager.ts b/browse/src/browser-manager.ts index b8c56f7ca..5cdfbb8ec 100644 --- a/browse/src/browser-manager.ts +++ b/browse/src/browser-manager.ts @@ -382,6 +382,7 @@ export class BrowserManager { // BROWSE_EXTENSIONS_DIR points to an unpacked Chrome extension directory. // Extensions only work in headed mode, so we use an off-screen window. const extensionsDir = process.env.BROWSE_EXTENSIONS_DIR; + if (extensionsDir) assertHeadedBrowserProvider(); const { STEALTH_LAUNCH_ARGS, buildGStackLaunchArgs } = await import('./stealth'); const launchArgs: string[] = [...STEALTH_LAUNCH_ARGS, ...buildGStackLaunchArgs()]; let useHeadless = true; @@ -1587,6 +1588,7 @@ export class BrowserManager { * If step 2 fails → return error, headless browser untouched */ async handoff(message: string): Promise { + assertHeadedBrowserProvider(); if (this.connectionMode === 'headed' || this.isHeaded) { return `HANDOFF: Already in headed mode at ${this.getCurrentUrl()}`; } diff --git a/browse/src/cli.ts b/browse/src/cli.ts index dee7b642f..93e4f17da 100644 --- a/browse/src/cli.ts +++ b/browse/src/cli.ts @@ -118,7 +118,7 @@ interface ServerState { serverPath: string; binaryVersion?: string; mode?: 'launched' | 'headed'; - /** Hash of (proxyUrl + headed flag), used by D2 daemon-mismatch check. */ + /** Hash of proxy, headed mode, and browser-provider intent, used by daemon-mismatch checks. */ configHash?: string; /** Xvfb child PID for cleanup on disconnect. */ xvfbPid?: number; @@ -431,8 +431,8 @@ async function ensureServer(flags?: GlobalFlags): Promise { // hint. No silent restart — that would drop tab state, cookies, and // logged-in sessions without warning. if (desiredHash && state.configHash && state.configHash !== desiredHash) { - console.error(`[browse] existing daemon has different config (proxy/headed mismatch).`); - console.error(`[browse] run 'browse disconnect' first to apply --proxy/--headed.`); + console.error(`[browse] existing daemon has different config (browser provider, proxy, or headed mode).`); + console.error(`[browse] run 'browse disconnect' first to apply the selected browser configuration.`); process.exit(1); } // Same path: existing daemon is plain (no flags) but caller passes @@ -782,7 +782,7 @@ export interface GlobalFlags { proxyUrl: string | null; /** Whether --headed was passed. */ headed: boolean; - /** Hash of (proxy + headed) for daemon-mismatch check. */ + /** Hash of proxy, headed mode, and browser-provider intent for daemon-mismatch checks. */ configHash: string; /** Redacted form of proxyUrl, safe for logs. */ redactedProxyUrl: string; @@ -842,7 +842,12 @@ export function extractGlobalFlags(rawArgs: string[], env: NodeJS.ProcessEnv): G args: out, proxyUrl: canonicalProxyUrl, headed, - configHash: computeConfigHash({ proxyUrl: canonicalProxyUrl, headed }), + configHash: computeConfigHash({ + proxyUrl: canonicalProxyUrl, + headed, + browserProvider: env.GSTACK_BROWSER_PROVIDER, + browserExecutable: env.GSTACK_CHROMIUM_PATH, + }), redactedProxyUrl: redactProxyUrl(canonicalProxyUrl), }; } diff --git a/browse/src/proxy-config.ts b/browse/src/proxy-config.ts index 161475825..e03f0f318 100644 --- a/browse/src/proxy-config.ts +++ b/browse/src/proxy-config.ts @@ -125,7 +125,7 @@ export function toUpstreamConfig(cfg: ParsedProxyConfig): UpstreamConfig { } /** - * Compute a stable hash of (proxyUrl + headed flag) for daemon-mismatch + * Compute a stable hash of proxy, headed mode, and browser-provider intent for daemon-mismatch * detection (D2). The hash is deterministic across CLI invocations on the * same machine and survives daemon restarts via the state file. * @@ -135,9 +135,18 @@ export function toUpstreamConfig(cfg: ParsedProxyConfig): UpstreamConfig { export function computeConfigHash(opts: { proxyUrl: string | null | undefined; headed: boolean; + browserProvider?: string | null; + browserExecutable?: string | null; }): string { const proxyKey = canonicalizeProxyUrl(opts.proxyUrl); - const input = JSON.stringify({ proxy: proxyKey, headed: opts.headed }); + const browserProvider = opts.browserProvider || null; + const browserExecutable = browserProvider === "installed" ? opts.browserExecutable || null : null; + const input = JSON.stringify({ + proxy: proxyKey, + headed: opts.headed, + browserProvider, + browserExecutable, + }); return createHash('sha256').update(input).digest('hex').slice(0, 16); } diff --git a/browse/src/write-commands.ts b/browse/src/write-commands.ts index 4a847141d..626ba8794 100644 --- a/browse/src/write-commands.ts +++ b/browse/src/write-commands.ts @@ -355,11 +355,18 @@ export async function handleWriteCommand( } } catch (err: any) { // Enhanced error guidance: clicking