dsh-site-bridge 0.6.4 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/README.ru.md +9 -0
- package/README.zh-CN.md +7 -0
- package/extension/manifest.json +1 -1
- package/extension/sw.js +9 -8
- package/lib/bridge.js +3 -1
- package/lib/index.js +30 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -197,6 +197,14 @@ instead of `<all_urls>` and does not inject a content script into every page. Th
|
|
|
197
197
|
step and Chrome's debugging banner. In exchange the permission is granted per tab, for the duration
|
|
198
198
|
of a session, and requests are read where they are made — no credential is copied out of the browser.
|
|
199
199
|
|
|
200
|
+
### Consent
|
|
201
|
+
|
|
202
|
+
Attaching a tab is a decision for the person by default. The agent may list tabs freely, but
|
|
203
|
+
`site_tabs {action:"claim"}` on a tab the user has not attached is refused unless the call carries
|
|
204
|
+
`confirmed: true` — and the refusal names the tab, so the agent knows what to ask about. Closing a tab
|
|
205
|
+
the user attached needs the same confirmation. This is a rule inside the plugin, not an OS permission:
|
|
206
|
+
it makes the agent stop and ask instead of quietly widening what it can reach.
|
|
207
|
+
|
|
200
208
|
## Limitations
|
|
201
209
|
|
|
202
210
|
Cross-origin iframes, closed shadow roots and body size caps are analysed in
|
package/README.ru.md
CHANGED
|
@@ -196,6 +196,15 @@ site_inject {js:"..."} → выполнить ваш скрипт в с
|
|
|
196
196
|
Chrome. Взамен право выдаётся на вкладку и на время сессии, а запросы читаются там, где они сделаны:
|
|
197
197
|
учётные данные не покидают браузер.
|
|
198
198
|
|
|
199
|
+
### Согласие
|
|
200
|
+
|
|
201
|
+
Подключение вкладки по умолчанию — решение человека. Список вкладок агент получает свободно, но
|
|
202
|
+
`site_tabs {action:"claim"}` для вкладки, которую пользователь не подключал, отклоняется, пока в вызове
|
|
203
|
+
нет `confirmed: true` — причём в отказе названа сама вкладка, чтобы было понятно, о чём спрашивать.
|
|
204
|
+
Закрытие вкладки, подключённой пользователем, требует такого же подтверждения. Это правило внутри
|
|
205
|
+
плагина, а не разрешение ОС: оно заставляет агента остановиться и спросить, вместо того чтобы тихо
|
|
206
|
+
расширять свою область доступа.
|
|
207
|
+
|
|
199
208
|
## Ограничения
|
|
200
209
|
|
|
201
210
|
Кросс-доменные iframe, закрытые shadow-корни и лимиты тела разобраны в
|
package/README.zh-CN.md
CHANGED
|
@@ -162,6 +162,13 @@ site_inject {js:"..."} → 在页面里执行你的脚本以验证
|
|
|
162
162
|
content script。代价是配对步骤与 Chrome 的调试提示条。换来的是:权限按标签页、按会话授予,请求在
|
|
163
163
|
产生的地方被读取,凭据不离开浏览器。
|
|
164
164
|
|
|
165
|
+
### 用户同意
|
|
166
|
+
|
|
167
|
+
默认情况下,连接标签页是人的决定。智能体可以自由列出标签页,但对用户未连接的标签页调用
|
|
168
|
+
`site_tabs {action:"claim"}` 会被拒绝,除非调用带有 `confirmed: true`——拒绝信息会指出具体标签页,便于
|
|
169
|
+
智能体询问。关闭用户自己连接的标签页同样需要确认。这是插件内部的规则,而不是操作系统权限:它让智能体
|
|
170
|
+
停下来询问,而不是悄悄扩大可达范围。
|
|
171
|
+
|
|
165
172
|
## 限制
|
|
166
173
|
|
|
167
174
|
跨域 iframe、封闭的 shadow root 与响应体大小上限在 [docs/limitations.md](docs/limitations.md) 中逐条分析 —— 包含代码位置、消除方案与预估成本。
|
package/extension/manifest.json
CHANGED
package/extension/sw.js
CHANGED
|
@@ -165,7 +165,7 @@ function handleMessage(message) {
|
|
|
165
165
|
|
|
166
166
|
async function handleMessageInner(message) {
|
|
167
167
|
if (message?.type === 'claim') {
|
|
168
|
-
const target = await claimTab(message.tabId)
|
|
168
|
+
const target = await claimTab(message.tabId, 'user')
|
|
169
169
|
return { ok: true, target }
|
|
170
170
|
}
|
|
171
171
|
if (message?.type === 'use') {
|
|
@@ -247,7 +247,7 @@ async function detach(tabId) {
|
|
|
247
247
|
await chrome.debugger.detach({ tabId: session.tabId }).catch(() => {})
|
|
248
248
|
}
|
|
249
249
|
|
|
250
|
-
async function attach(tabId) {
|
|
250
|
+
async function attach(tabId, attachedBy = 'agent') {
|
|
251
251
|
const existing = state.sessions.get(tabId)
|
|
252
252
|
if (existing) {
|
|
253
253
|
state.activeTabId = tabId
|
|
@@ -265,7 +265,8 @@ async function attach(tabId) {
|
|
|
265
265
|
const tab = await chrome.tabs.get(tabId)
|
|
266
266
|
const session = {
|
|
267
267
|
tabId,
|
|
268
|
-
|
|
268
|
+
attachedBy,
|
|
269
|
+
target: { id: tab.id, url: tab.url, title: tab.title, attachedBy },
|
|
269
270
|
recording: false,
|
|
270
271
|
entries: [],
|
|
271
272
|
picking: false,
|
|
@@ -278,7 +279,7 @@ async function attach(tabId) {
|
|
|
278
279
|
return session.target
|
|
279
280
|
}
|
|
280
281
|
|
|
281
|
-
async function claimTab(tabId) {
|
|
282
|
+
async function claimTab(tabId, attachedBy = 'agent') {
|
|
282
283
|
// Without an explicit id, claim whatever tab the user is looking at, so a
|
|
283
284
|
// caller that only knows "this tab" cannot silently pass NaN.
|
|
284
285
|
let id = Number(tabId)
|
|
@@ -291,13 +292,13 @@ async function claimTab(tabId) {
|
|
|
291
292
|
if (!/^https?:|^file:/.test(tab.url ?? '')) {
|
|
292
293
|
throw new Error(`cannot drive ${tab.url || 'this tab'}: only http(s) and file:// pages are supported`)
|
|
293
294
|
}
|
|
294
|
-
return attach(id)
|
|
295
|
+
return attach(id, attachedBy)
|
|
295
296
|
}
|
|
296
297
|
|
|
297
298
|
chrome.tabs.onUpdated.addListener((tabId, info, tab) => {
|
|
298
299
|
const session = state.sessions.get(tabId)
|
|
299
300
|
if (!session) return
|
|
300
|
-
const next = { id: tab.id, url: tab.url, title: tab.title }
|
|
301
|
+
const next = { id: tab.id, url: tab.url, title: tab.title, attachedBy: session.attachedBy }
|
|
301
302
|
if (next.title === session.target.title && next.url === session.target.url) return
|
|
302
303
|
session.target = next
|
|
303
304
|
void evt('target', { tab: next, tabId })
|
|
@@ -1530,7 +1531,7 @@ async function dispatch(op, args) {
|
|
|
1530
1531
|
}
|
|
1531
1532
|
case 'tab.open': {
|
|
1532
1533
|
const tab = await chrome.tabs.create({ url: String(args.url ?? 'about:blank'), active: args.active !== false })
|
|
1533
|
-
await attach(tab.id)
|
|
1534
|
+
await attach(tab.id, 'agent')
|
|
1534
1535
|
// A fresh tab has no title yet: wait for the load so the caller gets a
|
|
1535
1536
|
// target it can show, instead of an empty string.
|
|
1536
1537
|
const deadline = Date.now() + Math.min(Number(args.waitMs ?? 10000), 30000)
|
|
@@ -1542,7 +1543,7 @@ async function dispatch(op, args) {
|
|
|
1542
1543
|
}
|
|
1543
1544
|
const session = state.sessions.get(tab.id)
|
|
1544
1545
|
if (session) {
|
|
1545
|
-
session.target = { id: info.id, url: info.url, title: info.title }
|
|
1546
|
+
session.target = { id: info.id, url: info.url, title: info.title, attachedBy: session.attachedBy }
|
|
1546
1547
|
await evt('target', { tab: session.target, tabId: tab.id })
|
|
1547
1548
|
}
|
|
1548
1549
|
return { target: session?.target ?? { id: tab.id, url: tab.url, title: tab.title } }
|
package/lib/bridge.js
CHANGED
|
@@ -320,7 +320,9 @@ export class Bridge {
|
|
|
320
320
|
const tabId = Number(event.tabId ?? tab?.id)
|
|
321
321
|
if (single) {
|
|
322
322
|
const id = Number(tab.id)
|
|
323
|
-
|
|
323
|
+
// Keep whatever else the extension says about the tab (attachedBy, for
|
|
324
|
+
// example): the consent rule depends on it.
|
|
325
|
+
this.targets.set(id, { ...tab, id, url: String(tab.url ?? ''), title: String(tab.title ?? ''), at: Date.now() })
|
|
324
326
|
if (this.activeTabId == null) this.activeTabId = id
|
|
325
327
|
} else if (Number.isFinite(tabId)) {
|
|
326
328
|
this.targets.delete(tabId)
|
package/lib/index.js
CHANGED
|
@@ -259,15 +259,25 @@ export function apply(ctx, config) {
|
|
|
259
259
|
'site_tabs',
|
|
260
260
|
'Вкладки браузера и выбор рабочей вкладки. Можно держать подключёнными несколько вкладок: ' +
|
|
261
261
|
'claim добавляет ещё одну, use переключает активную, release отключает, open открывает новую и сразу ' +
|
|
262
|
-
'подключает её, focus выводит вкладку на передний план. Вкладка должна быть http(s) или file
|
|
262
|
+
'подключает её, focus выводит вкладку на передний план. Вкладка должна быть http(s) или file://.\n' +
|
|
263
|
+
'ПРАВИЛО: подключать вкладку, которую пользователь не подключал сам, можно только с его явного ' +
|
|
264
|
+
'разрешения. Без confirmed:true вызов claim отклоняется — сначала спросите пользователя.',
|
|
263
265
|
{
|
|
264
266
|
action: { type: 'string', description: 'list | claim | use | release | open | close | focus' },
|
|
265
|
-
tabId: { type: 'number', description: 'id вкладки для claim / use / release / focus (из action=list)' },
|
|
266
|
-
url: { type: 'string', description: 'адрес для action=open (по умолчанию about:blank)' }
|
|
267
|
+
tabId: { type: 'number', description: 'id вкладки для claim / use / release / focus / close (из action=list)' },
|
|
268
|
+
url: { type: 'string', description: 'адрес для action=open (по умолчанию about:blank)' },
|
|
269
|
+
confirmed: {
|
|
270
|
+
type: 'boolean',
|
|
271
|
+
description:
|
|
272
|
+
'Только после явного разрешения пользователя: подтверждает подключение вкладки, ' +
|
|
273
|
+
'которую он не подключал, или закрытие вкладки, которую он подключил сам'
|
|
274
|
+
}
|
|
267
275
|
},
|
|
268
276
|
async (args) => {
|
|
269
277
|
const instance = await ensureBridge()
|
|
270
278
|
const action = String(args.action ?? 'list')
|
|
279
|
+
const confirmed = args.confirmed === true
|
|
280
|
+
const attached = (id) => (instance.state().targets ?? []).find((item) => item.id === id) ?? null
|
|
271
281
|
if (action === 'open') {
|
|
272
282
|
const result = await instance.enqueue(
|
|
273
283
|
'tab.open',
|
|
@@ -280,6 +290,13 @@ export function apply(ctx, config) {
|
|
|
280
290
|
if (action === 'close') {
|
|
281
291
|
const tabId = Number(args.tabId ?? instance.state().activeTabId)
|
|
282
292
|
if (!Number.isFinite(tabId)) throw new Error('нечего закрывать: передайте tabId')
|
|
293
|
+
const known = attached(tabId)
|
|
294
|
+
if (known?.attachedBy === 'user' && !confirmed) {
|
|
295
|
+
throw new Error(
|
|
296
|
+
`Вкладку [${tabId}] ${known.title || known.url || ''} подключили вы, а не агент. ` +
|
|
297
|
+
'Закрытие такой вкладки требует явного подтверждения: спросите пользователя и повторите с confirmed:true.'
|
|
298
|
+
)
|
|
299
|
+
}
|
|
283
300
|
await instance.enqueue('tab.close', { tabId }, { timeout: 20000 })
|
|
284
301
|
return ok(`Вкладка ${tabId} закрыта.`, { tabId })
|
|
285
302
|
}
|
|
@@ -292,6 +309,16 @@ export function apply(ctx, config) {
|
|
|
292
309
|
if (action === 'claim' || action === 'use' || action === 'release') {
|
|
293
310
|
const tabId = Number(args.tabId)
|
|
294
311
|
if (!Number.isFinite(tabId)) throw new Error(`tabId is required for action=${action}`)
|
|
312
|
+
if (action === 'claim' && !attached(tabId) && !confirmed) {
|
|
313
|
+
const list = await instance.enqueue('tabs.list', {}, { timeout: 20000 })
|
|
314
|
+
const known = (list?.tabs ?? []).find((item) => item.id === tabId)
|
|
315
|
+
const label = known ? `${known.title || '(без названия)'} — ${known.url}` : `вкладка ${tabId}`
|
|
316
|
+
throw new Error(
|
|
317
|
+
`Вкладка не подключена: ${label}. Пользователь её не подключал, а правило плагина требует его ` +
|
|
318
|
+
'явного разрешения на доступ. Спросите пользователя (назовите эту вкладку) и только после ' +
|
|
319
|
+
'согласия повторите вызов с confirmed:true.'
|
|
320
|
+
)
|
|
321
|
+
}
|
|
295
322
|
const op = action === 'claim' ? 'tab.claim' : action === 'use' ? 'tab.use' : 'tab.release'
|
|
296
323
|
const result = await instance.enqueue(op, { tabId }, { timeout: 20000 })
|
|
297
324
|
if (action === 'release') return ok(`Вкладка ${tabId} отключена.`, { tabId })
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-site-bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Chrome/Edge MV3 bridge for DeepSeek Harness: capture page markup (DOM, styles, selectors) and BFF traffic (xhr/fetch with headers and bodies) into a workspace snapshot bundle, drive the page with trusted CDP input, and verify injected scripts. Authorization never leaves the browser and is never written to disk.",
|
|
5
5
|
"icon": "./icon.svg",
|
|
6
6
|
"type": "module",
|