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.
Files changed (76) hide show
  1. package/CHANGELOG.md +166 -0
  2. package/LICENSE +22 -0
  3. package/NOTICE.md +7 -0
  4. package/README.en.md +100 -0
  5. package/README.md +99 -2
  6. package/assets/dsh-browser-plus-256.png +0 -0
  7. package/assets/dsh-browser-plus-512.png +0 -0
  8. package/assets/dsh-browser-plus-small.svg +9 -0
  9. package/assets/dsh-browser-plus.ico +0 -0
  10. package/assets/dsh-browser-plus.svg +11 -0
  11. package/assets/readme-workspace.png +0 -0
  12. package/cordis.patch.yml +17 -0
  13. package/docs/MIGRATION.md +48 -0
  14. package/docs/README.md +22 -0
  15. package/docs/SOAK-CHECKLIST.md +98 -0
  16. package/docs/architecture.md +88 -0
  17. package/docs/tool-reference.md +124 -0
  18. package/docs/user-guide.md +121 -0
  19. package/docs/why-browser.md +45 -0
  20. package/lib/browser/runtime.d.ts +225 -0
  21. package/lib/browser/runtime.js +302 -0
  22. package/lib/browser/types.d.ts +668 -0
  23. package/lib/browser/types.js +18 -0
  24. package/lib/browser-electron/auth-cookies.d.ts +54 -0
  25. package/lib/browser-electron/auth-cookies.js +83 -0
  26. package/lib/browser-electron/chrome-state.d.ts +187 -0
  27. package/lib/browser-electron/chrome-state.js +12 -0
  28. package/lib/browser-electron/entry.d.ts +66 -0
  29. package/lib/browser-electron/entry.js +62 -0
  30. package/lib/browser-electron/fingerprint.d.ts +29 -0
  31. package/lib/browser-electron/fingerprint.js +42 -0
  32. package/lib/browser-electron/host-main.d.ts +18 -0
  33. package/lib/browser-electron/host-main.js +2494 -0
  34. package/lib/browser-electron/icon.d.ts +11 -0
  35. package/lib/browser-electron/icon.js +23 -0
  36. package/lib/browser-electron/page-chrome.d.ts +21 -0
  37. package/lib/browser-electron/page-chrome.js +2034 -0
  38. package/lib/browser-electron/provider.d.ts +709 -0
  39. package/lib/browser-electron/provider.js +2575 -0
  40. package/lib/browser-electron/remote-host.d.ts +143 -0
  41. package/lib/browser-electron/remote-host.js +952 -0
  42. package/lib/browser-electron/task-summary.d.ts +2 -0
  43. package/lib/browser-electron/task-summary.js +12 -0
  44. package/lib/browser-electron/task-thumbnail.d.ts +11 -0
  45. package/lib/browser-electron/task-thumbnail.js +9 -0
  46. package/lib/browser-electron/write-guard.d.ts +41 -0
  47. package/lib/browser-electron/write-guard.js +123 -0
  48. package/lib/index.d.ts +16 -0
  49. package/lib/index.js +14 -0
  50. package/lib/tool-browser/index.d.ts +31 -0
  51. package/lib/tool-browser/index.js +1931 -0
  52. package/package.json +95 -4
  53. package/screenshots.json +3 -0
  54. package/scripts/build-icons.mjs +80 -0
  55. package/scripts/capture-window.ps1 +79 -0
  56. package/scripts/crop-image.ps1 +20 -0
  57. package/scripts/smoke-browser-tools.mjs +1968 -0
  58. package/scripts/smoke-chrome-world.mjs +63 -0
  59. package/scripts/smoke-electron-host.mjs +50 -0
  60. package/src/browser/runtime.ts +470 -0
  61. package/src/browser/types.ts +649 -0
  62. package/src/browser-electron/auth-cookies.ts +125 -0
  63. package/src/browser-electron/chrome-state.ts +174 -0
  64. package/src/browser-electron/entry.ts +115 -0
  65. package/src/browser-electron/fingerprint.ts +45 -0
  66. package/src/browser-electron/host-main.ts +2330 -0
  67. package/src/browser-electron/icon.ts +26 -0
  68. package/src/browser-electron/page-chrome.ts +2046 -0
  69. package/src/browser-electron/provider.ts +3088 -0
  70. package/src/browser-electron/remote-host.ts +1004 -0
  71. package/src/browser-electron/task-summary.ts +10 -0
  72. package/src/browser-electron/task-thumbnail.ts +17 -0
  73. package/src/browser-electron/write-guard.ts +134 -0
  74. package/src/index.ts +52 -0
  75. package/src/tool-browser/index.ts +1974 -0
  76. 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'