dsh-builtin-browser 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dsh-browser contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.en.md ADDED
@@ -0,0 +1,89 @@
1
+ # dsh-builtin-browser (built-in shared real browser plugin for DeepSeek Harness)
2
+
3
+ 中文 | [English](README.en.md)
4
+
5
+ A shared real browser capability plugin for [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness): a visible browser the human watches and can take over, driven by the agent over CDP.
6
+
7
+ This plugin provides the **host-side capability** (browser seam, Electron CDP provider, `browser_*` model tools). The browser's **native view itself** is supplied by the host (a desktop shell) through `ctx.electronViewHost`; the plugin contains no browser UI.
8
+
9
+ ## Features
10
+
11
+ - **Real view, not a relay.** The browser is a native view (`WebContentsView`) the human can see and operate directly; the agent drives the very same page. The view is provided by the host shell; the plugin drives it.
12
+ - **DOM-referenced, not coordinate-guessing.** `browser_snapshot` returns numbered interactive elements; `browser_execute` runs JS in the page (native setters for framework inputs), so interaction works on React/Vue pages.
13
+ - **Multi-tab sessions.** Open URLs in parallel tabs, list/switch/close/reset, all keeping state.
14
+ - **Multi-format content.** Fetch pages as html / markdown / txt / json, scoped by selector, capped by length and timeout.
15
+
16
+ ## Requirements
17
+
18
+ - DeepSeek Harness (dsh) with the `web` profile
19
+ - A host that provides `ctx.electronViewHost` (a machine holding real Electron `WebContentsView`s, e.g. dsh's desktop shell). Without one, the plugin mounts the seam but the provider and tools stay disabled — plain `dsh web` is unaffected.
20
+
21
+ ## Install
22
+
23
+ ```sh
24
+ dsh plugin --profile web add dsh-builtin-browser # from npm once published
25
+ # or from a checkout (one plugin, one repository):
26
+ dsh plugin --profile web add <path-to-this-repo>
27
+ ```
28
+
29
+ This links the plugin, adds `dsh-builtin-browser` to the profile's bundle layer, and mounts:
30
+
31
+ | Row | Subpath | Role |
32
+ |---|---|---|
33
+ | `browser` | `dsh-builtin-browser/browser` | `ctx.browser` capability seam (always mounted) |
34
+ | `browser-electron` | `dsh-builtin-browser/browser-electron` | Electron CDP provider (needs `electronViewHost`) |
35
+ | `tool-browser` | `dsh-builtin-browser/tool-browser` | `browser_*` model-facing tools |
36
+
37
+ The provider and tools are gated on `ctx.get('electronViewHost')` being present, so a composition without a desktop shell simply keeps the seam and nothing else.
38
+
39
+ ## Configuration
40
+
41
+ The plugin mounts through `cordis.patch.yml`; per-row config:
42
+
43
+ | Row | Key | Type | Default | Description |
44
+ |---|---|---|---|---|
45
+ | `browser-electron` | `viewHost` | object | required | `ElectronBrowserViewHost` supplied by the host shell (typically `!!js ctx.get('electronViewHost')`) |
46
+ | `browser-electron` | `httpOnly` | boolean | `true` | Allow HTTP(S) navigation only; other protocols (e.g. `file:`/`data:`) are rejected (`BROWSER_NAVIGATION_BLOCKED`) |
47
+ | `browser-electron` | `snapshotMaxElements` | number | `60` | Max snapshot elements before truncation |
48
+ | `browser-electron` | `contentMaxChars` | number | `100000` | Default content character cap |
49
+ | `tool-browser` | `timeoutMs` | number | `60000` | Cooperative tool timeout (ms) |
50
+ | `tool-browser` | `tabTools` | boolean | `true` | Register tab-management tools (`browser_list_tabs` etc.) |
51
+
52
+ ## Tools
53
+
54
+ | Tool | Purpose |
55
+ |---|---|
56
+ | `browser_open` | Open a URL (optionally a new tab); returns a snapshot |
57
+ | `browser_snapshot` | Numbered inventory of interactive elements (inputs/buttons/links) |
58
+ | `browser_execute` | Run JS in the page; args arrive as `arguments[0..n]` |
59
+ | `browser_content` | Fetch the page as html / markdown / txt / json (selector, maxChars, timeoutMs) |
60
+ | `browser_screenshot` | PNG capture, optional `fullPage` |
61
+ | `browser_list_tabs` / `browser_switch_tab` / `browser_close_tab` / `browser_reset` | Multi-tab session management |
62
+
63
+ ## How it works
64
+
65
+ ```
66
+ agent (browser_* tools)
67
+ → ctx.browser (seam, dsh-builtin-browser/browser)
68
+ → dsh-builtin-browser/browser-electron (provider)
69
+ → ElectronBrowserViewHost (supplied by the host shell)
70
+ → WebContentsView + webContents.debugger (CDP)
71
+ ```
72
+
73
+ The provider is Electron-agnostic by construction: it operates through the `ElectronBrowserViewHost` seam (create/destroy/show views, `sendCommand`), which a real shell implements with Electron objects. The same seam lets a future relay provider (headless Chromium screenshot stream) serve remote deployments without touching the tools.
74
+
75
+ ## Division of labor with the desktop shell
76
+
77
+ The browser's **visible view**, the **browser column layout**, and the **column-to-view alignment** belong to the host shell (e.g. dsh's `apps/desktop`), not this plugin. This plugin only consumes the shell-provided `electronViewHost` and owns the seam, provider, and tools. Installing the plugin without a matching shell leaves the capability disabled.
78
+
79
+ ## Known limitations
80
+
81
+ - Screenshots are PNG-only (CDP JPEG hangs on Electron 43); JPEG awaits a non-CDP conversion path.
82
+ - `fullPage` capture is flaky under software compositing on some hosts.
83
+ - Session lifecycle is plugin-level (one session shared by the model), not per-agent yet.
84
+ - Private mode (`privateMode`) is not implemented: it needs Electron session partitioning, which is host-layer territory; this plugin does not promise it.
85
+ - This plugin contains no browser-column UI — that is host-shell territory; do not treat "browser column" as a plugin feature.
86
+
87
+ ## License
88
+
89
+ MIT
package/README.md ADDED
@@ -0,0 +1,89 @@
1
+ # dsh-builtin-browser(DeepSeek Harness 内置共享真实浏览器插件)
2
+
3
+ [English](README.en.md) | 中文
4
+
5
+ 面向 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness) 的共享真实浏览器能力插件:一个用户可见、可随时接管的浏览器,由 agent 通过 CDP 驱动。
6
+
7
+ 本插件提供 **host 侧能力**(浏览器 seam、Electron CDP provider、`browser_*` 模型工具)。浏览器本身的**原生视图**由宿主(桌面外壳)通过 `ctx.electronViewHost` 提供;插件不包含浏览器界面 UI。
8
+
9
+ ## 功能
10
+
11
+ - **真实视图,而非转播。** 浏览器是原生视图(`WebContentsView`),用户可直接看到并操作;agent 驱动的是同一个页面。视图由宿主外壳提供,插件负责驱动。
12
+ - **DOM 引用,而非猜坐标。** `browser_snapshot` 返回带编号的交互元素;`browser_execute` 在页面里执行 JS(框架输入用原生 setter),在 React/Vue 页面上也能可靠交互。
13
+ - **多标签会话。** 并行打开 URL、查看/切换/关闭/重置标签,状态保持。
14
+ - **多格式内容。** 以 html / markdown / txt / json 抓取页面,支持 selector 限定、长度与超时上限。
15
+
16
+ ## 环境要求
17
+
18
+ - DeepSeek Harness(dsh)且安装了 `web` profile
19
+ - 提供 `ctx.electronViewHost` 的宿主(持有真实 Electron `WebContentsView` 的主机,如 dsh 的桌面外壳)。没有它时,插件只挂载 seam,provider 与工具保持禁用——纯 `dsh web` 不受影响。
20
+
21
+ ## 安装
22
+
23
+ ```sh
24
+ dsh plugin --profile web add dsh-builtin-browser # 发布到 npm 后
25
+ # 或从源码目录(独立仓库,一插件一仓库):
26
+ dsh plugin --profile web add <本仓库路径>
27
+ ```
28
+
29
+ 这会链接插件、把 `dsh-builtin-browser` 加入 profile 的 bundle 层,并挂载:
30
+
31
+ | 行 | 子路径 | 角色 |
32
+ |---|---|---|
33
+ | `browser` | `dsh-builtin-browser/browser` | `ctx.browser` 能力 seam(始终挂载) |
34
+ | `browser-electron` | `dsh-builtin-browser/browser-electron` | Electron CDP provider(需要 `electronViewHost`) |
35
+ | `tool-browser` | `dsh-builtin-browser/tool-browser` | `browser_*` 模型侧工具 |
36
+
37
+ provider 与工具以 `ctx.get('electronViewHost')` 是否存在为门控,因此没有桌面外壳的组合只会保留 seam,其余不启用。
38
+
39
+ ## 配置
40
+
41
+ 插件通过 `cordis.patch.yml` 挂载,各行的配置:
42
+
43
+ | 行 | 配置项 | 类型 | 默认 | 说明 |
44
+ |---|---|---|---|---|
45
+ | `browser-electron` | `viewHost` | 对象 | 必填 | 宿主提供的 `ElectronBrowserViewHost` 实例(通常 `!!js ctx.get('electronViewHost')`) |
46
+ | `browser-electron` | `httpOnly` | 布尔 | `true` | 仅允许 HTTP(S) 导航;其余协议(如 `file:`/`data:`)拒绝(`BROWSER_NAVIGATION_BLOCKED`) |
47
+ | `browser-electron` | `snapshotMaxElements` | 数字 | `60` | 快照最多收录的交互元素数,超出截断 |
48
+ | `browser-electron` | `contentMaxChars` | 数字 | `100000` | 内容抓取默认字符上限 |
49
+ | `tool-browser` | `timeoutMs` | 数字 | `60000` | 工具协作超时(ms) |
50
+ | `tool-browser` | `tabTools` | 布尔 | `true` | 是否注册标签管理工具(`browser_list_tabs` 等) |
51
+
52
+ ## 工具
53
+
54
+ | 工具 | 用途 |
55
+ |---|---|
56
+ | `browser_open` | 打开 URL(可选新标签);返回快照 |
57
+ | `browser_snapshot` | 交互元素(输入框/按钮/链接)带编号清单 |
58
+ | `browser_execute` | 在页面执行 JS;参数以 `arguments[0..n]` 传入 |
59
+ | `browser_content` | 以 html / markdown / txt / json 抓取页面(selector、maxChars、timeoutMs) |
60
+ | `browser_screenshot` | PNG 截图,可选 `fullPage` |
61
+ | `browser_list_tabs` / `browser_switch_tab` / `browser_close_tab` / `browser_reset` | 多标签会话管理 |
62
+
63
+ ## 工作原理
64
+
65
+ ```
66
+ agent (browser_* 工具)
67
+ → ctx.browser (seam, dsh-builtin-browser/browser)
68
+ → dsh-builtin-browser/browser-electron (provider)
69
+ → ElectronBrowserViewHost (由宿主外壳提供)
70
+ → WebContentsView + webContents.debugger (CDP)
71
+ ```
72
+
73
+ provider 按构造与 Electron 解耦:它通过 `ElectronBrowserViewHost` 接缝操作(创建/销毁/显示视图、`sendCommand`),由真实外壳用 Electron 对象实现。同一接缝也让未来的转播 provider(无头 Chromium 截图流)服务远程部署,而无需改动工具。
74
+
75
+ ## 与桌面外壳的分工
76
+
77
+ 浏览器**可见视图**、**浏览器列布局**、**列与视图的对齐**都属于宿主外壳(如 dsh 的 `apps/desktop`),不在本插件内。本插件只消费外壳提供的 `electronViewHost`,负责 seam、provider 与工具。若你只装插件而没有配套外壳,插件功能保持禁用。
78
+
79
+ ## 已知限制
80
+
81
+ - 截图仅 PNG(CDP JPEG 在 Electron 43 上挂起);JPEG 等待非 CDP 转换路径。
82
+ - 部分主机在软件合成下 `fullPage` 截图不稳定。
83
+ - 会话生命周期为插件级(模型共享一个会话),尚未做到按 agent 隔离。
84
+ - 无痕模式(`privateMode`)未实现:它需要 Electron 的 session 分区能力,属于宿主层,本插件不承诺。
85
+ - 本插件不含浏览器列 UI——那是宿主外壳的配套,别把"浏览器列"当成插件能力。
86
+
87
+ ## 许可证
88
+
89
+ MIT
@@ -0,0 +1,19 @@
1
+ # dsh-builtin-browser bundle patch: mounts the shared-browser capability into a
2
+ # profile composition. The browser seam (ctx.browser) is always mounted; the
3
+ # Electron provider activates only where the host provides `electronViewHost`
4
+ # (the desktop shell or any host that supplies a view host through
5
+ # `ctx.get('electronViewHost')`), and the model-facing tools ride the same
6
+ # gate so plain `dsh web` (no native view) stays unaffected.
7
+ - insert:
8
+ - id: browser
9
+ name: dsh-builtin-browser/browser
10
+
11
+ - id: browser-electron
12
+ name: dsh-builtin-browser/browser-electron
13
+ disabled: !!js ctx.get('electronViewHost') === undefined
14
+ config:
15
+ viewHost: !!js ctx.get('electronViewHost')
16
+
17
+ - id: tool-browser
18
+ name: dsh-builtin-browser/tool-browser
19
+ disabled: !!js ctx.get('electronViewHost') === undefined
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Service Definition for the browser capability seam (`ctx.browser`): the
3
+ * provider registry and provider-selecting execution for browser sessions.
4
+ * Duplicate ids are rejected. At execution time, a configured provider must
5
+ * exist and be usable; without one, exactly one usable provider is required,
6
+ * so selection never depends on registration order.
7
+ * @module dsh-browser/browser
8
+ */
9
+ import { Context, Service } from '@deepseek-ai/cordis';
10
+ import z from '@deepseek-ai/schemastery';
11
+ import type { BrowserClickRequest, BrowserContentRequest, BrowserContentResult, BrowserExecuteRequest, BrowserExecuteResult, BrowserNavigateRequest, BrowserOpenRequest, BrowserProvider, BrowserScreenshotRequest, BrowserScreenshotResult, BrowserSessionId, BrowserSnapshotResult, BrowserTab, BrowserTypeRequest } from './types.ts';
12
+ export { BrowserError, } from './types.ts';
13
+ export type { BrowserClickRequest, BrowserContentFormat, BrowserContentRequest, BrowserContentResult, BrowserExecuteRequest, BrowserExecuteResult, BrowserNavigateRequest, BrowserOpenRequest, BrowserProvider, BrowserScreenshotRequest, BrowserScreenshotResult, BrowserSessionId, BrowserSnapshotElement, BrowserSnapshotResult, BrowserTab, BrowserTypeRequest, } from './types.ts';
14
+ declare module '@deepseek-ai/cordis' {
15
+ interface Context {
16
+ browser: BrowserRuntime;
17
+ }
18
+ }
19
+ /**
20
+ * Config for the browser seam. `browserProvider` pins which provider wins;
21
+ * it is optional (a single registered usable provider auto-selects).
22
+ */
23
+ export interface BrowserRuntimeConfig {
24
+ /** Explicit browser provider id. Omitted = auto-select when exactly one usable. */
25
+ readonly browserProvider?: string;
26
+ }
27
+ /**
28
+ * The browser access service. Registered as `ctx.browser` (one instance per
29
+ * context).
30
+ *
31
+ * Selection semantics (resolved at execution time, never order-dependent):
32
+ * - A configured id that is registered and `available()` 鈫?that provider.
33
+ * - A configured id not registered 鈫?`BROWSER_PROVIDER_CONFIGURED_MISSING`.
34
+ * - A configured id registered but unavailable 鈫? * `BROWSER_PROVIDER_CONFIGURED_UNAVAILABLE`.
35
+ * - No id configured, exactly one registered usable provider 鈫?that provider.
36
+ * - No id configured, multiple usable providers 鈫?`BROWSER_PROVIDER_AMBIGUOUS`.
37
+ * - No id configured, no usable provider 鈫?`BROWSER_PROVIDER_UNAVAILABLE`.
38
+ */
39
+ export declare class BrowserRuntime extends Service {
40
+ /** Provider selection config. */
41
+ static Config: z<BrowserRuntimeConfig>;
42
+ private providers;
43
+ private readonly providerId;
44
+ constructor(ctx: Context, config?: BrowserRuntimeConfig);
45
+ /**
46
+ * Register a browser provider. Throws {@link BrowserError}
47
+ * `BROWSER_DUPLICATE_PROVIDER` if its id is already registered. Returns a
48
+ * disposer; disposed with the calling fiber.
49
+ * @param provider - the provider; its `id` is the registry key.
50
+ * @returns the disposer that unregisters the provider.
51
+ */
52
+ registerBrowserProvider(provider: BrowserProvider): () => void;
53
+ /** Resolve the selected provider or throw the matching {@link BrowserError}. */
54
+ private resolveProvider;
55
+ /** Open a new browser session through the selected provider. */
56
+ open(): Promise<BrowserSessionId>;
57
+ /** Open a URL through the selected provider, optionally in a new tab. */
58
+ openUrl(session: BrowserSessionId, request: BrowserOpenRequest, signal?: AbortSignal): Promise<void>;
59
+ /** List the session's tabs through the selected provider. */
60
+ listTabs(session: BrowserSessionId): Promise<readonly BrowserTab[]>;
61
+ /** Switch to a tab through the selected provider. */
62
+ switchTab(session: BrowserSessionId, tabId: string): Promise<void>;
63
+ /** Close one tab through the selected provider. */
64
+ closeTab(session: BrowserSessionId, tabId: string): Promise<void>;
65
+ /** Close every tab and reset the session through the selected provider. */
66
+ reset(session: BrowserSessionId): Promise<void>;
67
+ /** Navigate the session's page through the selected provider. */
68
+ navigate(session: BrowserSessionId, request: BrowserNavigateRequest, signal?: AbortSignal): Promise<void>;
69
+ /** Execute JS in the session's page context through the selected provider. */
70
+ execute(session: BrowserSessionId, request: BrowserExecuteRequest, signal?: AbortSignal): Promise<BrowserExecuteResult>;
71
+ /** Produce an AI-friendly snapshot of the session's page. */
72
+ snapshot(session: BrowserSessionId, signal?: AbortSignal): Promise<BrowserSnapshotResult>;
73
+ /** Fetch page content in a requested format. */
74
+ content(session: BrowserSessionId, request: BrowserContentRequest, signal?: AbortSignal): Promise<BrowserContentResult>;
75
+ /** Click at viewport coordinates through the selected provider. */
76
+ click(session: BrowserSessionId, request: BrowserClickRequest, signal?: AbortSignal): Promise<void>;
77
+ /** Type into the focused element through the selected provider. */
78
+ type(session: BrowserSessionId, request: BrowserTypeRequest, signal?: AbortSignal): Promise<void>;
79
+ /** Capture the current page through the selected provider. */
80
+ screenshot(session: BrowserSessionId, request?: BrowserScreenshotRequest, signal?: AbortSignal): Promise<BrowserScreenshotResult>;
81
+ /** Close the session through the selected provider. Idempotent. */
82
+ close(session: BrowserSessionId): Promise<void>;
83
+ }
84
+ export default BrowserRuntime;
@@ -0,0 +1,138 @@
1
+ /**
2
+ * Service Definition for the browser capability seam (`ctx.browser`): the
3
+ * provider registry and provider-selecting execution for browser sessions.
4
+ * Duplicate ids are rejected. At execution time, a configured provider must
5
+ * exist and be usable; without one, exactly one usable provider is required,
6
+ * so selection never depends on registration order.
7
+ * @module dsh-browser/browser
8
+ */
9
+ import { Service } from '@deepseek-ai/cordis';
10
+ import z from '@deepseek-ai/schemastery';
11
+ import { BrowserError } from "./types.js";
12
+ export { BrowserError, } from "./types.js";
13
+ /**
14
+ * The browser access service. Registered as `ctx.browser` (one instance per
15
+ * context).
16
+ *
17
+ * Selection semantics (resolved at execution time, never order-dependent):
18
+ * - A configured id that is registered and `available()` 鈫?that provider.
19
+ * - A configured id not registered 鈫?`BROWSER_PROVIDER_CONFIGURED_MISSING`.
20
+ * - A configured id registered but unavailable 鈫? * `BROWSER_PROVIDER_CONFIGURED_UNAVAILABLE`.
21
+ * - No id configured, exactly one registered usable provider 鈫?that provider.
22
+ * - No id configured, multiple usable providers 鈫?`BROWSER_PROVIDER_AMBIGUOUS`.
23
+ * - No id configured, no usable provider 鈫?`BROWSER_PROVIDER_UNAVAILABLE`.
24
+ */
25
+ export class BrowserRuntime extends Service {
26
+ /** Provider selection config. */
27
+ static Config = z.object({
28
+ browserProvider: z.string(),
29
+ });
30
+ providers = new Map();
31
+ providerId;
32
+ constructor(ctx, config = {}) {
33
+ super(ctx, 'browser');
34
+ this.providerId = config.browserProvider;
35
+ }
36
+ /**
37
+ * Register a browser provider. Throws {@link BrowserError}
38
+ * `BROWSER_DUPLICATE_PROVIDER` if its id is already registered. Returns a
39
+ * disposer; disposed with the calling fiber.
40
+ * @param provider - the provider; its `id` is the registry key.
41
+ * @returns the disposer that unregisters the provider.
42
+ */
43
+ registerBrowserProvider(provider) {
44
+ if (this.providers.has(provider.id)) {
45
+ throw new BrowserError(`a browser provider with id "${provider.id}" is already registered`, 'BROWSER_DUPLICATE_PROVIDER');
46
+ }
47
+ // Bind the generator to this instance instead of aliasing `this` to a
48
+ // local (no-this-alias): the effect body mutates the instance registry.
49
+ const dispose = this.ctx.effect(function* () {
50
+ this.providers.set(provider.id, provider);
51
+ yield () => this.providers.delete(provider.id);
52
+ }.bind(this), 'browser.registerProvider()');
53
+ // ctx.effect's disposer returns Promise<void>; our disposer API is
54
+ // synchronous fire-and-forget 鈥?discard the (always-resolved) promise.
55
+ return () => void dispose();
56
+ }
57
+ /** Resolve the selected provider or throw the matching {@link BrowserError}. */
58
+ resolveProvider() {
59
+ const { providerId, providers } = this;
60
+ if (providerId !== undefined) {
61
+ const provider = providers.get(providerId);
62
+ if (!provider) {
63
+ throw new BrowserError(`configured browser provider "${providerId}" is not registered`, 'BROWSER_PROVIDER_CONFIGURED_MISSING');
64
+ }
65
+ if (!provider.available()) {
66
+ throw new BrowserError(`configured browser provider "${providerId}" is registered but unavailable`, 'BROWSER_PROVIDER_CONFIGURED_UNAVAILABLE');
67
+ }
68
+ return provider;
69
+ }
70
+ const usable = [...providers.values()].filter(provider => provider.available());
71
+ const [single] = usable;
72
+ if (single === undefined) {
73
+ throw new BrowserError('no usable browser provider is registered', 'BROWSER_PROVIDER_UNAVAILABLE');
74
+ }
75
+ if (usable.length > 1) {
76
+ const ids = usable.map(provider => provider.id).join(', ');
77
+ throw new BrowserError(`multiple usable browser providers are registered (${ids}); configure one explicitly`, 'BROWSER_PROVIDER_AMBIGUOUS');
78
+ }
79
+ return single;
80
+ }
81
+ /** Open a new browser session through the selected provider. */
82
+ async open() {
83
+ return this.resolveProvider().open();
84
+ }
85
+ /** Open a URL through the selected provider, optionally in a new tab. */
86
+ async openUrl(session, request, signal) {
87
+ return this.resolveProvider().openUrl(session, request, signal);
88
+ }
89
+ /** List the session's tabs through the selected provider. */
90
+ async listTabs(session) {
91
+ return this.resolveProvider().listTabs(session);
92
+ }
93
+ /** Switch to a tab through the selected provider. */
94
+ async switchTab(session, tabId) {
95
+ return this.resolveProvider().switchTab(session, tabId);
96
+ }
97
+ /** Close one tab through the selected provider. */
98
+ async closeTab(session, tabId) {
99
+ return this.resolveProvider().closeTab(session, tabId);
100
+ }
101
+ /** Close every tab and reset the session through the selected provider. */
102
+ async reset(session) {
103
+ return this.resolveProvider().reset(session);
104
+ }
105
+ /** Navigate the session's page through the selected provider. */
106
+ async navigate(session, request, signal) {
107
+ return this.resolveProvider().navigate(session, request, signal);
108
+ }
109
+ /** Execute JS in the session's page context through the selected provider. */
110
+ async execute(session, request, signal) {
111
+ return this.resolveProvider().execute(session, request, signal);
112
+ }
113
+ /** Produce an AI-friendly snapshot of the session's page. */
114
+ async snapshot(session, signal) {
115
+ return this.resolveProvider().snapshot(session, signal);
116
+ }
117
+ /** Fetch page content in a requested format. */
118
+ async content(session, request, signal) {
119
+ return this.resolveProvider().content(session, request, signal);
120
+ }
121
+ /** Click at viewport coordinates through the selected provider. */
122
+ async click(session, request, signal) {
123
+ return this.resolveProvider().click(session, request, signal);
124
+ }
125
+ /** Type into the focused element through the selected provider. */
126
+ async type(session, request, signal) {
127
+ return this.resolveProvider().type(session, request, signal);
128
+ }
129
+ /** Capture the current page through the selected provider. */
130
+ async screenshot(session, request, signal) {
131
+ return this.resolveProvider().screenshot(session, request, signal);
132
+ }
133
+ /** Close the session through the selected provider. Idempotent. */
134
+ async close(session) {
135
+ return this.resolveProvider().close(session);
136
+ }
137
+ }
138
+ export default BrowserRuntime;
@@ -0,0 +1,203 @@
1
+ /**
2
+ * Vocabulary for the browser capability seam (`ctx.browser`). One seam owns
3
+ * provider registration, session lifecycle, cancellation, errors, and product
4
+ * configuration; providers differ only in what backs a session (an Electron
5
+ * `WebContentsView` in the desktop shell, a headless Chromium relay for
6
+ * remote deployments, 鈥?.
7
+ * @module dsh-browser/browser/types
8
+ */
9
+ import { HarnessError } from '@deepseek-ai/dsh-llm';
10
+ /**
11
+ * Stable identity of one browser session, resolved by the Host through a
12
+ * Typert `LookupMap` to the live session object (the same mechanism
13
+ * `Agent 鈫?agentId` uses). The id is opaque to the wire; only the provider
14
+ * and its host binding can interpret it.
15
+ */
16
+ export type BrowserSessionId = string;
17
+ /**
18
+ * What one browser-capable backend is asked to do. Navigation is the minimal
19
+ * operation every provider shares; the seam deliberately keeps the request
20
+ * minimal so a provider swap never changes how the model asks.
21
+ */
22
+ export interface BrowserNavigateRequest {
23
+ /** The URL to open. Admission (HTTP(S) only, no credentials/private targets) is provider-owned. */
24
+ readonly url: string;
25
+ }
26
+ /**
27
+ * Click at viewport coordinates. Coordinates are viewport-relative pixels in
28
+ * the session's own coordinate space 鈥?the same space the human interacts
29
+ * with, so a real view and a relay agree without scaling.
30
+ */
31
+ export interface BrowserClickRequest {
32
+ /** Viewport-relative x in CSS pixels. */
33
+ readonly x: number;
34
+ /** Viewport-relative y in CSS pixels. */
35
+ readonly y: number;
36
+ }
37
+ /** Type text into the focused element. */
38
+ export interface BrowserTypeRequest {
39
+ /** The text to insert. */
40
+ readonly text: string;
41
+ }
42
+ /**
43
+ * A screenshot of the current page state. `dataUrl` is a base64 data URL the
44
+ * tool layer renders directly; the live view itself never crosses the wire
45
+ * (the human watches the real view in the desktop shape).
46
+ */
47
+ export interface BrowserScreenshotResult {
48
+ /** Base64 data URL of the capture (PNG or JPEG per the request). */
49
+ readonly dataUrl: string;
50
+ }
51
+ /** Screenshot capture options. PNG only (CDP JPEG hangs on Electron 43). */
52
+ export interface BrowserScreenshotRequest {
53
+ /** Capture the full scrollable page instead of the viewport. Default false. */
54
+ readonly fullPage?: boolean;
55
+ }
56
+ /**
57
+ * Execute arbitrary JavaScript in the session's page context. This is the
58
+ * primary interaction path 鈥?DOM-referenced, not screen-coordinate-based:
59
+ * focus/input/click go through the page's own JS (native setters for
60
+ * framework-controlled inputs), matching how a human-driven agent operates.
61
+ */
62
+ export interface BrowserExecuteRequest {
63
+ /** The JavaScript expression to evaluate in the page context. */
64
+ readonly script: string;
65
+ /** Optional arguments injected into the script scope as `arguments[0..n]`. */
66
+ readonly args?: readonly unknown[];
67
+ }
68
+ /**
69
+ * Result of an execute. Mirrors CDP `Runtime.evaluate` semantics: either a
70
+ * value (by value, not by reference) or the exception thrown.
71
+ */
72
+ export type BrowserExecuteResult = {
73
+ readonly ok: true;
74
+ readonly value: unknown;
75
+ } | {
76
+ readonly ok: false;
77
+ readonly exception: string;
78
+ };
79
+ /**
80
+ * One interactive element in a snapshot, with a stable reference number the
81
+ * tool layer (and the model) can cite to target the element.
82
+ */
83
+ export interface BrowserSnapshotElement {
84
+ /** 1-based reference number, stable for the lifetime of one snapshot. */
85
+ readonly ref: number;
86
+ /** Element kind: 'input' | 'button' | 'link' | 'textarea' | 'select' | 'checkbox' | 'other'. */
87
+ readonly kind: string;
88
+ /** Text content or placeholder/label, truncated for the snapshot. */
89
+ readonly label: string;
90
+ /** Best-effort CSS selector; may be empty when none is derivable. */
91
+ readonly selector: string;
92
+ /** Viewport-relative center, for coordinate fallbacks. */
93
+ readonly x: number;
94
+ readonly y: number;
95
+ }
96
+ /**
97
+ * AI-friendly page snapshot: a compact, numbered inventory of interactive
98
+ * elements plus page facts. Not raw HTML 鈥?the model dialogues with this.
99
+ */
100
+ export interface BrowserSnapshotResult {
101
+ /** Final page URL. */
102
+ readonly url: string;
103
+ /** Page title, if any. */
104
+ readonly title?: string;
105
+ /** Numbered interactive elements. */
106
+ readonly elements: readonly BrowserSnapshotElement[];
107
+ /** True when the snapshot was truncated (element cap reached). */
108
+ readonly truncated: boolean;
109
+ }
110
+ /**
111
+ * Content fetch formats. `html` is the raw DOM; `markdown` is a structured
112
+ * reading view; `txt` is plain text (smallest); `json` is structured data.
113
+ */
114
+ export type BrowserContentFormat = 'html' | 'markdown' | 'txt' | 'json';
115
+ /** Content fetch request: format, optional selector scoping, and caps. */
116
+ export interface BrowserContentRequest {
117
+ /** Desired format. */
118
+ readonly format: BrowserContentFormat;
119
+ /** Optional CSS selector limiting the fetch to one region (e.g. `#main`). */
120
+ readonly selector?: string;
121
+ /** Maximum characters of the returned content. */
122
+ readonly maxChars?: number;
123
+ /** Evaluation timeout in ms; the fetch aborts after this. Default 30000. */
124
+ readonly timeoutMs?: number;
125
+ }
126
+ /** A fetched page content in the requested format. */
127
+ export interface BrowserContentResult {
128
+ /** The content in the requested format, capped at `maxChars`. */
129
+ readonly content: string;
130
+ /** True when the content was truncated. */
131
+ readonly truncated: boolean;
132
+ }
133
+ /**
134
+ * A tab inside a browser session. Tabs share the session's profile/cookies
135
+ * and are switchable without losing state.
136
+ */
137
+ export interface BrowserTab {
138
+ /** Stable tab id within the session. */
139
+ readonly id: string;
140
+ /** Current URL, or empty for a blank tab. */
141
+ readonly url: string;
142
+ /** Page title, if any. */
143
+ readonly title?: string;
144
+ /** Whether this tab is the active one. */
145
+ readonly active: boolean;
146
+ }
147
+ /** Request to open a URL: into the active tab or a new tab. */
148
+ export interface BrowserOpenRequest {
149
+ /** The URL to open. */
150
+ readonly url: string;
151
+ /** Open in a new tab (parallel). Default false (reuse the active tab). */
152
+ readonly newTab?: boolean;
153
+ }
154
+ /**
155
+ * A browser-capable backend. Registered with `ctx.browser.registerBrowserProvider`.
156
+ * `id` is a stable string, unique across the seam.
157
+ */
158
+ export interface BrowserProvider {
159
+ readonly id: string;
160
+ /** Cheap local usability check; must not make network calls. */
161
+ available(): boolean;
162
+ /**
163
+ * Open a new session. The provider mints the session id and prepares its
164
+ * backing surface (a view, a headless page, 鈥?. Sessions are isolated from
165
+ * each other; a session must not be visible to any other session's caller.
166
+ */
167
+ open(): Promise<BrowserSessionId>;
168
+ /** Open a URL, optionally in a new tab. Honor `signal` for cancellation. */
169
+ openUrl(session: BrowserSessionId, request: BrowserOpenRequest, signal?: AbortSignal): Promise<void>;
170
+ /** List the session's tabs. */
171
+ listTabs(session: BrowserSessionId): Promise<readonly BrowserTab[]>;
172
+ /** Switch to a tab by id. Unknown id 鈫?`BROWSER_TAB_UNKNOWN`. */
173
+ switchTab(session: BrowserSessionId, tabId: string): Promise<void>;
174
+ /** Close one tab. Closing the active tab activates the next. */
175
+ closeTab(session: BrowserSessionId, tabId: string): Promise<void>;
176
+ /** Close every tab and reset the session's state. */
177
+ reset(session: BrowserSessionId): Promise<void>;
178
+ /** Navigate the active tab. Honor `signal` for cancellation. */
179
+ navigate(session: BrowserSessionId, request: BrowserNavigateRequest, signal?: AbortSignal): Promise<void>;
180
+ /** Execute JS in the active tab's page context. */
181
+ execute(session: BrowserSessionId, request: BrowserExecuteRequest, signal?: AbortSignal): Promise<BrowserExecuteResult>;
182
+ /** Produce an AI-friendly snapshot of the active tab. */
183
+ snapshot(session: BrowserSessionId, signal?: AbortSignal): Promise<BrowserSnapshotResult>;
184
+ /** Fetch page content in a requested format. */
185
+ content(session: BrowserSessionId, request: BrowserContentRequest, signal?: AbortSignal): Promise<BrowserContentResult>;
186
+ /** Click at viewport coordinates (fallback path; execute is preferred). Honor `signal` for cancellation. */
187
+ click(session: BrowserSessionId, request: BrowserClickRequest, signal?: AbortSignal): Promise<void>;
188
+ /** Type into the focused element (fallback path). Honor `signal` for cancellation. */
189
+ type(session: BrowserSessionId, request: BrowserTypeRequest, signal?: AbortSignal): Promise<void>;
190
+ /** Capture the current page. Honor `signal` for cancellation. */
191
+ screenshot(session: BrowserSessionId, request?: BrowserScreenshotRequest, signal?: AbortSignal): Promise<BrowserScreenshotResult>;
192
+ /** Close the session and destroy its backing surface. Idempotent. */
193
+ close(session: BrowserSessionId): Promise<void>;
194
+ }
195
+ /**
196
+ * Typed browser error with a machine-routable, open-string `code` and chained
197
+ * `cause`. Consumers must tolerate provider-specific codes. Shared codes cover
198
+ * unavailable, missing, unusable, ambiguous, or duplicate providers, unknown
199
+ * or closed sessions, cancellation, and provider failure; a provider adds its
200
+ * own (e.g. `BROWSER_CDP_ATTACH_FAILED`).
201
+ */
202
+ export declare class BrowserError extends HarnessError {
203
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Vocabulary for the browser capability seam (`ctx.browser`). One seam owns
3
+ * provider registration, session lifecycle, cancellation, errors, and product
4
+ * configuration; providers differ only in what backs a session (an Electron
5
+ * `WebContentsView` in the desktop shell, a headless Chromium relay for
6
+ * remote deployments, 鈥?.
7
+ * @module dsh-browser/browser/types
8
+ */
9
+ import { HarnessError } from '@deepseek-ai/dsh-llm';
10
+ /**
11
+ * Typed browser error with a machine-routable, open-string `code` and chained
12
+ * `cause`. Consumers must tolerate provider-specific codes. Shared codes cover
13
+ * unavailable, missing, unusable, ambiguous, or duplicate providers, unknown
14
+ * or closed sessions, cancellation, and provider failure; a provider adds its
15
+ * own (e.g. `BROWSER_CDP_ATTACH_FAILED`).
16
+ */
17
+ export class BrowserError extends HarnessError {
18
+ }