mirror of
https://github.com/yaklang/yaklang-chrome-extension.git
synced 2026-09-26 05:01:53 +08:00
feat(browser): add managed instance agent capabilities
This commit is contained in:
@@ -57,6 +57,12 @@ describe('Bridge v3 protocol', () => {
|
||||
expect(() => parseBridgeEnvelope('x'.repeat(BRIDGE_MAX_MESSAGE_BYTES + 1))).toThrow('16 MiB');
|
||||
});
|
||||
|
||||
it('opens only HTTP(S) pages in the attached browser instance', () => {
|
||||
expect(parseCapabilityParams('browser.tab.open', { url: 'https://www.baidu.com/' }))
|
||||
.toEqual({ url: 'https://www.baidu.com/' });
|
||||
expect(() => parseCapabilityParams('browser.tab.open', { url: 'chrome://settings' })).toThrow('HTTP(S)');
|
||||
});
|
||||
|
||||
it('accepts exact Worker boundary handles for remote deep capture', () => {
|
||||
expect(parseCapabilityParams('browser.deep_capture.start', {
|
||||
matcher: {
|
||||
|
||||
@@ -19,6 +19,7 @@ export interface BridgePairingEnvelope {
|
||||
protocolVersion?: number;
|
||||
requestId?: string;
|
||||
installationId?: string;
|
||||
managedInstance?: BridgeEnvelope['managedInstance'];
|
||||
client?: string;
|
||||
version?: string;
|
||||
nonce?: string;
|
||||
@@ -99,6 +100,8 @@ const authorizationResourceValue = v.strictObject({
|
||||
export const capabilityParams = {
|
||||
'system.ping': v.optional(v.strictObject({})),
|
||||
'browser.tabs': v.optional(v.strictObject({})),
|
||||
'browser.tab.open': v.strictObject({ url: httpUrl }),
|
||||
'browser.thumbnail': v.optional(v.strictObject({ tabId: optionalTabId })),
|
||||
'browser.frames': v.optional(v.strictObject({ tabId: optionalTabId })),
|
||||
'browser.isolation.inspect': v.optional(v.strictObject({
|
||||
tabIds: v.optional(v.pipe(v.array(tabId), v.minLength(1), v.maxLength(256))),
|
||||
@@ -183,6 +186,7 @@ export const capabilityParams = {
|
||||
}), v.check((input) => input.action !== 'setValue' || typeof input.value === 'string', 'setValue 操作必须提供 value')),
|
||||
'browser.cookies': v.optional(v.strictObject(targetFields)),
|
||||
'browser.takeover': v.optional(v.strictObject(targetFields)),
|
||||
'browser.instance.close': v.optional(v.strictObject({})),
|
||||
'browser.handoff.request': v.strictObject({
|
||||
...targetFields,
|
||||
reason: v.picklist(['qr_code', 'mfa', 'captcha', 'device_confirmation', 'other']),
|
||||
@@ -354,6 +358,7 @@ export function parseBridgeEnvelope(raw: unknown): BridgeEnvelope {
|
||||
const allowedKeys = new Set([
|
||||
'id', 'type', 'method', 'params', 'result', 'error', 'client', 'version', 'protocolVersion',
|
||||
'capabilities', 'capabilityCatalog', 'sessionId', 'taskId', 'grantId', 'installationId',
|
||||
'managedInstance',
|
||||
'engineInstanceId', 'engineIdentityId', 'challenge', 'signature', 'publicKey', 'connectionId',
|
||||
'resumeSessionId', 'resumed', 'sequence', 'timestamp', 'replyTimestamp', 'transferId', 'index',
|
||||
'total', 'data', 'originalBytes',
|
||||
@@ -361,6 +366,16 @@ export function parseBridgeEnvelope(raw: unknown): BridgeEnvelope {
|
||||
const unexpected = Object.keys(message).find((key) => !allowedKeys.has(key));
|
||||
if (unexpected) throw new Error(`Bridge 消息包含未声明字段 $.${unexpected}`);
|
||||
if (typeof message.type !== 'string') throw new Error('Bridge 消息缺少 type');
|
||||
if (message.managedInstance !== undefined) {
|
||||
const managed = message.managedInstance as Record<string, unknown>;
|
||||
if (!managed || typeof managed !== 'object' || Array.isArray(managed)
|
||||
|| !['ytray', 'yakit'].includes(String(managed.manager || ''))
|
||||
|| typeof managed.instanceId !== 'string' || !/^[A-Za-z0-9-]{1,160}$/.test(managed.instanceId)
|
||||
|| typeof managed.badge !== 'string' || !/^[A-Z]{1,2}$/.test(managed.badge)
|
||||
|| Object.keys(managed).some((key) => !['manager', 'instanceId', 'badge'].includes(key))) {
|
||||
throw new Error('Bridge 浏览器实例身份无效');
|
||||
}
|
||||
}
|
||||
|
||||
if (message.type === 'challenge') {
|
||||
if (message.protocolVersion !== BRIDGE_PROTOCOL_VERSION) throw new Error(`Bridge 协议版本不兼容: ${String(message.protocolVersion)}`);
|
||||
|
||||
@@ -33,11 +33,19 @@ const CAPABILITY_METADATA = {
|
||||
scopes: [], targetMode: 'none', defaultTimeoutMs: READ_TIMEOUT_MS,
|
||||
},
|
||||
'browser.tabs': {
|
||||
domain: 'page', access: 'read', summary: '列出当前 grant 明确共享的标签页',
|
||||
domain: 'page', access: 'read', summary: '列出当前浏览器实例中的全部 HTTP(S) 标签页;配对实例无需逐页授权',
|
||||
scopes: ['browser.tabs.read'], targetMode: 'none', defaultTimeoutMs: READ_TIMEOUT_MS,
|
||||
},
|
||||
'browser.tab.open': {
|
||||
domain: 'page', access: 'write', summary: '在当前浏览器实例中新建并前台打开 HTTP(S) 页面',
|
||||
scopes: ['browser.tabs.write'], targetMode: 'none', defaultTimeoutMs: READ_TIMEOUT_MS,
|
||||
},
|
||||
'browser.thumbnail': {
|
||||
domain: 'page', access: 'read', summary: '读取当前可见标签页的低清预览图,供 Yakit 实例列表展示',
|
||||
scopes: ['browser.tabs.read'], targetMode: 'tab', defaultTimeoutMs: READ_TIMEOUT_MS,
|
||||
},
|
||||
'browser.isolation.inspect': {
|
||||
domain: 'isolation', access: 'read', summary: '读取共享标签页的 Cookie Store 与身份隔离上下文',
|
||||
domain: 'isolation', access: 'read', summary: '读取浏览器实例内标签页的 Cookie Store 与身份隔离上下文',
|
||||
scopes: ['browser.isolation.read'], targetMode: 'none', defaultTimeoutMs: READ_TIMEOUT_MS,
|
||||
},
|
||||
'browser.isolation.proof': {
|
||||
@@ -67,7 +75,7 @@ const CAPABILITY_METADATA = {
|
||||
targetMode: 'document', defaultTimeoutMs: READ_TIMEOUT_MS,
|
||||
},
|
||||
'browser.authorization.context.get': {
|
||||
domain: 'authorization', access: 'sensitive-read', summary: '实时复核并读取当前共享会话中的短时认证上下文句柄',
|
||||
domain: 'authorization', access: 'sensitive-read', summary: '实时复核并读取当前浏览器实例中的短时认证上下文句柄',
|
||||
scopes: ['browser.isolation.read', 'browser.cookies.read', 'browser.storage.read'],
|
||||
targetMode: 'none', defaultTimeoutMs: READ_TIMEOUT_MS,
|
||||
},
|
||||
@@ -127,7 +135,7 @@ const CAPABILITY_METADATA = {
|
||||
targetMode: 'none', defaultTimeoutMs: REPLAY_TIMEOUT_MS,
|
||||
},
|
||||
'browser.frames': {
|
||||
domain: 'page', access: 'read', summary: '列出共享标签页中的 Frame',
|
||||
domain: 'page', access: 'read', summary: '列出浏览器实例指定标签页中的 Frame',
|
||||
scopes: ['browser.tabs.read'], targetMode: 'tab', defaultTimeoutMs: READ_TIMEOUT_MS,
|
||||
},
|
||||
'browser.context': {
|
||||
@@ -155,6 +163,10 @@ const CAPABILITY_METADATA = {
|
||||
domain: 'page', access: 'write', summary: '将目标标签页切换到前台',
|
||||
scopes: ['browser.tab.activate'], targetMode: 'document', defaultTimeoutMs: READ_TIMEOUT_MS,
|
||||
},
|
||||
'browser.instance.close': {
|
||||
domain: 'page', access: 'dangerous', summary: '关闭当前浏览器实例的全部窗口',
|
||||
scopes: ['browser.instance.close'], targetMode: 'none', defaultTimeoutMs: READ_TIMEOUT_MS,
|
||||
},
|
||||
'browser.handoff.request': {
|
||||
domain: 'handoff', access: 'write', summary: '请求用户完成扫码、MFA、验证码或设备确认',
|
||||
scopes: ['browser.human.takeover'], targetMode: 'document', defaultTimeoutMs: READ_TIMEOUT_MS,
|
||||
@@ -174,7 +186,7 @@ const CAPABILITY_METADATA = {
|
||||
scopes: ['browser.network.read'], targetMode: 'document', defaultTimeoutMs: READ_TIMEOUT_MS,
|
||||
},
|
||||
'browser.network.list': {
|
||||
domain: 'network', access: 'read', summary: '列出已捕获请求,敏感字段继续受 grant 约束',
|
||||
domain: 'network', access: 'read', summary: '列出已捕获请求,敏感字段仍由 Agent 操作审核策略保护',
|
||||
scopes: ['browser.network.read'], targetMode: 'document', defaultTimeoutMs: READ_TIMEOUT_MS,
|
||||
},
|
||||
'browser.network.clear': {
|
||||
@@ -385,9 +397,11 @@ export const READ_CAPABILITY_SCOPES: CapabilityScope[] = [
|
||||
|
||||
export const CONTROL_CAPABILITY_SCOPES: CapabilityScope[] = [
|
||||
...READ_CAPABILITY_SCOPES,
|
||||
'browser.tabs.write',
|
||||
'browser.dom.write',
|
||||
'browser.isolation.manage',
|
||||
'browser.tab.activate',
|
||||
'browser.instance.close',
|
||||
...(!(import.meta.env.FIREFOX && import.meta.env.MODE === 'store')
|
||||
? ['browser.page.invoke' as const, 'browser.page.eval.expression' as const]
|
||||
: []),
|
||||
@@ -410,6 +424,7 @@ export const CONTROL_CAPABILITY_SCOPES: CapabilityScope[] = [
|
||||
|
||||
export const CAPABILITY_LABELS: Record<CapabilityScope, string> = {
|
||||
'browser.tabs.read': '标签页列表',
|
||||
'browser.tabs.write': '打开网页',
|
||||
'browser.isolation.read': '读取身份隔离状态',
|
||||
'browser.isolation.manage': '创建隔离身份页面',
|
||||
'browser.dom.read': '页面 DOM',
|
||||
@@ -417,6 +432,7 @@ export const CAPABILITY_LABELS: Record<CapabilityScope, string> = {
|
||||
'browser.storage.read': '页面 Storage',
|
||||
'browser.cookies.read': 'Cookie',
|
||||
'browser.tab.activate': '切到前台',
|
||||
'browser.instance.close': '关闭浏览器实例',
|
||||
'browser.page.invoke': '调用页面函数',
|
||||
'browser.page.eval.expression': '执行页面表达式',
|
||||
'browser.page.eval.program': '执行页面程序',
|
||||
|
||||
@@ -120,6 +120,17 @@ describe('extension request schemas', () => {
|
||||
})).toThrow('HTTP(S)');
|
||||
});
|
||||
|
||||
it('validates manager-owned browser instance binding', () => {
|
||||
expect(parseExtensionRequest({
|
||||
action: 'bridge.managed-instance.bind',
|
||||
payload: { manager: 'ytray', instanceId: '13367db6-232a-40d1-ad84-81ee5d97634f', badge: 'B' },
|
||||
}).action).toBe('bridge.managed-instance.bind');
|
||||
expect(() => parseExtensionRequest({
|
||||
action: 'bridge.managed-instance.bind',
|
||||
payload: { manager: 'web', instanceId: '../shared', badge: '3' },
|
||||
})).toThrow();
|
||||
});
|
||||
|
||||
it('validates recording bounds and recorded page callables', () => {
|
||||
expect(parseExtensionRequest({
|
||||
action: 'recording.start',
|
||||
|
||||
@@ -167,12 +167,19 @@ const userAgentProfileInput = v.strictObject({
|
||||
userAgent: userAgentValue,
|
||||
});
|
||||
|
||||
const managedInstance = v.strictObject({
|
||||
manager: v.picklist(['ytray', 'yakit']),
|
||||
instanceId: v.pipe(v.string(), v.regex(/^[A-Za-z0-9-]{1,160}$/)),
|
||||
badge: v.pipe(v.string(), v.regex(/^[A-Z]{1,2}$/)),
|
||||
});
|
||||
|
||||
const bridgeConfig = v.strictObject({
|
||||
transport: v.picklist(['native', 'websocket']),
|
||||
nativeHost: v.pipe(v.string(), v.trim(), v.maxLength(253)),
|
||||
endpoint: v.pipe(v.string(), v.trim(), v.maxLength(2_048)),
|
||||
autoConnect: v.boolean(),
|
||||
installationId: v.pipe(v.string(), v.trim(), v.minLength(1), v.maxLength(160)),
|
||||
managedInstance: v.optional(managedInstance),
|
||||
pairedEngine: v.optional(v.strictObject({
|
||||
engineIdentityId: id,
|
||||
deviceId: id,
|
||||
@@ -226,6 +233,7 @@ const contextOptions = {
|
||||
|
||||
const capabilityScopes: readonly CapabilityScope[] = [
|
||||
'browser.tabs.read',
|
||||
'browser.tabs.write',
|
||||
'browser.isolation.read',
|
||||
'browser.isolation.manage',
|
||||
'browser.dom.read',
|
||||
@@ -233,6 +241,7 @@ const capabilityScopes: readonly CapabilityScope[] = [
|
||||
'browser.storage.read',
|
||||
'browser.cookies.read',
|
||||
'browser.tab.activate',
|
||||
'browser.instance.close',
|
||||
'browser.page.invoke',
|
||||
'browser.page.eval.expression',
|
||||
'browser.page.eval.program',
|
||||
@@ -462,6 +471,7 @@ const payloadSchemas = {
|
||||
'metrics.get': noPayload,
|
||||
'metrics.reset': noPayload,
|
||||
'bridge.config.save': bridgeConfig,
|
||||
'bridge.managed-instance.bind': managedInstance,
|
||||
'bridge.pair': noPayload,
|
||||
'bridge.pair.cancel': noPayload,
|
||||
'bridge.pair.status': noPayload,
|
||||
|
||||
Reference in New Issue
Block a user