doxa-maps/internal/check-browser-gates-guarded.mjs
Doxa Maps fbc517c94a A browser gate refuses to launch outside the browser slot; check:browser-gates-guarded keeps it that way
Only the browser seat runs browsers, and only inside internal/browser-slot.sh.
That rule was remembered, not enforced: an agent ran a gate it took for a
headless source check and it launched Chromium outside the slot.
internal/lib/playwright-guarded.mjs is now the one way a repo file reaches
playwright: its launch(), launchPersistentContext(), launchServer(), connect()
and connectOverCDP() exit 75 (NOT MEASURED) with a message naming
browser-slot.sh unless DOXA_BROWSER_SLOT is set, and browser-slot.sh exports it
for the command it runs, so a bash -c grandchild inherits it. All 72 files that
imported playwright switch to it (internal/lib/browser.mjs and 64 internal gates;
plugin-designs scripts: doxa-research-map 3, my-upg-100-list 2,
research-map-plus-grid 2, research-dashboard 1): a mechanical import swap.
check:browser-gates-guarded (gates:pre) fails a direct import;
bun run browser-gates lists the 97 scripts that launch a browser. isBrowserGate
moves to internal/lib/browser-gates.mjs so the runner and the check share it.
2026-09-28 08:20:21 +00:00

38 lines
2.3 KiB
JavaScript

#!/usr/bin/env node
/**
* check-browser-gates-guarded — no repo gate reaches Playwright except through the slot guard. Headless.
*
* Browsers run only on the browser seat and only inside internal/browser-slot.sh. The rule is enforced
* by internal/lib/playwright-guarded.mjs (a launch refuses, exit 75, without DOXA_BROWSER_SLOT), so a
* file that imports 'playwright' DIRECTLY would bypass it. This fails any such file under internal/ or
* plugin-designs/. `--list` prints every gate script that launches a browser instead.
* EXIT 0 pass · 1 fail
*/
import { readFileSync } from 'node:fs'
import { execFileSync } from 'node:child_process'
import { resolve, dirname } from 'node:path'
import { fileURLToPath } from 'node:url'
import { isBrowserGate } from './lib/browser-gates.mjs'
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
if (process.argv.includes('--list')) {
const scripts = JSON.parse(readFileSync(resolve(ROOT, 'package.json'), 'utf8')).scripts || {}
const names = Object.keys(scripts).filter((n) => !n.startsWith('gates:') && isBrowserGate(ROOT, n)).sort()
console.log(names.join('\n'))
console.error(`${names.length} script(s) launch a browser: run each as internal/browser-slot.sh bun run <name>`)
process.exit(0)
}
const DIRECT = /from\s+['"]playwright['"]|import\(\s*['"]playwright['"]\s*\)|require\(\s*['"]playwright['"]\s*\)/
const files = execFileSync('git', ['ls-files', '--', 'internal', 'plugin-designs'], { cwd: ROOT, encoding: 'utf8' })
.split('\n').filter((f) => /\.(mjs|js|cjs)$/.test(f) && f !== 'internal/lib/playwright-guarded.mjs' && f !== 'internal/check-browser-gates-guarded.mjs' && f !== 'internal/run-parallel.mjs')
const bad = []
for (const f of files) {
const code = readFileSync(resolve(ROOT, f), 'utf8').replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '')
if (DIRECT.test(code)) bad.push(f)
}
if (bad.length) {
for (const f of bad) console.log(` FAIL ${f} imports playwright directly; import it from internal/lib/playwright-guarded.mjs`)
console.log(`BROWSER-GATES-GUARDED: FAIL — ${bad.length} file(s) can launch a browser outside the slot.`)
process.exit(1)
}
console.log(`BROWSER-GATES-GUARDED: PASS — no repo file imports playwright except through the slot guard (${files.length} files read).`)