feat(browser): add managed instance agent capabilities

This commit is contained in:
go0p
2026-09-03 13:31:59 +08:00
parent 8af9bb777e
commit e00746d834
37 changed files with 608 additions and 281 deletions
@@ -3,6 +3,7 @@ import type { HandoffReason } from '@/types/models';
import type { CapabilityDomainHandler } from '../capability-context';
import { allowedTarget } from '../capability-context';
import { activateTab } from '@/platform/browser/targets';
import { getTab } from '@/platform/browser/targets';
import { getState, updateState } from '@/platform/storage/state';
import { setAgentRuntimeState } from '@/features/agent-runtime/service';
import { ExtensionError } from '@/shared/errors';
@@ -16,15 +17,23 @@ export const handoffCapabilityHandler: CapabilityDomainHandler = {
return handoff?.taskId === grant.taskId ? handoff : { state: 'idle' };
}
const resolvedTarget = await allowedTarget(grant, input);
const grantTarget = grant.targets.find((target) => (
target.tabId === resolvedTarget.tabId && target.frameId === resolvedTarget.frameId
));
if (!grantTarget) throw new Error('目标标签页不在本次共享会话中');
const [tab, frame] = await Promise.all([
getTab(resolvedTarget.tabId),
browser.webNavigation.getFrame(resolvedTarget),
]);
if (!frame?.url || !/^https?:/i.test(frame.url)) {
throw new ExtensionError('target_unavailable', '目标 frame 不是可接管的 HTTP(S) 页面');
}
const grantTarget = {
...resolvedTarget,
isolationContextId: tab.isolationContextId || `browser-profile:tab-${tab.id}`,
cookieStoreId: tab.cookieStoreId,
origin: new URL(frame.url).origin,
grantedUrl: frame.url,
title: tab.title,
};
const now = Date.now();
const state = await updateState((current) => {
if (current.activeGrant?.id !== grant.id || current.activeGrant.expiresAt <= Date.now()) {
throw new ExtensionError('grant_expired', '浏览器共享会话已经变化,请重新发起请求');
}
if (current.handoff?.state === 'waiting_for_user') {
throw new ExtensionError('handoff_in_progress', '已有人工接管请求正在等待处理');
}
@@ -1,7 +1,8 @@
import { browser } from 'wxt/browser';
import type { CapabilityDomainHandler } from '../capability-context';
import { allowedTarget, requireScope } from '../capability-context';
import { getFrameInventory } from '@/features/page-context/frames';
import { getTab } from '@/platform/browser/targets';
import { activateTab, getTab, scheduleBrowserInstanceClose } from '@/platform/browser/targets';
import {
createBrowserIsolationProof,
deleteFirefoxContainerIdentity,
@@ -11,70 +12,88 @@ import {
openIncognitoIdentity,
} from '@/features/authorization-testing/isolation';
import { ExtensionError } from '@/shared/errors';
import { assertBrowserAccessPolicy, getEnterprisePolicy } from '@/platform/policy/managed';
import { NAVIGATION_CAPABILITY_DOMAIN } from '../capability-domains';
export const navigationCapabilityHandler: CapabilityDomainHandler = {
...NAVIGATION_CAPABILITY_DOMAIN,
async handle({ method, input, grant }) {
if (method === 'browser.tabs') {
const tabIds = [...new Set(grant.targets.map((target) => target.tabId))];
const tabs = await Promise.all(tabIds.map(async (tabId) => {
const targets = grant.targets.filter((target) => target.tabId === tabId);
for (const target of targets) {
try {
await allowedTarget(grant, {
tabId,
frameId: target.frameId,
documentId: target.documentId,
});
return getTab(tabId);
} catch {
// A tab remains visible while at least one explicitly granted frame is current.
}
}
return undefined;
}));
return tabs.filter(Boolean);
const { tabs } = await inspectBrowserIsolation();
const allowedOrigins = (await getEnterprisePolicy()).policy.grantAllowedOrigins;
return tabs.filter((tab) => !allowedOrigins?.length || allowedOrigins.includes(new URL(tab.url).origin))
.sort((left, right) => Number(Boolean(right.active)) - Number(Boolean(left.active))
|| (right.lastAccessed || 0) - (left.lastAccessed || 0));
}
if (method === 'browser.tab.open') {
const url = String(input.url || '');
assertBrowserAccessPolicy((await getEnterprisePolicy()).policy, { origin: new URL(url).origin });
const tab = await browser.tabs.create({ url, active: true });
if (!tab.id) throw new ExtensionError('target_unavailable', '浏览器没有返回新标签页 ID');
await activateTab(tab.id);
return { opened: true, id: tab.id, windowId: tab.windowId, active: true, url };
}
if (method === 'browser.thumbnail') {
const tab = await getTab(typeof input.tabId === 'number' ? input.tabId : undefined);
await allowedTarget(grant, { tabId: tab.id }, false);
if (!tab.active) {
throw new ExtensionError('target_not_active', '只能预览浏览器窗口当前可见的标签页');
}
return {
tabId: tab.id,
title: tab.title,
url: tab.url,
capturedAt: Date.now(),
dataUrl: await browser.tabs.captureVisibleTab(tab.windowId, { format: 'jpeg', quality: 55 }),
};
}
if (method === 'browser.frames') {
const tabId = typeof input.tabId === 'number' ? input.tabId : grant.targets[0]?.tabId;
if (!tabId || !grant.targets.some((target) => target.tabId === tabId)) {
throw new ExtensionError('target_denied', '目标标签页不在本次共享会话中');
}
return getFrameInventory(tabId);
const tabId = (await getTab(typeof input.tabId === 'number' ? input.tabId : undefined)).id;
await allowedTarget(grant, { tabId }, false);
const frames = await getFrameInventory(tabId);
const allowedOrigins = (await getEnterprisePolicy()).policy.grantAllowedOrigins;
return frames.filter((frame) => !allowedOrigins?.length
|| Boolean(frame.origin && allowedOrigins.includes(frame.origin)));
}
if (method === 'browser.instance.close') return scheduleBrowserInstanceClose();
if (method === 'browser.isolation.inspect') {
const grantedTabIds = [...new Set(grant.targets.map((target) => target.tabId))];
const requestedTabIds = Array.isArray(input.tabIds)
? input.tabIds.map(Number)
: grantedTabIds;
if (requestedTabIds.some((tabId) => !grantedTabIds.includes(tabId))) {
throw new ExtensionError(
'target_denied',
'身份隔离检查只能读取本次共享会话中的标签页',
);
}
return inspectBrowserIsolation(requestedTabIds);
: undefined;
const inspection = await inspectBrowserIsolation(requestedTabIds);
const allowedOrigins = (await getEnterprisePolicy()).policy.grantAllowedOrigins;
if (!allowedOrigins?.length) return inspection;
const tabs = inspection.tabs.filter((tab) => allowedOrigins.includes(new URL(tab.url).origin));
const tabIds = new Set(tabs.map((tab) => tab.id));
return {
...inspection,
tabs,
contexts: inspection.contexts
.map((context) => ({ ...context, tabIds: context.tabIds.filter((tabId) => tabIds.has(tabId)) }))
.filter((context) => context.tabIds.length > 0),
};
}
if (method === 'browser.isolation.proof') {
requireScope(grant, 'browser.cookies.read');
requireScope(grant, 'browser.storage.read');
const leftTabId = Number(input.leftTabId);
const rightTabId = Number(input.rightTabId);
if (![leftTabId, rightTabId].every((tabId) => (
grant.targets.some((target) => target.tabId === tabId)
))) {
throw new ExtensionError(
'target_denied',
'隔离证明的两个身份都必须在本次共享会话中',
);
}
await Promise.all([
allowedTarget(grant, { tabId: leftTabId }, false),
allowedTarget(grant, { tabId: rightTabId }, false),
]);
return createBrowserIsolationProof(leftTabId, rightTabId);
}
if (method === 'browser.isolation.incognito.open') {
assertBrowserAccessPolicy((await getEnterprisePolicy()).policy, {
origin: new URL(String(input.url || '')).origin,
});
return openIncognitoIdentity(String(input.url || ''));
}
if (method === 'browser.isolation.container.open') {
assertBrowserAccessPolicy((await getEnterprisePolicy()).policy, {
origin: new URL(String(input.url || '')).origin,
});
return openFirefoxContainerIdentity({
url: String(input.url || ''),
name: typeof input.name === 'string' ? input.name : undefined,
@@ -48,12 +48,7 @@ export const pageCapabilityHandler: CapabilityDomainHandler = {
tabId: target.tabId,
frameId: target.frameId,
});
const grantTarget = grant.targets.find((item) => (
item.tabId === target.tabId && item.frameId === target.frameId
));
const url = frame?.url && /^https?:/i.test(frame.url)
? frame.url
: `${grantTarget?.origin || ''}/`;
const url = frame?.url || '';
if (!/^https?:/i.test(url)) {
throw new ExtensionError(
'target_unavailable',
@@ -3,6 +3,7 @@ import type {
BrowserTransformPacket,
BrowserTransformProfileInput,
} from '@/types/models';
import { browser } from 'wxt/browser';
import type { CapabilityDomainHandler } from '../capability-context';
import { allowedTarget, requireScope } from '../capability-context';
import {
@@ -124,11 +125,9 @@ export const transformCapabilityHandler: CapabilityDomainHandler = {
if (method === 'browser.transform.profile.save') {
const profileInput = input as unknown as BrowserTransformProfileInput;
const target = await allowedTarget(grant, profileInput.target);
const grantedTarget = grant.targets.find((item) => (
item.tabId === target.tabId && item.frameId === target.frameId
));
if (!grantedTarget || profileInput.origin !== grantedTarget.origin) {
throw new ExtensionError('target_denied', '转换配置来源不在本次共享会话中');
const frame = await browser.webNavigation.getFrame(target);
if (!frame?.url || profileInput.origin !== new URL(frame.url).origin) {
throw new ExtensionError('target_denied', '转换配置来源与当前页面不一致');
}
return saveBrowserTransformProfile({ ...profileInput, target });
}