dsh-builtin-browser 0.3.0 → 0.4.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.en.md +78 -33
- package/README.md +74 -33
- package/cordis.patch.yml +13 -7
- package/desktop-bridge/install.mjs +274 -12
- package/desktop-bridge/plugin-browser-bridge.js +55 -15
- package/lib/browser-electron/desktop-bridge-host.d.ts +10 -1
- package/lib/browser-electron/desktop-bridge-host.js +10 -2
- package/lib/browser-electron/entry.js +41 -22
- package/lib/browser-electron/history-store.js +5 -1
- package/lib/browser-electron/missing-system-browser.d.ts +54 -0
- package/lib/browser-electron/missing-system-browser.js +89 -0
- package/lib/browser-electron/provider.d.ts +54 -1
- package/lib/browser-electron/provider.js +241 -37
- package/lib/browser-electron/remote-host.js +6 -3
- package/lib/browser-electron/settings-store.js +10 -4
- package/lib/browser-electron/system-browser.d.ts +75 -22
- package/lib/browser-electron/system-browser.js +314 -65
- package/lib/tool-browser/index.d.ts +14 -0
- package/lib/tool-browser/index.js +142 -57
- package/package.json +1 -1
- package/src/browser-electron/desktop-bridge-host.ts +11 -2
- package/src/browser-electron/entry.ts +42 -22
- package/src/browser-electron/history-store.ts +5 -1
- package/src/browser-electron/missing-system-browser.ts +113 -0
- package/src/browser-electron/provider.ts +267 -36
- package/src/browser-electron/remote-host.ts +6 -3
- package/src/browser-electron/settings-store.ts +11 -4
- package/src/browser-electron/system-browser.ts +292 -59
- package/src/tool-browser/index.ts +148 -62
|
@@ -19,6 +19,27 @@ import { defineTool } from '@deepseek-ai/dsh-tools'
|
|
|
19
19
|
import type {} from '@deepseek-ai/dsh-system-prompt'
|
|
20
20
|
import type { BrowserSessionId } from '../browser/types.js'
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* Register a cleanup that runs once when the process exits.
|
|
24
|
+
*
|
|
25
|
+
* A single hook per process, holding a list of callbacks, rather than one listener per
|
|
26
|
+
* session: sessions come and go, listeners do not, and Node warns when a listener count
|
|
27
|
+
* passes ten.
|
|
28
|
+
*/
|
|
29
|
+
const exitCleanups = new Set<() => void>()
|
|
30
|
+
let exitHookInstalled = false
|
|
31
|
+
function registerExitCleanup(cleanup: () => void): void {
|
|
32
|
+
exitCleanups.add(cleanup)
|
|
33
|
+
if (exitHookInstalled) return
|
|
34
|
+
exitHookInstalled = true
|
|
35
|
+
process.on('exit', () => {
|
|
36
|
+
for (const run of exitCleanups) {
|
|
37
|
+
try { run() } catch { /* exiting; nothing useful to do */ }
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
|
|
22
43
|
/** Plugin name used by loader diagnostics. */
|
|
23
44
|
export const name = 'tool-browser'
|
|
24
45
|
/** The tool registry, browser seam, and system-prompt registry this tool layer consumes. */
|
|
@@ -41,17 +62,21 @@ interface ToolBrowserState {
|
|
|
41
62
|
/** In-flight first-open per task key, so concurrent first calls share one session. */
|
|
42
63
|
readonly pendingOpens: Map<string, Promise<BrowserSessionId>>
|
|
43
64
|
/**
|
|
44
|
-
* Action restriction
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
65
|
+
* Action restriction per task, plus one from the plugin's own configuration.
|
|
66
|
+
*
|
|
67
|
+
* Two different things share this policy. A restriction from `cordis.patch.yml` is an
|
|
68
|
+
* operator decision about the whole plugin, so it applies to everyone and lives in
|
|
69
|
+
* `configRestrictedTo`. One set by `browser_restrict` is a decision by one task about
|
|
70
|
+
* its own work, so it is keyed by task — a single shared value let one task lock the
|
|
71
|
+
* browser tools of every other task, which is not what any caller asks for. Sessions
|
|
72
|
+
* beside it were already keyed this way; this was not.
|
|
49
73
|
*/
|
|
50
|
-
|
|
74
|
+
configRestrictedTo: readonly string[] | undefined
|
|
75
|
+
restrictedByTask: Map<string, readonly string[]>
|
|
51
76
|
}
|
|
52
77
|
|
|
53
78
|
function createState(): ToolBrowserState {
|
|
54
|
-
return { sessionsByTask: new Map(), pendingOpens: new Map(),
|
|
79
|
+
return { sessionsByTask: new Map(), pendingOpens: new Map(), configRestrictedTo: undefined, restrictedByTask: new Map() }
|
|
55
80
|
}
|
|
56
81
|
|
|
57
82
|
/** Every live state, for the test hook (introspection only, never shared). */
|
|
@@ -63,11 +88,41 @@ const liveStates = new Set<ToolBrowserState>()
|
|
|
63
88
|
* @param state - the calling context's tool state.
|
|
64
89
|
* @param toolName - the browser tool about to run.
|
|
65
90
|
*/
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
91
|
+
/**
|
|
92
|
+
* Tools that only observe, plus the ones that undo a restriction.
|
|
93
|
+
*
|
|
94
|
+
* Never subject to an allow-list: restricting what the agent may *do* must not also
|
|
95
|
+
* blind it or trap it, and the tool descriptions promise as much. browser_restrict
|
|
96
|
+
* is in here so a task that restricted everything can still lift it.
|
|
97
|
+
*/
|
|
98
|
+
export const READ_ONLY_TOOLS: ReadonlySet<string> = new Set([
|
|
99
|
+
// Tools that only observe.
|
|
100
|
+
'browser_snapshot', 'browser_a11y', 'browser_content', 'browser_scrape',
|
|
101
|
+
'browser_screenshot', 'browser_get_value', 'browser_wait', 'browser_challenge',
|
|
102
|
+
'browser_list_tabs', 'browser_session', 'browser_history', 'browser_visited',
|
|
103
|
+
// browser_auth reads cookies out of the browser: an observation, and one the
|
|
104
|
+
// credentials switch gates on its own (see credentials.allowRead in the settings).
|
|
105
|
+
'browser_auth',
|
|
106
|
+
// Getting out of a bad state, or lifting the restriction itself. Without these a task
|
|
107
|
+
// that restricted everything could neither recover nor be released.
|
|
108
|
+
'browser_restrict', 'browser_reset_session', 'browser_reset',
|
|
109
|
+
])
|
|
110
|
+
function assertAllowed(state: ToolBrowserState, toolName: string, task: string): void {
|
|
111
|
+
// browser_restrict must stay usable, or a task that restricted everything
|
|
112
|
+
// could never lift it again.
|
|
113
|
+
// An allow-list is a statement about ACTIONS, so it must not take away the ability to
|
|
114
|
+
// look, or to get out of a bad state. The tool description and both READMEs promise
|
|
115
|
+
// that read-only tools are never blocked; before this they were, because only
|
|
116
|
+
// browser_restrict itself was exempt — a narrow list then locked browser_history,
|
|
117
|
+
// browser_reset_session and browser_auth away with no way back.
|
|
118
|
+
if (READ_ONLY_TOOLS.has(toolName) || toolName === 'browser_restrict') return
|
|
119
|
+
const config = state.configRestrictedTo
|
|
120
|
+
if (config !== undefined && !config.includes(toolName)) {
|
|
121
|
+
throw new Error(`browser action "${toolName}" is restricted by the plugin configuration (allow-list: ${config.join(', ')})`)
|
|
122
|
+
}
|
|
123
|
+
const allowed = state.restrictedByTask.get(task)
|
|
124
|
+
if (allowed === undefined || allowed.includes(toolName)) return
|
|
125
|
+
throw new Error(`browser action "${toolName}" is restricted (allow-list: ${allowed.join(', ')})`)
|
|
71
126
|
}
|
|
72
127
|
|
|
73
128
|
/** The agent view a tool execution carries (id + agent-scoped context). */
|
|
@@ -135,9 +190,10 @@ async function ensureSession(browser: NonNullable<Context['browser']>, state: To
|
|
|
135
190
|
} else if (key === 'default') {
|
|
136
191
|
// Agentless / CLI probe: close the session on process exit so the
|
|
137
192
|
// window is not orphaned.
|
|
138
|
-
process
|
|
139
|
-
|
|
140
|
-
|
|
193
|
+
// One hook for the whole process, not one listener per session: a default session can
|
|
194
|
+
// be created repeatedly (a CLI probe does), and each registration lived for the life of
|
|
195
|
+
// the process — so the listener list grew without bound and Node warned at eleven.
|
|
196
|
+
registerExitCleanup(() => browser.close(session).catch(() => {}))
|
|
141
197
|
}
|
|
142
198
|
} catch (error) {
|
|
143
199
|
// effect() failed — undo the registration so future calls retry.
|
|
@@ -157,7 +213,10 @@ async function ensureSession(browser: NonNullable<Context['browser']>, state: To
|
|
|
157
213
|
function parseFillValue(v: string | undefined): string | number | boolean {
|
|
158
214
|
if (v === 'true') return true
|
|
159
215
|
if (v === 'false') return false
|
|
160
|
-
|
|
216
|
+
// Deliberately NOT coerced to a number. The description says numbers are accepted in
|
|
217
|
+
// string form, and callers pass account numbers, postcodes and zero-padded ids through
|
|
218
|
+
// here: "007" became "7" and "1e3" became "1000", which silently rewrote the value they
|
|
219
|
+
// meant to type. A numeric-looking string is a string; the DOM decides how to store it.
|
|
161
220
|
return v ?? ''
|
|
162
221
|
}
|
|
163
222
|
|
|
@@ -200,7 +259,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
200
259
|
liveStates.add(state)
|
|
201
260
|
ctx.effect(() => () => { liveStates.delete(state) })
|
|
202
261
|
// Re-apply resets the restriction: an omitted allowedActions lifts it.
|
|
203
|
-
state.
|
|
262
|
+
state.configRestrictedTo = config.allowedActions !== undefined ? [...config.allowedActions] : undefined
|
|
204
263
|
|
|
205
264
|
ctx.systemPrompt.section({
|
|
206
265
|
name: 'tool:browser',
|
|
@@ -255,12 +314,12 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
255
314
|
// browser_open is an overview of what just loaded, not a locator: it has no
|
|
256
315
|
// `coords` parameter, so coordinates stay off here (browser_snapshot owns
|
|
257
316
|
// that switch for callers that actually target pixels).
|
|
258
|
-
render: (args, value) => [{ type: 'text', text: formatSnapshot(value, {
|
|
317
|
+
render: (args, value) => [{ type: 'text', text: formatSnapshot(value, {}) }],
|
|
259
318
|
},
|
|
260
319
|
timeoutMs,
|
|
261
320
|
isConcurrencySafe: () => false, // opens tabs / navigates; exclusive within a task
|
|
262
321
|
async execute(args, exec) {
|
|
263
|
-
assertAllowed(state, 'browser_open')
|
|
322
|
+
assertAllowed(state, 'browser_open', taskKey(exec))
|
|
264
323
|
const browser = ctx.get('browser')
|
|
265
324
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
266
325
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -525,7 +584,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
525
584
|
timeoutMs,
|
|
526
585
|
isConcurrencySafe: () => false, // page JS can be stateful
|
|
527
586
|
async execute(args, exec) {
|
|
528
|
-
assertAllowed(state, 'browser_execute')
|
|
587
|
+
assertAllowed(state, 'browser_execute', taskKey(exec))
|
|
529
588
|
const browser = ctx.get('browser')
|
|
530
589
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
531
590
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -612,7 +671,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
612
671
|
timeoutMs,
|
|
613
672
|
isConcurrencySafe: () => true,
|
|
614
673
|
async execute(args, exec) {
|
|
615
|
-
assertAllowed(state, 'browser_scrape')
|
|
674
|
+
assertAllowed(state, 'browser_scrape', taskKey(exec))
|
|
616
675
|
const browser = ctx.get('browser')
|
|
617
676
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
618
677
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -650,11 +709,17 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
650
709
|
timeoutMs,
|
|
651
710
|
isConcurrencySafe: () => false,
|
|
652
711
|
async execute(args, exec) {
|
|
653
|
-
assertAllowed(state, 'browser_click')
|
|
712
|
+
assertAllowed(state, 'browser_click', taskKey(exec))
|
|
654
713
|
const browser = ctx.get('browser')
|
|
655
714
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
656
715
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
657
716
|
const target = args.target as { by?: string; value?: string; index?: number } | undefined
|
|
717
|
+
// A target whose value is empty is a caller mistake, not a request to fall back.
|
|
718
|
+
// Degrading silently sent typed text to whatever held focus — possibly a password
|
|
719
|
+
// field — or dropped the semantic target in favour of coordinates.
|
|
720
|
+
if (target !== undefined && typeof target.value === 'string' && target.value === '') {
|
|
721
|
+
throw new Error('target.value is empty; pass a css selector, visible text or XPath, or omit target entirely to use x/y')
|
|
722
|
+
}
|
|
658
723
|
if (target !== undefined && typeof target.value === 'string' && target.value !== '') {
|
|
659
724
|
await browser.click(session, {
|
|
660
725
|
target: {
|
|
@@ -696,11 +761,17 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
696
761
|
timeoutMs,
|
|
697
762
|
isConcurrencySafe: () => false,
|
|
698
763
|
async execute(args, exec) {
|
|
699
|
-
assertAllowed(state, 'browser_type')
|
|
764
|
+
assertAllowed(state, 'browser_type', taskKey(exec))
|
|
700
765
|
const browser = ctx.get('browser')
|
|
701
766
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
702
767
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
703
768
|
const target = args.target as { by?: string; value?: string; index?: number } | undefined
|
|
769
|
+
// A target whose value is empty is a caller mistake, not a request to fall back.
|
|
770
|
+
// Degrading silently sent typed text to whatever held focus — possibly a password
|
|
771
|
+
// field — or dropped the semantic target in favour of coordinates.
|
|
772
|
+
if (target !== undefined && typeof target.value === 'string' && target.value === '') {
|
|
773
|
+
throw new Error('target.value is empty; pass a css selector, visible text or XPath, or omit target entirely to use x/y')
|
|
774
|
+
}
|
|
704
775
|
if (target !== undefined && typeof target.value === 'string' && target.value !== '') {
|
|
705
776
|
await browser.type(session, {
|
|
706
777
|
text: args.text,
|
|
@@ -734,7 +805,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
734
805
|
timeoutMs,
|
|
735
806
|
isConcurrencySafe: () => false, // mutates page scroll state; exclusive within a task
|
|
736
807
|
async execute(args, exec) {
|
|
737
|
-
assertAllowed(state, 'browser_scroll')
|
|
808
|
+
assertAllowed(state, 'browser_scroll', taskKey(exec))
|
|
738
809
|
const browser = ctx.get('browser')
|
|
739
810
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
740
811
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -760,7 +831,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
760
831
|
timeoutMs,
|
|
761
832
|
isConcurrencySafe: () => false, // mutates page state; exclusive within a task
|
|
762
833
|
async execute(_args, exec) {
|
|
763
|
-
assertAllowed(state, 'browser_back')
|
|
834
|
+
assertAllowed(state, 'browser_back', taskKey(exec))
|
|
764
835
|
const browser = ctx.get('browser')
|
|
765
836
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
766
837
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -780,7 +851,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
780
851
|
timeoutMs,
|
|
781
852
|
isConcurrencySafe: () => false, // mutates page state; exclusive within a task
|
|
782
853
|
async execute(_args, exec) {
|
|
783
|
-
assertAllowed(state, 'browser_forward')
|
|
854
|
+
assertAllowed(state, 'browser_forward', taskKey(exec))
|
|
784
855
|
const browser = ctx.get('browser')
|
|
785
856
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
786
857
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -800,7 +871,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
800
871
|
timeoutMs,
|
|
801
872
|
isConcurrencySafe: () => false, // reloads the page; exclusive within a task
|
|
802
873
|
async execute(_args, exec) {
|
|
803
|
-
assertAllowed(state, 'browser_refresh')
|
|
874
|
+
assertAllowed(state, 'browser_refresh', taskKey(exec))
|
|
804
875
|
const browser = ctx.get('browser')
|
|
805
876
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
806
877
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -822,7 +893,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
822
893
|
timeoutMs,
|
|
823
894
|
isConcurrencySafe: () => false, // mutates page state; exclusive within a task
|
|
824
895
|
async execute(args, exec) {
|
|
825
|
-
assertAllowed(state, 'browser_key')
|
|
896
|
+
assertAllowed(state, 'browser_key', taskKey(exec))
|
|
826
897
|
const browser = ctx.get('browser')
|
|
827
898
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
828
899
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -847,7 +918,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
847
918
|
label: { type: 'string', description: 'Match by associated <label> text or aria-label.' },
|
|
848
919
|
placeholder: { type: 'string', description: 'Match by placeholder text.' },
|
|
849
920
|
kind: { type: 'string', enum: ['text', 'textarea', 'checkbox', 'radio', 'select'], description: 'Field kind; defaults to text.' },
|
|
850
|
-
value: { type: 'string', description: 'Value to set (string form; booleans/numbers accepted as strings).' },
|
|
921
|
+
value: { type: 'string', required: true, description: 'Value to set (string form; booleans/numbers accepted as strings). Required: omitting it would be read as "set empty", which unchecks a checkbox and clears a text field while still reporting success.' },
|
|
851
922
|
},
|
|
852
923
|
},
|
|
853
924
|
},
|
|
@@ -891,18 +962,28 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
891
962
|
timeoutMs,
|
|
892
963
|
isConcurrencySafe: () => false,
|
|
893
964
|
async execute(args, exec) {
|
|
894
|
-
assertAllowed(state, 'browser_fill')
|
|
965
|
+
assertAllowed(state, 'browser_fill', taskKey(exec))
|
|
895
966
|
const browser = ctx.get('browser')
|
|
896
967
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
897
968
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
898
|
-
const fields = (args.fields ?? []).map((f: { selector?: string; name?: string; label?: string; placeholder?: string; kind?: string; value?: string }) =>
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
969
|
+
const fields = (args.fields ?? []).map((f: { selector?: string; name?: string; label?: string; placeholder?: string; kind?: string; value?: string }) => {
|
|
970
|
+
// A missing value is a caller mistake, not a request to clear the field. Turning
|
|
971
|
+
// it into '' silently unchecks a checkbox or empties a text input while the tool
|
|
972
|
+
// still reports success, so it fails loudly. The schema marks value required, but
|
|
973
|
+
// a call can still arrive without it.
|
|
974
|
+
if (f.value === undefined) {
|
|
975
|
+
const matcher = f.selector ?? f.name ?? f.label ?? f.placeholder ?? '(no matcher)'
|
|
976
|
+
throw new Error(`browser_fill: field "${matcher}" has no "value"; pass one (use "false" to uncheck, or "" to clear deliberately)`)
|
|
977
|
+
}
|
|
978
|
+
return {
|
|
979
|
+
...f.selector !== undefined ? { selector: f.selector } : {},
|
|
980
|
+
...f.name !== undefined ? { name: f.name } : {},
|
|
981
|
+
...f.label !== undefined ? { label: f.label } : {},
|
|
982
|
+
...f.placeholder !== undefined ? { placeholder: f.placeholder } : {},
|
|
983
|
+
...f.kind !== undefined ? { kind: f.kind as 'text' | 'textarea' | 'checkbox' | 'radio' | 'select' } : {},
|
|
984
|
+
value: parseFillValue(f.value),
|
|
985
|
+
}
|
|
986
|
+
})
|
|
906
987
|
const result = await browser.fillForm(session, {
|
|
907
988
|
fields,
|
|
908
989
|
...args.submit === true ? { submit: true } : {},
|
|
@@ -950,7 +1031,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
950
1031
|
timeoutMs,
|
|
951
1032
|
isConcurrencySafe: () => false, // mutates a field; exclusive within a task
|
|
952
1033
|
async execute(args, exec) {
|
|
953
|
-
assertAllowed(state, 'browser_set_value')
|
|
1034
|
+
assertAllowed(state, 'browser_set_value', taskKey(exec))
|
|
954
1035
|
const browser = ctx.get('browser')
|
|
955
1036
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
956
1037
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -981,7 +1062,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
981
1062
|
timeoutMs,
|
|
982
1063
|
isConcurrencySafe: () => false, // mutates a field; exclusive within a task
|
|
983
1064
|
async execute(args, exec) {
|
|
984
|
-
assertAllowed(state, 'browser_check')
|
|
1065
|
+
assertAllowed(state, 'browser_check', taskKey(exec))
|
|
985
1066
|
const browser = ctx.get('browser')
|
|
986
1067
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
987
1068
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -1021,7 +1102,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1021
1102
|
timeoutMs,
|
|
1022
1103
|
isConcurrencySafe: () => false, // mutates a field; exclusive within a task
|
|
1023
1104
|
async execute(args, exec) {
|
|
1024
|
-
assertAllowed(state, 'browser_select')
|
|
1105
|
+
assertAllowed(state, 'browser_select', taskKey(exec))
|
|
1025
1106
|
const browser = ctx.get('browser')
|
|
1026
1107
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
1027
1108
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -1053,7 +1134,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1053
1134
|
timeoutMs,
|
|
1054
1135
|
isConcurrencySafe: () => false, // mutates a field; exclusive within a task
|
|
1055
1136
|
async execute(args, exec) {
|
|
1056
|
-
assertAllowed(state, 'browser_clear')
|
|
1137
|
+
assertAllowed(state, 'browser_clear', taskKey(exec))
|
|
1057
1138
|
const browser = ctx.get('browser')
|
|
1058
1139
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
1059
1140
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -1094,7 +1175,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1094
1175
|
timeoutMs,
|
|
1095
1176
|
isConcurrencySafe: () => true,
|
|
1096
1177
|
async execute(args, exec) {
|
|
1097
|
-
assertAllowed(state, 'browser_get_value')
|
|
1178
|
+
assertAllowed(state, 'browser_get_value', taskKey(exec))
|
|
1098
1179
|
const browser = ctx.get('browser')
|
|
1099
1180
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
1100
1181
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -1116,11 +1197,11 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1116
1197
|
|
|
1117
1198
|
ctx.tools.register(defineTool({
|
|
1118
1199
|
name: 'browser_screenshot',
|
|
1119
|
-
description: 'Capture the current shared-browser page as a screenshot (PNG default, JPEG optional). This is for models that can read images: layout checks, charts, designs, CAPTCHAs, or locating an element by eye before clicking its coordinates. A model without image input gains nothing from it — browser_snapshot, browser_a11y and browser_content carry the same page as text, and browser_scrape extracts structured data. Supports full-page capture, save-to-file, JPEG encoding, and downscaling (maxWidth/maxHeight) to cut vision-tool token cost. JPEG
|
|
1200
|
+
description: 'Capture the current shared-browser page as a screenshot (PNG default, JPEG optional). This is for models that can read images: layout checks, charts, designs, CAPTCHAs, or locating an element by eye before clicking its coordinates. A model without image input gains nothing from it — browser_snapshot, browser_a11y and browser_content carry the same page as text, and browser_scrape extracts structured data. Supports full-page capture, save-to-file, JPEG encoding, and downscaling (maxWidth/maxHeight) to cut vision-tool token cost. JPEG needs a browser whose CDP encoder works — the self-hosted browser and an installed Chrome/Edge; the desktop sidebar falls back to PNG because the Electron CDP JPEG encoder hangs. Downscaling works on every carrier.',
|
|
1120
1201
|
parameters: {
|
|
1121
1202
|
fullPage: { type: 'boolean', description: 'Capture the full scrollable page instead of the viewport (default false).' },
|
|
1122
1203
|
savePath: { type: 'string', description: 'Absolute file path to also save the image to (e.g. for read_image vision location). Must resolve inside the configured downloadDir (default: the system Downloads folder, localized names such as ~/下载 included); an existing file is never overwritten.' },
|
|
1123
|
-
format: { type: 'string', enum: ['png', 'jpeg'], description: 'Image format (default png; jpeg
|
|
1204
|
+
format: { type: 'string', enum: ['png', 'jpeg'], description: 'Image format (default png; jpeg needs the self-hosted browser or an installed Chrome/Edge; the desktop sidebar returns PNG because the Electron CDP JPEG encoder hangs).' },
|
|
1124
1205
|
quality: { type: 'number', description: 'JPEG quality 1-100 (default 80); ignored for PNG.' },
|
|
1125
1206
|
maxWidth: { type: 'number', description: 'Downscale to fit within this width (aspect preserved).' },
|
|
1126
1207
|
maxHeight: { type: 'number', description: 'Downscale to fit within this height (aspect preserved).' },
|
|
@@ -1212,7 +1293,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1212
1293
|
timeoutMs,
|
|
1213
1294
|
isConcurrencySafe: () => false, // mutates the active tab; exclusive within a task
|
|
1214
1295
|
async execute(args, exec) {
|
|
1215
|
-
assertAllowed(state, 'browser_switch_tab')
|
|
1296
|
+
assertAllowed(state, 'browser_switch_tab', taskKey(exec))
|
|
1216
1297
|
const browser = ctx.get('browser')
|
|
1217
1298
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
1218
1299
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -1234,7 +1315,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1234
1315
|
timeoutMs,
|
|
1235
1316
|
isConcurrencySafe: () => false, // mutates the tab list; exclusive within a task
|
|
1236
1317
|
async execute(args, exec) {
|
|
1237
|
-
assertAllowed(state, 'browser_close_tab')
|
|
1318
|
+
assertAllowed(state, 'browser_close_tab', taskKey(exec))
|
|
1238
1319
|
const browser = ctx.get('browser')
|
|
1239
1320
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
1240
1321
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -1254,7 +1335,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1254
1335
|
timeoutMs,
|
|
1255
1336
|
isConcurrencySafe: () => false, // closes every tab; exclusive within a task
|
|
1256
1337
|
async execute(_args, exec) {
|
|
1257
|
-
assertAllowed(state, 'browser_reset')
|
|
1338
|
+
assertAllowed(state, 'browser_reset', taskKey(exec))
|
|
1258
1339
|
const browser = ctx.get('browser')
|
|
1259
1340
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
1260
1341
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -1327,9 +1408,6 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1327
1408
|
if (e.action === 'replay' && params.of === 'type' && typeof params.text === 'string') {
|
|
1328
1409
|
params.text = '*'.repeat(Math.min(params.text.length, 64)) + ` (${params.text.length} chars)`
|
|
1329
1410
|
}
|
|
1330
|
-
if (e.action === 'replay' && params.of === 'setValue' && typeof params.value === 'string') {
|
|
1331
|
-
params.value = '*'.repeat(Math.min(params.value.length, 64)) + ` (${params.value.length} chars)`
|
|
1332
|
-
}
|
|
1333
1411
|
// Execute scripts may embed form tokens / credentials; mask script,
|
|
1334
1412
|
// args, and result for both direct execute and replay-of-execute.
|
|
1335
1413
|
const isExecute = e.action === 'execute' || (e.action === 'replay' && params.of === 'execute')
|
|
@@ -1446,7 +1524,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1446
1524
|
timeoutMs,
|
|
1447
1525
|
isConcurrencySafe: () => false,
|
|
1448
1526
|
async execute(args, exec) {
|
|
1449
|
-
assertAllowed(state, 'browser_replay')
|
|
1527
|
+
assertAllowed(state, 'browser_replay', taskKey(exec))
|
|
1450
1528
|
const browser = ctx.get('browser')
|
|
1451
1529
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
1452
1530
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -1469,7 +1547,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1469
1547
|
timeoutMs,
|
|
1470
1548
|
isConcurrencySafe: () => false,
|
|
1471
1549
|
async execute(args, exec) {
|
|
1472
|
-
assertAllowed(state, 'browser_download')
|
|
1550
|
+
assertAllowed(state, 'browser_download', taskKey(exec))
|
|
1473
1551
|
const browser = ctx.get('browser')
|
|
1474
1552
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
1475
1553
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -1530,7 +1608,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1530
1608
|
timeoutMs,
|
|
1531
1609
|
isConcurrencySafe: () => false, // closes the whole session; exclusive within a task
|
|
1532
1610
|
async execute(_args, exec) {
|
|
1533
|
-
assertAllowed(state, 'browser_reset_session')
|
|
1611
|
+
assertAllowed(state, 'browser_reset_session', taskKey(exec))
|
|
1534
1612
|
const browser = ctx.get('browser')
|
|
1535
1613
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
1536
1614
|
const key = taskKey(exec)
|
|
@@ -1571,12 +1649,14 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1571
1649
|
if (unknown.length > 0) {
|
|
1572
1650
|
throw new Error(`browser_restrict: unknown tool name(s) ${unknown.map(t => `"${t}"`).join(', ')} (must start with "browser_")`)
|
|
1573
1651
|
}
|
|
1574
|
-
// Empty list (or omitted) lifts
|
|
1575
|
-
// new allow-list.
|
|
1576
|
-
//
|
|
1577
|
-
//
|
|
1578
|
-
|
|
1579
|
-
|
|
1652
|
+
// Empty list (or omitted) lifts this task's restriction; a non-empty list is its
|
|
1653
|
+
// new allow-list. Per task, not per plugin: this state object is shared by every
|
|
1654
|
+
// agent, so one value here used to lock every other task's browser tools.
|
|
1655
|
+
//
|
|
1656
|
+
if (allowed.length === 0) state.restrictedByTask.delete(taskKey(exec))
|
|
1657
|
+
else state.restrictedByTask.set(taskKey(exec), [...allowed])
|
|
1658
|
+
const current = state.restrictedByTask.get(taskKey(exec))
|
|
1659
|
+
return { restrictedTo: current === undefined ? [] : [...current] }
|
|
1580
1660
|
},
|
|
1581
1661
|
}))
|
|
1582
1662
|
|
|
@@ -1601,7 +1681,7 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1601
1681
|
timeoutMs,
|
|
1602
1682
|
isConcurrencySafe: () => false,
|
|
1603
1683
|
async execute(args, exec) {
|
|
1604
|
-
assertAllowed(state, 'browser_auth')
|
|
1684
|
+
assertAllowed(state, 'browser_auth', taskKey(exec))
|
|
1605
1685
|
const browser = ctx.get('browser')
|
|
1606
1686
|
if (browser === undefined) throw new Error('tool-browser: browser service unavailable')
|
|
1607
1687
|
const session = await ensureSession(browser, state, taskKey(exec), agentOf(exec))
|
|
@@ -1609,7 +1689,13 @@ export function apply(ctx: Context, config: Config = {}): void {
|
|
|
1609
1689
|
const cookies = await browser.flushAuth(session)
|
|
1610
1690
|
return { cookies: cookies.map(c => ({ ...c })) as never }
|
|
1611
1691
|
}
|
|
1612
|
-
|
|
1692
|
+
// The description says cookies are required for restore; without this check an
|
|
1693
|
+
// empty call returned {restored: 0} and rendered 'Restored 0 cookies.', which
|
|
1694
|
+
// reads as success and would leave the caller believing the login was restored.
|
|
1695
|
+
if (!Array.isArray(args.cookies) || args.cookies.length === 0) {
|
|
1696
|
+
throw new Error('browser_auth: action "restore" needs a non-empty "cookies" array (get one from action "export" on the same profile)')
|
|
1697
|
+
}
|
|
1698
|
+
const list = args.cookies as unknown[]
|
|
1613
1699
|
const restored = await browser.restoreAuth(session, list as never)
|
|
1614
1700
|
return { restored }
|
|
1615
1701
|
},
|