dsh-browser-plus 0.0.0-stage → 0.5.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/CHANGELOG.md +166 -0
- package/LICENSE +22 -0
- package/NOTICE.md +7 -0
- package/README.en.md +100 -0
- package/README.md +99 -2
- package/assets/dsh-browser-plus-256.png +0 -0
- package/assets/dsh-browser-plus-512.png +0 -0
- package/assets/dsh-browser-plus-small.svg +9 -0
- package/assets/dsh-browser-plus.ico +0 -0
- package/assets/dsh-browser-plus.svg +11 -0
- package/assets/readme-workspace.png +0 -0
- package/cordis.patch.yml +17 -0
- package/docs/MIGRATION.md +48 -0
- package/docs/README.md +22 -0
- package/docs/SOAK-CHECKLIST.md +98 -0
- package/docs/architecture.md +88 -0
- package/docs/tool-reference.md +124 -0
- package/docs/user-guide.md +121 -0
- package/docs/why-browser.md +45 -0
- package/lib/browser/runtime.d.ts +225 -0
- package/lib/browser/runtime.js +302 -0
- package/lib/browser/types.d.ts +668 -0
- package/lib/browser/types.js +18 -0
- package/lib/browser-electron/auth-cookies.d.ts +54 -0
- package/lib/browser-electron/auth-cookies.js +83 -0
- package/lib/browser-electron/chrome-state.d.ts +187 -0
- package/lib/browser-electron/chrome-state.js +12 -0
- package/lib/browser-electron/entry.d.ts +66 -0
- package/lib/browser-electron/entry.js +62 -0
- package/lib/browser-electron/fingerprint.d.ts +29 -0
- package/lib/browser-electron/fingerprint.js +42 -0
- package/lib/browser-electron/host-main.d.ts +18 -0
- package/lib/browser-electron/host-main.js +2494 -0
- package/lib/browser-electron/icon.d.ts +11 -0
- package/lib/browser-electron/icon.js +23 -0
- package/lib/browser-electron/page-chrome.d.ts +21 -0
- package/lib/browser-electron/page-chrome.js +2034 -0
- package/lib/browser-electron/provider.d.ts +709 -0
- package/lib/browser-electron/provider.js +2575 -0
- package/lib/browser-electron/remote-host.d.ts +143 -0
- package/lib/browser-electron/remote-host.js +952 -0
- package/lib/browser-electron/task-summary.d.ts +2 -0
- package/lib/browser-electron/task-summary.js +12 -0
- package/lib/browser-electron/task-thumbnail.d.ts +11 -0
- package/lib/browser-electron/task-thumbnail.js +9 -0
- package/lib/browser-electron/write-guard.d.ts +41 -0
- package/lib/browser-electron/write-guard.js +123 -0
- package/lib/index.d.ts +16 -0
- package/lib/index.js +14 -0
- package/lib/tool-browser/index.d.ts +31 -0
- package/lib/tool-browser/index.js +1931 -0
- package/package.json +95 -4
- package/screenshots.json +3 -0
- package/scripts/build-icons.mjs +80 -0
- package/scripts/capture-window.ps1 +79 -0
- package/scripts/crop-image.ps1 +20 -0
- package/scripts/smoke-browser-tools.mjs +1968 -0
- package/scripts/smoke-chrome-world.mjs +63 -0
- package/scripts/smoke-electron-host.mjs +50 -0
- package/src/browser/runtime.ts +470 -0
- package/src/browser/types.ts +649 -0
- package/src/browser-electron/auth-cookies.ts +125 -0
- package/src/browser-electron/chrome-state.ts +174 -0
- package/src/browser-electron/entry.ts +115 -0
- package/src/browser-electron/fingerprint.ts +45 -0
- package/src/browser-electron/host-main.ts +2330 -0
- package/src/browser-electron/icon.ts +26 -0
- package/src/browser-electron/page-chrome.ts +2046 -0
- package/src/browser-electron/provider.ts +3088 -0
- package/src/browser-electron/remote-host.ts +1004 -0
- package/src/browser-electron/task-summary.ts +10 -0
- package/src/browser-electron/task-thumbnail.ts +17 -0
- package/src/browser-electron/write-guard.ts +134 -0
- package/src/index.ts +52 -0
- package/src/tool-browser/index.ts +1974 -0
- package/src/types/electron-shim.d.ts +143 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Return a page-safe task location without path, query, or fragment data. */
|
|
2
|
+
export function taskSummaryUrl(raw: string): string {
|
|
3
|
+
if (raw === '') return ''
|
|
4
|
+
try {
|
|
5
|
+
const origin = new URL(raw).origin
|
|
6
|
+
return origin === 'null' ? '' : origin
|
|
7
|
+
} catch {
|
|
8
|
+
return ''
|
|
9
|
+
}
|
|
10
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export const TASK_THUMBNAIL_WIDTH = 288
|
|
2
|
+
export const TASK_THUMBNAIL_JPEG_QUALITY = 58
|
|
3
|
+
export const MAX_TASK_THUMBNAIL_BYTES = 180 * 1024
|
|
4
|
+
|
|
5
|
+
export interface ThumbnailImage {
|
|
6
|
+
resize(options: { width: number }): {
|
|
7
|
+
toJPEG(quality: number): Buffer
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function taskThumbnailDataUrl(image: ThumbnailImage): string | undefined {
|
|
12
|
+
const jpeg = image.resize({ width: TASK_THUMBNAIL_WIDTH }).toJPEG(TASK_THUMBNAIL_JPEG_QUALITY)
|
|
13
|
+
|
|
14
|
+
if (jpeg.length === 0 || jpeg.length > MAX_TASK_THUMBNAIL_BYTES) return undefined
|
|
15
|
+
|
|
16
|
+
return `data:image/jpeg;base64,${jpeg.toString('base64')}`
|
|
17
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Write-path admission for browser-produced files (screenshots, downloads).
|
|
3
|
+
*
|
|
4
|
+
* The provider — not the host — owns this guard so one implementation covers
|
|
5
|
+
* the self-hosted Electron host and desktop-shell handles alike, and so it can
|
|
6
|
+
* be exercised without Electron. Roots are resolved through the deepest
|
|
7
|
+
* existing ancestor, which keeps a symlinked or `..`-laden target from
|
|
8
|
+
* escaping a root it only appears to sit inside.
|
|
9
|
+
* @module dsh-browser-plus/browser-electron/write-guard
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { realpathSync } from 'node:fs'
|
|
13
|
+
import { tmpdir } from 'node:os'
|
|
14
|
+
import { basename, dirname, join, resolve, sep } from 'node:path'
|
|
15
|
+
import { BrowserError } from '../browser/types.ts'
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Roots a browser write may target when the config does not name any: the
|
|
19
|
+
* DSH workspace (the process working directory) and the OS temp directory.
|
|
20
|
+
* This mirrors the DSH file sandbox's "write inside the workspace" boundary.
|
|
21
|
+
*/
|
|
22
|
+
export function defaultWriteRoots(): string[] {
|
|
23
|
+
return [process.cwd(), tmpdir()]
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Windows paths compare case-insensitively; POSIX paths do not. */
|
|
27
|
+
function comparable(value: string): string {
|
|
28
|
+
return process.platform === 'win32' ? value.toLowerCase() : value
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Resolve symlinks on the deepest ancestor that exists, then re-append the
|
|
33
|
+
* segments that do not. A save target usually does not exist yet, so the
|
|
34
|
+
* target itself cannot be realpath'd — but its parent usually can.
|
|
35
|
+
*/
|
|
36
|
+
function realpathOfNearestAncestor(target: string): string {
|
|
37
|
+
let current = target
|
|
38
|
+
const tail: string[] = []
|
|
39
|
+
for (;;) {
|
|
40
|
+
try {
|
|
41
|
+
const real = realpathSync.native(current)
|
|
42
|
+
return tail.length === 0 ? real : join(real, ...[...tail].reverse())
|
|
43
|
+
} catch {
|
|
44
|
+
const parent = dirname(current)
|
|
45
|
+
// Reached the filesystem root without finding anything real: fall back
|
|
46
|
+
// to the lexical path rather than denying a legitimate write.
|
|
47
|
+
if (parent === current) return target
|
|
48
|
+
tail.push(basename(current))
|
|
49
|
+
current = parent
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** True when `candidate` is `root` itself or lives beneath it. */
|
|
55
|
+
function within(candidate: string, root: string): boolean {
|
|
56
|
+
if (candidate === root) return true
|
|
57
|
+
return candidate.startsWith(root.endsWith(sep) ? root : root + sep)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Real, comparable forms of both sides; roots are normalized exactly once per call. */
|
|
61
|
+
function resolvedPair(candidate: string, roots: readonly string[]): { target: string; roots: string[] } {
|
|
62
|
+
return {
|
|
63
|
+
target: comparable(realpathOfNearestAncestor(resolve(candidate))),
|
|
64
|
+
roots: roots.map(root => comparable(realpathOfNearestAncestor(resolve(root)))),
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Whether a path would be admitted by {@link resolveWritePath}, without
|
|
70
|
+
* throwing. Exposed for focused tests and for callers that want to probe.
|
|
71
|
+
* @param candidate - the path to test.
|
|
72
|
+
* @param roots - the allowed roots.
|
|
73
|
+
*/
|
|
74
|
+
export function isWithinRoots(candidate: string, roots: readonly string[]): boolean {
|
|
75
|
+
const { target, roots: resolved } = resolvedPair(candidate, roots)
|
|
76
|
+
return resolved.some(root => within(target, root))
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Resolve a write target and admit it only when it lands inside one of the
|
|
81
|
+
* allowed roots. Returns the absolute path to write.
|
|
82
|
+
* @param savePath - the caller-supplied path.
|
|
83
|
+
* @param roots - the allowed roots; an empty list denies every write.
|
|
84
|
+
* @throws BrowserError `BROWSER_WRITE_PATH_DENIED` when the path is unusable or outside every root.
|
|
85
|
+
*/
|
|
86
|
+
export function resolveWritePath(savePath: string, roots: readonly string[]): string {
|
|
87
|
+
if (typeof savePath !== 'string' || savePath.trim() === '') {
|
|
88
|
+
throw new BrowserError('browser: refusing to write without a save path', 'BROWSER_WRITE_PATH_DENIED')
|
|
89
|
+
}
|
|
90
|
+
const absolute = resolve(savePath)
|
|
91
|
+
if (!isWithinRoots(absolute, roots)) {
|
|
92
|
+
// This message reaches the model context and, through a task's error field,
|
|
93
|
+
// the page itself — so it never names an absolute path.
|
|
94
|
+
const hint = roots.length === 0 ? ' (none configured)' : ''
|
|
95
|
+
throw new BrowserError(
|
|
96
|
+
`browser: refusing to write "${savePath}" outside the allowed roots${hint}; `
|
|
97
|
+
+ 'add the directory to the browser-electron "writeRoots" config to allow it',
|
|
98
|
+
'BROWSER_WRITE_PATH_DENIED',
|
|
99
|
+
)
|
|
100
|
+
}
|
|
101
|
+
return absolute
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Resolve a file the browser is about to hand to a page and admit it only when
|
|
106
|
+
* it lands inside one of the allowed roots. Unlike a write target the file must
|
|
107
|
+
* already exist, so the path itself is realpath'd: a symlink to a permitted
|
|
108
|
+
* file is admitted, a symlink that leaves the roots is not.
|
|
109
|
+
* @param filePath - the caller-supplied path.
|
|
110
|
+
* @param roots - the allowed roots; an empty list denies every read.
|
|
111
|
+
* @throws BrowserError `BROWSER_READ_PATH_DENIED` when the path is unusable, missing, or outside every root.
|
|
112
|
+
*/
|
|
113
|
+
export function resolveReadPath(filePath: string, roots: readonly string[]): string {
|
|
114
|
+
if (typeof filePath !== 'string' || filePath.trim() === '') {
|
|
115
|
+
throw new BrowserError('browser: refusing to read without a file path', 'BROWSER_READ_PATH_DENIED')
|
|
116
|
+
}
|
|
117
|
+
const absolute = resolve(filePath)
|
|
118
|
+
let real: string
|
|
119
|
+
try {
|
|
120
|
+
real = realpathSync.native(absolute)
|
|
121
|
+
} catch {
|
|
122
|
+
throw new BrowserError(`browser: refusing to read "${filePath}": the file does not exist`, 'BROWSER_READ_PATH_DENIED')
|
|
123
|
+
}
|
|
124
|
+
const allowed = roots.map(root => comparable(realpathOfNearestAncestor(resolve(root))))
|
|
125
|
+
if (!allowed.some(root => within(comparable(real), root))) {
|
|
126
|
+
const hint = roots.length === 0 ? ' (none configured)' : ''
|
|
127
|
+
throw new BrowserError(
|
|
128
|
+
`browser: refusing to read "${filePath}" outside the allowed roots${hint}; `
|
|
129
|
+
+ 'add the directory to the browser-electron "readRoots" config to allow it',
|
|
130
|
+
'BROWSER_READ_PATH_DENIED',
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
return absolute
|
|
134
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-browser-plus plugin entry: aggregates the shared-browser capability
|
|
3
|
+
* pieces. The cordis.patch.yml rows reference subpath exports:
|
|
4
|
+
* - `dsh-browser-plus/browser` -> the ctx.browser seam (Service)
|
|
5
|
+
* - `dsh-browser-plus/browser-electron` -> the Electron CDP provider
|
|
6
|
+
* - `dsh-browser-plus/tool-browser` -> the model-facing browser_* tools
|
|
7
|
+
* This root entry only re-exports for programmatic use; the loader rows are
|
|
8
|
+
* the composition surface.
|
|
9
|
+
* @module dsh-browser-plus
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
export { BrowserError } from './browser/types.ts'
|
|
13
|
+
export type {
|
|
14
|
+
BrowserChallenge,
|
|
15
|
+
BrowserContentFormat,
|
|
16
|
+
BrowserControlOwner,
|
|
17
|
+
|
|
18
|
+
BrowserContentRequest,
|
|
19
|
+
BrowserContentResult,
|
|
20
|
+
BrowserDragRequest,
|
|
21
|
+
BrowserDragResult,
|
|
22
|
+
BrowserExecuteRequest,
|
|
23
|
+
BrowserPointerResult,
|
|
24
|
+
BrowserPointerTarget,
|
|
25
|
+
BrowserExecuteResult,
|
|
26
|
+
BrowserFillField,
|
|
27
|
+
BrowserFillRequest,
|
|
28
|
+
BrowserFillResult,
|
|
29
|
+
BrowserHandoffState,
|
|
30
|
+
BrowserNavigateRequest,
|
|
31
|
+
BrowserOpenRequest,
|
|
32
|
+
BrowserProvider,
|
|
33
|
+
BrowserRefRequest,
|
|
34
|
+
BrowserScreenshotRequest,
|
|
35
|
+
BrowserScrollIntoViewRequest,
|
|
36
|
+
BrowserScrollRequest,
|
|
37
|
+
BrowserScrollResult,
|
|
38
|
+
BrowserScreenshotResult,
|
|
39
|
+
BrowserSessionId,
|
|
40
|
+
BrowserSnapshotElement,
|
|
41
|
+
BrowserSnapshotResult,
|
|
42
|
+
BrowserTab,
|
|
43
|
+
BrowserTaskInfo,
|
|
44
|
+
BrowserTaskStatus,
|
|
45
|
+
BrowserTaskUpdate,
|
|
46
|
+
BrowserTypeRequest,
|
|
47
|
+
ExportedCookie,
|
|
48
|
+
} from './browser/types.ts'
|
|
49
|
+
export { BrowserRuntime } from './browser/runtime.ts'
|
|
50
|
+
export { ElectronBrowserProvider } from './browser-electron/provider.ts'
|
|
51
|
+
export type { ElectronBrowserViewHost, ElectronViewHandle } from './browser-electron/provider.ts'
|
|
52
|
+
export { RemoteElectronViewHost, defaultHostMainPath } from './browser-electron/remote-host.ts'
|