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.
38 lines
2.3 KiB
JavaScript
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).`)
|