mirror of
https://github.com/garrytan/gstack.git
synced 2026-05-17 17:51:27 +08:00
Codex T2: flip CDP posture to deny-default. Allowed methods enumerated in cdp-allowlist.ts with (scope: tab|browser, output: trusted|untrusted, justification) per entry. Initial allowlist (~25 methods) covers: - Accessibility tree extraction (read-only) - DOM/CSS inspection (read-only) - Performance metrics - Tracing - Emulation viewport/UA override - Page screenshot/PDF capture (output is binary, no marker injection vector) - Network.enable/disable (no bodies/cookies — those are exfil surfaces) - Runtime.getProperties (NO evaluate/callFunctionOn — those would be RCE) Page.navigate is INTENTIONALLY NOT allowed; agents use $B goto which goes through the URL blocklist. Codex T7: two-tier mutex. tab-scoped methods take per-tab lock; browser- scoped take global lock that blocks all tab locks. 5s acquire timeout yields CDPMutexAcquireTimeout (no silent hangs). All lock acquires use try/finally so errors don't leak the lock. Path A from spike: uses Playwright's newCDPSession() per page. No second WebSocket, no need for --remote-debugging-port. CDPSession is cached per page in a WeakMap and cleared on page close. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
65 lines
2.5 KiB
TypeScript
65 lines
2.5 KiB
TypeScript
/**
|
|
* $B cdp <Domain.method> [json-params] — CLI surface for the CDP escape hatch.
|
|
*
|
|
* Output for trusted methods is a plain JSON pretty-print.
|
|
* Output for untrusted methods is wrapped with the centralized UNTRUSTED EXTERNAL
|
|
* CONTENT envelope so the sidebar-agent classifier sees it (matches the pattern
|
|
* used by other untrusted-content commands in commands.ts).
|
|
*/
|
|
|
|
import type { BrowserManager } from './browser-manager';
|
|
import { dispatchCdpCall } from './cdp-bridge';
|
|
import { wrapUntrustedContent } from './commands';
|
|
|
|
function parseQualified(name: string): { domain: string; method: string } {
|
|
const idx = name.indexOf('.');
|
|
if (idx <= 0 || idx === name.length - 1) {
|
|
throw new Error(
|
|
`Usage: $B cdp <Domain.method> [json-params]\n` +
|
|
`Cause: '${name}' is not in Domain.method format.\n` +
|
|
'Action: e.g. $B cdp Accessibility.getFullAXTree {}'
|
|
);
|
|
}
|
|
return { domain: name.slice(0, idx), method: name.slice(idx + 1) };
|
|
}
|
|
|
|
export async function handleCdpCommand(args: string[], bm: BrowserManager): Promise<string> {
|
|
if (args.length === 0 || args[0] === 'help' || args[0] === '--help') {
|
|
return [
|
|
'$B cdp — raw CDP method dispatch (deny-default escape hatch)',
|
|
'',
|
|
'Usage: $B cdp <Domain.method> [json-params]',
|
|
'',
|
|
'Allowed methods are listed in browse/src/cdp-allowlist.ts. To add one,',
|
|
'open a PR with a one-line justification and the (scope, output) tags.',
|
|
'Examples:',
|
|
' $B cdp Accessibility.getFullAXTree {}',
|
|
' $B cdp Performance.getMetrics {}',
|
|
' $B cdp DOM.describeNode \'{"backendNodeId":42,"depth":3}\'',
|
|
].join('\n');
|
|
}
|
|
const qualified = args[0]!;
|
|
const { domain, method } = parseQualified(qualified);
|
|
// Optional second arg is JSON params; default to {}.
|
|
let params: Record<string, unknown> = {};
|
|
if (args[1]) {
|
|
try {
|
|
params = JSON.parse(args[1]) ?? {};
|
|
} catch (e: any) {
|
|
throw new Error(
|
|
`Cannot parse params as JSON: ${e.message}\n` +
|
|
`Cause: argument '${args[1]}' is not valid JSON.\n` +
|
|
'Action: pass a JSON object literal, e.g. \'{"backendNodeId":42}\'.'
|
|
);
|
|
}
|
|
}
|
|
// Dispatch via the bridge (allowlist + mutex + timeout + finally-release).
|
|
const tabId = bm.getActiveTabId();
|
|
const { raw, entry } = await dispatchCdpCall({ domain, method, params, tabId, bm });
|
|
const json = JSON.stringify(raw, null, 2);
|
|
if (entry.output === 'untrusted') {
|
|
return wrapUntrustedContent(json, `cdp:${qualified}`);
|
|
}
|
|
return json;
|
|
}
|