需求:在 AI 编程会话中让用户与 Agent 共享同一浏览器页面,并查看控制台与网络信息。 实现:新增沙箱浏览器内核、Host API/渲染器面板、OpenCode 工具接入及安全边界测试。
109 lines
3.1 KiB
TypeScript
109 lines
3.1 KiB
TypeScript
import { AgentBrowserFault } from './fault';
|
|
|
|
const BLOCKED_METHODS = new Set([
|
|
'DOM.getFileInfo',
|
|
'DOM.setFileInputFiles',
|
|
'Page.crash',
|
|
'Page.setDownloadBehavior',
|
|
'Security.handleCertificateError',
|
|
'Security.setDisableNetworkAccessForOrigins',
|
|
'Security.setIgnoreCertificateErrors',
|
|
'Security.setOverrideCertificateErrors',
|
|
]);
|
|
|
|
const BLOCKED_DOMAINS = [
|
|
'Cast.',
|
|
'DeviceAccess.',
|
|
'Extensions.',
|
|
'Tethering.',
|
|
] as const;
|
|
|
|
const URL_FIELDS_BY_METHOD = new Map<string, string[]>([
|
|
['Fetch.continueRequest', ['url']],
|
|
['Network.continueInterceptedRequest', ['url']],
|
|
['Network.loadNetworkResource', ['url']],
|
|
['Page.navigate', ['url']],
|
|
]);
|
|
|
|
function hasExternalTargetReference(params: Record<string, unknown> | undefined): boolean {
|
|
return Boolean(params && (
|
|
typeof params.targetId === 'string'
|
|
|| typeof params.browserContextId === 'string'
|
|
));
|
|
}
|
|
|
|
function isAllowedPageUrl(value: string): boolean {
|
|
try {
|
|
const url = new URL(value);
|
|
return url.protocol === 'http:' || url.protocol === 'https:';
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
export class AgentBrowserCdpGuard {
|
|
assertAllowed(
|
|
method: string,
|
|
params: Record<string, unknown> | undefined,
|
|
sessionRef: string | undefined,
|
|
childSessions: ReadonlySet<string>,
|
|
ioHandles: ReadonlySet<string>,
|
|
): void {
|
|
const blockedHostDomain = (
|
|
(method.startsWith('Browser.') && method !== 'Browser.getVersion')
|
|
|| method.startsWith('SystemInfo.')
|
|
|| method.startsWith('Memory.')
|
|
|| method.startsWith('Tracing.')
|
|
|| BLOCKED_DOMAINS.some((domain) => method.startsWith(domain))
|
|
);
|
|
if (BLOCKED_METHODS.has(method) || blockedHostDomain) {
|
|
throw new AgentBrowserFault(
|
|
'CDP_METHOD_BLOCKED',
|
|
`CDP method ${method} 可能影响 Makelore 宿主,已阻止。`,
|
|
false,
|
|
);
|
|
}
|
|
if (method.startsWith('Target.')) {
|
|
throw new AgentBrowserFault(
|
|
'TARGET_DENIED',
|
|
'Target 域由开发浏览器托管,不能直接操作其他 Electron 页面。',
|
|
false,
|
|
);
|
|
}
|
|
if (hasExternalTargetReference(params)) {
|
|
throw new AgentBrowserFault(
|
|
'TARGET_DENIED',
|
|
'不能指定开发浏览器之外的 CDP target。',
|
|
false,
|
|
);
|
|
}
|
|
for (const field of URL_FIELDS_BY_METHOD.get(method) ?? []) {
|
|
const value = params?.[field];
|
|
if (typeof value === 'string' && !isAllowedPageUrl(value)) {
|
|
throw new AgentBrowserFault(
|
|
'CDP_METHOD_BLOCKED',
|
|
`${method} 只允许访问 HTTP 或 HTTPS 地址。`,
|
|
false,
|
|
);
|
|
}
|
|
}
|
|
if (sessionRef && !childSessions.has(sessionRef)) {
|
|
throw new AgentBrowserFault(
|
|
'TARGET_DENIED',
|
|
'CDP session 不属于当前开发浏览器。',
|
|
false,
|
|
);
|
|
}
|
|
if (method.startsWith('IO.')) {
|
|
const handle = params?.handle;
|
|
if (typeof handle !== 'string' || !ioHandles.has(handle)) {
|
|
throw new AgentBrowserFault(
|
|
'TARGET_DENIED',
|
|
'IO handle 不属于当前开发浏览器会话。',
|
|
false,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|