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,12 @@
|
|
|
1
|
+
/** Return a page-safe task location without path, query, or fragment data. */
|
|
2
|
+
export function taskSummaryUrl(raw) {
|
|
3
|
+
if (raw === '')
|
|
4
|
+
return '';
|
|
5
|
+
try {
|
|
6
|
+
const origin = new URL(raw).origin;
|
|
7
|
+
return origin === 'null' ? '' : origin;
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return '';
|
|
11
|
+
}
|
|
12
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export declare const TASK_THUMBNAIL_WIDTH = 288;
|
|
2
|
+
export declare const TASK_THUMBNAIL_JPEG_QUALITY = 58;
|
|
3
|
+
export declare const MAX_TASK_THUMBNAIL_BYTES: number;
|
|
4
|
+
export interface ThumbnailImage {
|
|
5
|
+
resize(options: {
|
|
6
|
+
width: number;
|
|
7
|
+
}): {
|
|
8
|
+
toJPEG(quality: number): Buffer;
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export declare function taskThumbnailDataUrl(image: ThumbnailImage): string | undefined;
|
|
@@ -0,0 +1,9 @@
|
|
|
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
|
+
export function taskThumbnailDataUrl(image) {
|
|
5
|
+
const jpeg = image.resize({ width: TASK_THUMBNAIL_WIDTH }).toJPEG(TASK_THUMBNAIL_JPEG_QUALITY);
|
|
6
|
+
if (jpeg.length === 0 || jpeg.length > MAX_TASK_THUMBNAIL_BYTES)
|
|
7
|
+
return undefined;
|
|
8
|
+
return `data:image/jpeg;base64,${jpeg.toString('base64')}`;
|
|
9
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
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
|
+
* Roots a browser write may target when the config does not name any: the
|
|
13
|
+
* DSH workspace (the process working directory) and the OS temp directory.
|
|
14
|
+
* This mirrors the DSH file sandbox's "write inside the workspace" boundary.
|
|
15
|
+
*/
|
|
16
|
+
export declare function defaultWriteRoots(): string[];
|
|
17
|
+
/**
|
|
18
|
+
* Whether a path would be admitted by {@link resolveWritePath}, without
|
|
19
|
+
* throwing. Exposed for focused tests and for callers that want to probe.
|
|
20
|
+
* @param candidate - the path to test.
|
|
21
|
+
* @param roots - the allowed roots.
|
|
22
|
+
*/
|
|
23
|
+
export declare function isWithinRoots(candidate: string, roots: readonly string[]): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Resolve a write target and admit it only when it lands inside one of the
|
|
26
|
+
* allowed roots. Returns the absolute path to write.
|
|
27
|
+
* @param savePath - the caller-supplied path.
|
|
28
|
+
* @param roots - the allowed roots; an empty list denies every write.
|
|
29
|
+
* @throws BrowserError `BROWSER_WRITE_PATH_DENIED` when the path is unusable or outside every root.
|
|
30
|
+
*/
|
|
31
|
+
export declare function resolveWritePath(savePath: string, roots: readonly string[]): string;
|
|
32
|
+
/**
|
|
33
|
+
* Resolve a file the browser is about to hand to a page and admit it only when
|
|
34
|
+
* it lands inside one of the allowed roots. Unlike a write target the file must
|
|
35
|
+
* already exist, so the path itself is realpath'd: a symlink to a permitted
|
|
36
|
+
* file is admitted, a symlink that leaves the roots is not.
|
|
37
|
+
* @param filePath - the caller-supplied path.
|
|
38
|
+
* @param roots - the allowed roots; an empty list denies every read.
|
|
39
|
+
* @throws BrowserError `BROWSER_READ_PATH_DENIED` when the path is unusable, missing, or outside every root.
|
|
40
|
+
*/
|
|
41
|
+
export declare function resolveReadPath(filePath: string, roots: readonly string[]): string;
|
|
@@ -0,0 +1,123 @@
|
|
|
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
|
+
import { realpathSync } from 'node:fs';
|
|
12
|
+
import { tmpdir } from 'node:os';
|
|
13
|
+
import { basename, dirname, join, resolve, sep } from 'node:path';
|
|
14
|
+
import { BrowserError } from "../browser/types.js";
|
|
15
|
+
/**
|
|
16
|
+
* Roots a browser write may target when the config does not name any: the
|
|
17
|
+
* DSH workspace (the process working directory) and the OS temp directory.
|
|
18
|
+
* This mirrors the DSH file sandbox's "write inside the workspace" boundary.
|
|
19
|
+
*/
|
|
20
|
+
export function defaultWriteRoots() {
|
|
21
|
+
return [process.cwd(), tmpdir()];
|
|
22
|
+
}
|
|
23
|
+
/** Windows paths compare case-insensitively; POSIX paths do not. */
|
|
24
|
+
function comparable(value) {
|
|
25
|
+
return process.platform === 'win32' ? value.toLowerCase() : value;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Resolve symlinks on the deepest ancestor that exists, then re-append the
|
|
29
|
+
* segments that do not. A save target usually does not exist yet, so the
|
|
30
|
+
* target itself cannot be realpath'd — but its parent usually can.
|
|
31
|
+
*/
|
|
32
|
+
function realpathOfNearestAncestor(target) {
|
|
33
|
+
let current = target;
|
|
34
|
+
const tail = [];
|
|
35
|
+
for (;;) {
|
|
36
|
+
try {
|
|
37
|
+
const real = realpathSync.native(current);
|
|
38
|
+
return tail.length === 0 ? real : join(real, ...[...tail].reverse());
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
const parent = dirname(current);
|
|
42
|
+
// Reached the filesystem root without finding anything real: fall back
|
|
43
|
+
// to the lexical path rather than denying a legitimate write.
|
|
44
|
+
if (parent === current)
|
|
45
|
+
return target;
|
|
46
|
+
tail.push(basename(current));
|
|
47
|
+
current = parent;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** True when `candidate` is `root` itself or lives beneath it. */
|
|
52
|
+
function within(candidate, root) {
|
|
53
|
+
if (candidate === root)
|
|
54
|
+
return true;
|
|
55
|
+
return candidate.startsWith(root.endsWith(sep) ? root : root + sep);
|
|
56
|
+
}
|
|
57
|
+
/** Real, comparable forms of both sides; roots are normalized exactly once per call. */
|
|
58
|
+
function resolvedPair(candidate, roots) {
|
|
59
|
+
return {
|
|
60
|
+
target: comparable(realpathOfNearestAncestor(resolve(candidate))),
|
|
61
|
+
roots: roots.map(root => comparable(realpathOfNearestAncestor(resolve(root)))),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Whether a path would be admitted by {@link resolveWritePath}, without
|
|
66
|
+
* throwing. Exposed for focused tests and for callers that want to probe.
|
|
67
|
+
* @param candidate - the path to test.
|
|
68
|
+
* @param roots - the allowed roots.
|
|
69
|
+
*/
|
|
70
|
+
export function isWithinRoots(candidate, roots) {
|
|
71
|
+
const { target, roots: resolved } = resolvedPair(candidate, roots);
|
|
72
|
+
return resolved.some(root => within(target, root));
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Resolve a write target and admit it only when it lands inside one of the
|
|
76
|
+
* allowed roots. Returns the absolute path to write.
|
|
77
|
+
* @param savePath - the caller-supplied path.
|
|
78
|
+
* @param roots - the allowed roots; an empty list denies every write.
|
|
79
|
+
* @throws BrowserError `BROWSER_WRITE_PATH_DENIED` when the path is unusable or outside every root.
|
|
80
|
+
*/
|
|
81
|
+
export function resolveWritePath(savePath, roots) {
|
|
82
|
+
if (typeof savePath !== 'string' || savePath.trim() === '') {
|
|
83
|
+
throw new BrowserError('browser: refusing to write without a save path', 'BROWSER_WRITE_PATH_DENIED');
|
|
84
|
+
}
|
|
85
|
+
const absolute = resolve(savePath);
|
|
86
|
+
if (!isWithinRoots(absolute, roots)) {
|
|
87
|
+
// This message reaches the model context and, through a task's error field,
|
|
88
|
+
// the page itself — so it never names an absolute path.
|
|
89
|
+
const hint = roots.length === 0 ? ' (none configured)' : '';
|
|
90
|
+
throw new BrowserError(`browser: refusing to write "${savePath}" outside the allowed roots${hint}; `
|
|
91
|
+
+ 'add the directory to the browser-electron "writeRoots" config to allow it', 'BROWSER_WRITE_PATH_DENIED');
|
|
92
|
+
}
|
|
93
|
+
return absolute;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Resolve a file the browser is about to hand to a page and admit it only when
|
|
97
|
+
* it lands inside one of the allowed roots. Unlike a write target the file must
|
|
98
|
+
* already exist, so the path itself is realpath'd: a symlink to a permitted
|
|
99
|
+
* file is admitted, a symlink that leaves the roots is not.
|
|
100
|
+
* @param filePath - the caller-supplied path.
|
|
101
|
+
* @param roots - the allowed roots; an empty list denies every read.
|
|
102
|
+
* @throws BrowserError `BROWSER_READ_PATH_DENIED` when the path is unusable, missing, or outside every root.
|
|
103
|
+
*/
|
|
104
|
+
export function resolveReadPath(filePath, roots) {
|
|
105
|
+
if (typeof filePath !== 'string' || filePath.trim() === '') {
|
|
106
|
+
throw new BrowserError('browser: refusing to read without a file path', 'BROWSER_READ_PATH_DENIED');
|
|
107
|
+
}
|
|
108
|
+
const absolute = resolve(filePath);
|
|
109
|
+
let real;
|
|
110
|
+
try {
|
|
111
|
+
real = realpathSync.native(absolute);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
throw new BrowserError(`browser: refusing to read "${filePath}": the file does not exist`, 'BROWSER_READ_PATH_DENIED');
|
|
115
|
+
}
|
|
116
|
+
const allowed = roots.map(root => comparable(realpathOfNearestAncestor(resolve(root))));
|
|
117
|
+
if (!allowed.some(root => within(comparable(real), root))) {
|
|
118
|
+
const hint = roots.length === 0 ? ' (none configured)' : '';
|
|
119
|
+
throw new BrowserError(`browser: refusing to read "${filePath}" outside the allowed roots${hint}; `
|
|
120
|
+
+ 'add the directory to the browser-electron "readRoots" config to allow it', 'BROWSER_READ_PATH_DENIED');
|
|
121
|
+
}
|
|
122
|
+
return absolute;
|
|
123
|
+
}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
export { BrowserError } from './browser/types.ts';
|
|
12
|
+
export type { BrowserChallenge, BrowserContentFormat, BrowserControlOwner, BrowserContentRequest, BrowserContentResult, BrowserDragRequest, BrowserDragResult, BrowserExecuteRequest, BrowserPointerResult, BrowserPointerTarget, BrowserExecuteResult, BrowserFillField, BrowserFillRequest, BrowserFillResult, BrowserHandoffState, BrowserNavigateRequest, BrowserOpenRequest, BrowserProvider, BrowserRefRequest, BrowserScreenshotRequest, BrowserScrollIntoViewRequest, BrowserScrollRequest, BrowserScrollResult, BrowserScreenshotResult, BrowserSessionId, BrowserSnapshotElement, BrowserSnapshotResult, BrowserTab, BrowserTaskInfo, BrowserTaskStatus, BrowserTaskUpdate, BrowserTypeRequest, ExportedCookie, } from './browser/types.ts';
|
|
13
|
+
export { BrowserRuntime } from './browser/runtime.ts';
|
|
14
|
+
export { ElectronBrowserProvider } from './browser-electron/provider.ts';
|
|
15
|
+
export type { ElectronBrowserViewHost, ElectronViewHandle } from './browser-electron/provider.ts';
|
|
16
|
+
export { RemoteElectronViewHost, defaultHostMainPath } from './browser-electron/remote-host.ts';
|
package/lib/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
export { BrowserError } from "./browser/types.js";
|
|
12
|
+
export { BrowserRuntime } from "./browser/runtime.js";
|
|
13
|
+
export { ElectronBrowserProvider } from "./browser-electron/provider.js";
|
|
14
|
+
export { RemoteElectronViewHost, defaultHostMainPath } from "./browser-electron/remote-host.js";
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model-facing browser tools over `ctx.browser`: `browser_open`,
|
|
3
|
+
* `browser_snapshot`, `browser_execute`, `browser_content`,
|
|
4
|
+
* `browser_screenshot`, and tab management (`browser_list_tabs`,
|
|
5
|
+
* `browser_switch_tab`, `browser_close_tab`, `browser_reset`).
|
|
6
|
+
*
|
|
7
|
+
* The tool layer owns only the model-facing schema, argument validation, and
|
|
8
|
+
* result formatting — never provider selection or page driving, which belong
|
|
9
|
+
* to the seam. Session lifecycle is owned here at the plugin level: each
|
|
10
|
+
* calling task (a DSH session) gets its own browser session — the first
|
|
11
|
+
* `browser_open` (or any tool when no session exists) opens it, and later
|
|
12
|
+
* tools in the same task reuse it. Concurrent tasks therefore never fight
|
|
13
|
+
* over tabs, history, or navigation state.
|
|
14
|
+
* @module dsh-browser-plus/tool-browser
|
|
15
|
+
*/
|
|
16
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
17
|
+
/** Plugin name used by loader diagnostics. */
|
|
18
|
+
export declare const name = "tool-browser";
|
|
19
|
+
/** The tool registry, browser seam, and system-prompt registry this tool layer consumes. */
|
|
20
|
+
export declare const inject: string[];
|
|
21
|
+
/** Plugin config: tool timeouts and session defaults. */
|
|
22
|
+
export interface Config {
|
|
23
|
+
/** Cooperative tool-call budget in ms. Default 60000. */
|
|
24
|
+
readonly timeoutMs?: number;
|
|
25
|
+
/** Whether to offer tab-management tools. Default true. */
|
|
26
|
+
readonly tabTools?: boolean;
|
|
27
|
+
/** Optional initial allow-list of browser tool names; other tools are refused. */
|
|
28
|
+
readonly allowedActions?: readonly string[];
|
|
29
|
+
}
|
|
30
|
+
/** Register all browser tools with `ctx.tools`. */
|
|
31
|
+
export declare function apply(ctx: Context, config?: Config): void;
|