@crewhaus/computer-use-driver 0.1.4 → 0.1.6

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.
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Section 30 — host backend for `computer-use-driver`. Drives the
3
+ * dev's actual desktop via the OS-level computer-use surface (macOS
4
+ * Quartz / Linux X11+xdotool / Windows SendInput). Production
5
+ * deployments use this for cross-app workflows that no browser can
6
+ * reach (Maps, Notes, Photos, Finder, native System Settings).
7
+ *
8
+ * **Safety gate:** the constructor refuses to return a working driver
9
+ * unless `CREWHAUS_BROW_HOST_ENABLED=1` is set in the environment. The
10
+ * goal is fail-loud ergonomics: deployment errors that try to use the
11
+ * host backend without explicit opt-in should immediately surface
12
+ * rather than silently driving the dev's machine.
13
+ *
14
+ * The OS-level command dispatch is delegated to a caller-supplied
15
+ * `HostExecutor` so tests + the smoke harness can stub the surface.
16
+ * In production, codegen wires a thin wrapper that shells out to
17
+ * `xdotool` (Linux), `cliclick` (macOS), or PowerShell (Windows).
18
+ */
19
+ import { type Driver, type Viewport } from "../index";
20
+ export type HostExecutor = {
21
+ /** Click at absolute screen coordinates. Button: "left" | "right" | "middle". */
22
+ click(x: number, y: number, button: string): Promise<void>;
23
+ /** Type a string at the current focus. */
24
+ type(text: string): Promise<void>;
25
+ /** Press a single key (or modifier+key, e.g. "cmd+space"). */
26
+ key(combo: string): Promise<void>;
27
+ /** Scroll at absolute screen coordinates. */
28
+ scroll(x: number, y: number, deltaX: number, deltaY: number): Promise<void>;
29
+ /** Capture a PNG screenshot of the entire screen, returning base64. */
30
+ screenshot(): Promise<{
31
+ pngBase64: string;
32
+ viewport: Viewport;
33
+ }>;
34
+ /** Disconnect / clean up any handles. */
35
+ close?(): Promise<void>;
36
+ };
37
+ export type HostBackendOptions = {
38
+ /** Required to pass; otherwise the constructor throws. */
39
+ readonly enabled: boolean;
40
+ /** Test injection (or production OS shim). */
41
+ readonly executor: HostExecutor;
42
+ };
43
+ export declare function createHostDriver(opts: HostBackendOptions): Driver;
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Section 30 — host backend for `computer-use-driver`. Drives the
3
+ * dev's actual desktop via the OS-level computer-use surface (macOS
4
+ * Quartz / Linux X11+xdotool / Windows SendInput). Production
5
+ * deployments use this for cross-app workflows that no browser can
6
+ * reach (Maps, Notes, Photos, Finder, native System Settings).
7
+ *
8
+ * **Safety gate:** the constructor refuses to return a working driver
9
+ * unless `CREWHAUS_BROW_HOST_ENABLED=1` is set in the environment. The
10
+ * goal is fail-loud ergonomics: deployment errors that try to use the
11
+ * host backend without explicit opt-in should immediately surface
12
+ * rather than silently driving the dev's machine.
13
+ *
14
+ * The OS-level command dispatch is delegated to a caller-supplied
15
+ * `HostExecutor` so tests + the smoke harness can stub the surface.
16
+ * In production, codegen wires a thin wrapper that shells out to
17
+ * `xdotool` (Linux), `cliclick` (macOS), or PowerShell (Windows).
18
+ */
19
+ import { ComputerUseDriverError } from "../index";
20
+ export function createHostDriver(opts) {
21
+ if (!opts.enabled) {
22
+ throw new ComputerUseDriverError("host backend disabled. Set CREWHAUS_BROW_HOST_ENABLED=1 to opt in (this drives the actual desktop — never enable in smoke tests).");
23
+ }
24
+ let connected = false;
25
+ return {
26
+ backend: "host",
27
+ async connect() {
28
+ connected = true;
29
+ },
30
+ async goto(_url) {
31
+ // The host backend doesn't navigate URLs — callers should open
32
+ // a browser app first via the OS shell. We surface this as a
33
+ // soft-fail since some workflows want goto to be a no-op.
34
+ throw new ComputerUseDriverError("host backend cannot navigate URLs directly — open a browser via OS shell first");
35
+ },
36
+ async screenshot() {
37
+ if (!connected)
38
+ throw new ComputerUseDriverError("host driver not connected");
39
+ const { pngBase64 } = await opts.executor.screenshot();
40
+ return Buffer.from(pngBase64, "base64");
41
+ },
42
+ async click(x, y, button = "left") {
43
+ if (!connected)
44
+ throw new ComputerUseDriverError("host driver not connected");
45
+ await opts.executor.click(x, y, button);
46
+ },
47
+ async type(text) {
48
+ if (!connected)
49
+ throw new ComputerUseDriverError("host driver not connected");
50
+ await opts.executor.type(text);
51
+ },
52
+ async key(combo) {
53
+ if (!connected)
54
+ throw new ComputerUseDriverError("host driver not connected");
55
+ await opts.executor.key(combo);
56
+ },
57
+ async scroll(dx, dy) {
58
+ if (!connected)
59
+ throw new ComputerUseDriverError("host driver not connected");
60
+ // Driver.scroll has no cursor coords; pass (0, 0) for the executor's
61
+ // anchor and let the host shim scroll at the focused window's origin.
62
+ await opts.executor.scroll(0, 0, dx, dy);
63
+ },
64
+ async getViewport() {
65
+ if (!connected)
66
+ throw new ComputerUseDriverError("host driver not connected");
67
+ const shot = await opts.executor.screenshot();
68
+ return shot.viewport;
69
+ },
70
+ async disconnect() {
71
+ connected = false;
72
+ await opts.executor.close?.();
73
+ },
74
+ };
75
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Section 30 — remote backend for `computer-use-driver`. Connects to a
3
+ * remote chromium instance over Chrome DevTools Protocol (browserless.io,
4
+ * BrowserBase, self-hosted DevTools). Same `Driver` surface as the
5
+ * Playwright-backed `chromium` backend; the difference is the connection
6
+ * layer.
7
+ *
8
+ * Production deployments install `puppeteer-core` and pass it as
9
+ * `_puppeteer`. Without it, the constructor returns a driver whose
10
+ * `connect()` throws a clear diagnostic — matching the SQS / Vapi
11
+ * pattern of "abstraction always builds; deployment-time SDK presence
12
+ * gates the operation".
13
+ */
14
+ import { type Driver, type Viewport } from "../index";
15
+ export type RemoteBackendOptions = {
16
+ /** Browserless / BrowserBase WebSocket endpoint. */
17
+ readonly url: string;
18
+ /** Optional viewport override; defaults to 1280×800. */
19
+ readonly viewport?: Viewport;
20
+ /**
21
+ * Test / production injection: `puppeteer-core` `import` namespace.
22
+ * When undefined, the driver throws at `connect()` with a hint to
23
+ * install puppeteer-core.
24
+ */
25
+ readonly _puppeteer?: PuppeteerCoreLike;
26
+ };
27
+ export type PuppeteerCoreLike = {
28
+ connect(opts: {
29
+ browserWSEndpoint: string;
30
+ }): Promise<{
31
+ newPage(): Promise<{
32
+ goto(url: string): Promise<void>;
33
+ screenshot(opts?: {
34
+ encoding?: "base64";
35
+ }): Promise<string | Buffer>;
36
+ mouse: {
37
+ click(x: number, y: number, opts?: {
38
+ button?: string;
39
+ }): Promise<void>;
40
+ };
41
+ keyboard: {
42
+ type(text: string): Promise<void>;
43
+ press(key: string): Promise<void>;
44
+ };
45
+ evaluate<T>(fn: () => T): Promise<T>;
46
+ close(): Promise<void>;
47
+ }>;
48
+ close(): Promise<void>;
49
+ }>;
50
+ };
51
+ export declare function createRemoteDriver(opts: RemoteBackendOptions): Driver;
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Section 30 — remote backend for `computer-use-driver`. Connects to a
3
+ * remote chromium instance over Chrome DevTools Protocol (browserless.io,
4
+ * BrowserBase, self-hosted DevTools). Same `Driver` surface as the
5
+ * Playwright-backed `chromium` backend; the difference is the connection
6
+ * layer.
7
+ *
8
+ * Production deployments install `puppeteer-core` and pass it as
9
+ * `_puppeteer`. Without it, the constructor returns a driver whose
10
+ * `connect()` throws a clear diagnostic — matching the SQS / Vapi
11
+ * pattern of "abstraction always builds; deployment-time SDK presence
12
+ * gates the operation".
13
+ */
14
+ import { ComputerUseDriverError } from "../index";
15
+ export function createRemoteDriver(opts) {
16
+ if (!opts.url)
17
+ throw new ComputerUseDriverError("remote backend requires url");
18
+ let browser;
19
+ let page;
20
+ let connected = false;
21
+ function ensure() {
22
+ if (!page)
23
+ throw new ComputerUseDriverError("remote driver not connected");
24
+ return page;
25
+ }
26
+ return {
27
+ backend: "remote",
28
+ async connect() {
29
+ const pup = opts._puppeteer;
30
+ if (!pup) {
31
+ throw new ComputerUseDriverError("remote backend requires `puppeteer-core` to be installed and passed as `_puppeteer`. e.g. `import * as pc from 'puppeteer-core'; createDriver({ backend: 'remote', url, _puppeteer: pc })`.");
32
+ }
33
+ browser = await pup.connect({ browserWSEndpoint: opts.url });
34
+ page = await browser.newPage();
35
+ connected = true;
36
+ },
37
+ async goto(url) {
38
+ await ensure().goto(url);
39
+ },
40
+ async screenshot() {
41
+ const buffer = await ensure().screenshot({ encoding: "base64" });
42
+ const pngBase64 = typeof buffer === "string" ? buffer : buffer.toString("base64");
43
+ return Buffer.from(pngBase64, "base64");
44
+ },
45
+ async click(x, y, button = "left") {
46
+ await ensure().mouse.click(x, y, { button });
47
+ },
48
+ async type(text) {
49
+ await ensure().keyboard.type(text);
50
+ },
51
+ async key(combo) {
52
+ await ensure().keyboard.press(combo);
53
+ },
54
+ async scroll(dx, dy) {
55
+ // Puppeteer doesn't expose mouse.wheel directly; dispatch from the
56
+ // page context where `window` is defined. The eval-with-args shape
57
+ // isn't on the local PuppeteerCoreLike type; cast to puppeteer's
58
+ // actual surface to call it.
59
+ const pageWithArgs = ensure();
60
+ await pageWithArgs.evaluate((d) => {
61
+ globalThis.scrollBy(d.dx, d.dy);
62
+ }, { dx, dy });
63
+ },
64
+ async getViewport() {
65
+ const v = opts.viewport;
66
+ if (v !== undefined)
67
+ return v;
68
+ return { width: 1280, height: 800, devicePixelRatio: 1 };
69
+ },
70
+ async disconnect() {
71
+ try {
72
+ if (page)
73
+ await page.close();
74
+ if (browser)
75
+ await browser.close();
76
+ }
77
+ catch {
78
+ /* best-effort */
79
+ }
80
+ connected = false;
81
+ page = undefined;
82
+ browser = undefined;
83
+ },
84
+ };
85
+ }
@@ -0,0 +1,5 @@
1
+ import { CrewhausError } from "@crewhaus/errors";
2
+ export declare class ComputerUseDriverError extends CrewhausError {
3
+ readonly name = "ComputerUseDriverError";
4
+ constructor(message: string, cause?: unknown);
5
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,7 @@
1
+ import { CrewhausError } from "@crewhaus/errors";
2
+ export class ComputerUseDriverError extends CrewhausError {
3
+ name = "ComputerUseDriverError";
4
+ constructor(message, cause) {
5
+ super("tool", message, cause);
6
+ }
7
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Catalog R18 `computer-use-driver` — Section 25 BROW.
3
+ *
4
+ * Cross-platform driver for mouse/keyboard/screenshot operations. The
5
+ * `Driver` interface exposes:
6
+ * - `screenshot()` → PNG `Uint8Array`
7
+ * - `click(x, y, button?)`
8
+ * - `type(text)`
9
+ * - `key(combo)` // "Enter", "Tab", "Control+a"
10
+ * - `scroll(dx, dy)`
11
+ * - `getViewport()` → { width, height, devicePixelRatio }
12
+ * - `goto(url)` // browser-only; `host` backend rejects
13
+ *
14
+ * v0 ships ONE concrete backend — `chromium` — driven by Playwright's
15
+ * bundled headless chromium. The kickoff names this `docker-chromium`
16
+ * because it's intended to run inside a Section-18 sandbox; in v0 we
17
+ * lean on Playwright's own chromium-sandboxing (the chromium binary
18
+ * runs as a sandboxed renderer process) and defer the explicit Docker
19
+ * wrapping to a follow-up.
20
+ *
21
+ * The `host` backend (drives the developer's actual desktop) and
22
+ * `remote` backend (CDP-style remote control) are interface stubs that
23
+ * throw at `connect()` — see the kickoff explicitly forbidding the host
24
+ * backend in the smoke ("never use `host` backend for the smoke since
25
+ * it would drive the dev's actual desktop").
26
+ */
27
+ import { ComputerUseDriverError } from "./errors";
28
+ export { ComputerUseDriverError };
29
+ export { type DnsLookupFn, type SsrfPinningProxy, type StartSsrfPinningProxyOptions, startSsrfPinningProxy, } from "./ssrf-proxy";
30
+ export type DriverBackend = "host" | "chromium" | "remote";
31
+ export type MouseButton = "left" | "right" | "middle";
32
+ export type Viewport = {
33
+ readonly width: number;
34
+ readonly height: number;
35
+ readonly devicePixelRatio: number;
36
+ };
37
+ export interface Driver {
38
+ readonly backend: DriverBackend;
39
+ /** Open / connect / launch chromium. Idempotent. */
40
+ connect(): Promise<void>;
41
+ /** Navigate the chromium tab. Throws on `host` backend. */
42
+ goto(url: string): Promise<void>;
43
+ /** Capture a PNG screenshot. */
44
+ screenshot(): Promise<Uint8Array>;
45
+ click(x: number, y: number, button?: MouseButton): Promise<void>;
46
+ type(text: string): Promise<void>;
47
+ key(combo: string): Promise<void>;
48
+ scroll(dx: number, dy: number): Promise<void>;
49
+ getViewport(): Promise<Viewport>;
50
+ /** Best-effort DOM snapshot. Used by tests + the post-action assertions. */
51
+ domText?(): Promise<string>;
52
+ disconnect(): Promise<void>;
53
+ }
54
+ export type CreateDriverOptions = {
55
+ readonly backend: DriverBackend;
56
+ /** Playwright launch overrides (e.g. headless: false). chromium-only. */
57
+ readonly playwrightOptions?: Record<string, unknown>;
58
+ /**
59
+ * SECURITY (audit follow-up R1) — route ALL chromium traffic through a
60
+ * local DNS-pinning forward proxy (see `ssrf-proxy.ts`). The proxy resolves
61
+ * each hostname once, validates the IP against the private/loopback/
62
+ * link-local/metadata floor, and dials that exact pinned IP, closing the
63
+ * DNS-rebinding TOCTOU the pre-goto guard in tool-navigate cannot close
64
+ * (the browser re-resolves at connect time, and sub-resource fetches never
65
+ * pass the guard at all). Default true. Set `false` ONLY when the browser
66
+ * must reach private/internal targets (e.g. testing an intranet app) AND
67
+ * the page content is trusted. Ignored when `playwrightOptions.proxy` is
68
+ * set — an operator-supplied proxy wins, and is then responsible for its
69
+ * own egress policy. chromium-only.
70
+ */
71
+ readonly ssrfProxy?: boolean;
72
+ /** Initial viewport — chromium-only. Default 1280x720@1. */
73
+ readonly viewport?: {
74
+ width: number;
75
+ height: number;
76
+ };
77
+ /** Test injection: a pre-built Driver instance the factory returns verbatim. */
78
+ readonly _injected?: Driver;
79
+ /**
80
+ * Test injection: replaces the chromium backend's dynamic
81
+ * `import("playwright")`. Lets the import-failure path be exercised without
82
+ * `mock.module` — a throwing module mock cannot be registered once
83
+ * playwright has been imported anywhere in the test process. chromium-only.
84
+ */
85
+ readonly _importPlaywright?: () => Promise<unknown>;
86
+ };
87
+ export declare function createDriver(opts: CreateDriverOptions): Driver;
88
+ export { createHostDriver, type HostBackendOptions, type HostExecutor } from "./backends/host";
89
+ export { createRemoteDriver, type PuppeteerCoreLike, type RemoteBackendOptions, } from "./backends/remote";
package/dist/index.js ADDED
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Catalog R18 `computer-use-driver` — Section 25 BROW.
3
+ *
4
+ * Cross-platform driver for mouse/keyboard/screenshot operations. The
5
+ * `Driver` interface exposes:
6
+ * - `screenshot()` → PNG `Uint8Array`
7
+ * - `click(x, y, button?)`
8
+ * - `type(text)`
9
+ * - `key(combo)` // "Enter", "Tab", "Control+a"
10
+ * - `scroll(dx, dy)`
11
+ * - `getViewport()` → { width, height, devicePixelRatio }
12
+ * - `goto(url)` // browser-only; `host` backend rejects
13
+ *
14
+ * v0 ships ONE concrete backend — `chromium` — driven by Playwright's
15
+ * bundled headless chromium. The kickoff names this `docker-chromium`
16
+ * because it's intended to run inside a Section-18 sandbox; in v0 we
17
+ * lean on Playwright's own chromium-sandboxing (the chromium binary
18
+ * runs as a sandboxed renderer process) and defer the explicit Docker
19
+ * wrapping to a follow-up.
20
+ *
21
+ * The `host` backend (drives the developer's actual desktop) and
22
+ * `remote` backend (CDP-style remote control) are interface stubs that
23
+ * throw at `connect()` — see the kickoff explicitly forbidding the host
24
+ * backend in the smoke ("never use `host` backend for the smoke since
25
+ * it would drive the dev's actual desktop").
26
+ */
27
+ import { ComputerUseDriverError } from "./errors";
28
+ import { startSsrfPinningProxy } from "./ssrf-proxy";
29
+ export { ComputerUseDriverError };
30
+ export { startSsrfPinningProxy, } from "./ssrf-proxy";
31
+ export function createDriver(opts) {
32
+ if (opts._injected !== undefined)
33
+ return opts._injected;
34
+ if (opts.backend === "chromium")
35
+ return createChromiumDriver(opts);
36
+ if (opts.backend === "host") {
37
+ // Section 30 — host backend stays gated. Callers that have wired
38
+ // a real executor use `createHostDriver` directly.
39
+ return createHostDriverStub();
40
+ }
41
+ if (opts.backend === "remote") {
42
+ // Same pattern: callers with a puppeteer-core import use
43
+ // `createRemoteDriver` directly.
44
+ return createRemoteDriverStub();
45
+ }
46
+ throw new ComputerUseDriverError(`unknown backend: ${opts.backend}`);
47
+ }
48
+ // Section 30 — direct exports for callers that wire backends with an
49
+ // executor or puppeteer-core import.
50
+ export { createHostDriver } from "./backends/host";
51
+ export { createRemoteDriver, } from "./backends/remote";
52
+ // ---------------------------------------------------------------------------
53
+ // Playwright-backed chromium driver.
54
+ // ---------------------------------------------------------------------------
55
+ function createChromiumDriver(opts) {
56
+ let browser;
57
+ let context;
58
+ let page;
59
+ let connected = false;
60
+ let ssrfProxy;
61
+ async function loadPlaywright() {
62
+ try {
63
+ // Lazy import so non-browser users don't need Playwright in their tree.
64
+ // The import is dynamic via a string indirection so `tsc` doesn't fail
65
+ // when playwright (an optional peer dep) isn't installed in CI.
66
+ const playwrightModule = "playwright";
67
+ const mod = opts._importPlaywright !== undefined
68
+ ? await opts._importPlaywright()
69
+ : await import(playwrightModule);
70
+ return mod;
71
+ }
72
+ catch (err) {
73
+ throw new ComputerUseDriverError("Playwright not installed. Run `bun add -D playwright` and `bunx playwright install chromium` to use the chromium backend.", err);
74
+ }
75
+ }
76
+ return {
77
+ backend: "chromium",
78
+ async connect() {
79
+ if (connected)
80
+ return;
81
+ const { chromium } = await loadPlaywright();
82
+ const userOpts = opts.playwrightOptions ?? {};
83
+ // SECURITY — DNS-pinning proxy on by default (see CreateDriverOptions.
84
+ // ssrfProxy). An operator-supplied playwrightOptions.proxy wins; the
85
+ // proxy starts only after the playwright import succeeds so the
86
+ // import-failure path can't leak a listening server.
87
+ const useSsrfProxy = opts.ssrfProxy !== false && userOpts["proxy"] === undefined;
88
+ if (useSsrfProxy) {
89
+ ssrfProxy = await startSsrfPinningProxy();
90
+ }
91
+ try {
92
+ // Playwright defaults: chromium runs in its own sandbox (its own
93
+ // sandboxed renderer process). Headless by default. The
94
+ // `<-loopback>` bypass rule removes Chromium's implicit proxy bypass
95
+ // for localhost targets, so loopback requests also route through the
96
+ // pinning proxy — and get blocked there.
97
+ const launchOpts = {
98
+ headless: true,
99
+ ...userOpts,
100
+ ...(ssrfProxy !== undefined
101
+ ? { proxy: { server: ssrfProxy.url, bypass: "<-loopback>" } }
102
+ : {}),
103
+ };
104
+ browser = await chromium.launch(launchOpts);
105
+ const ctxOpts = {
106
+ viewport: opts.viewport ?? { width: 1280, height: 720 },
107
+ };
108
+ context = await browser.newContext(ctxOpts);
109
+ page = await context.newPage();
110
+ }
111
+ catch (err) {
112
+ // Launch/context failure must not leak the proxy server (or a
113
+ // half-launched browser).
114
+ try {
115
+ await browser?.close?.();
116
+ }
117
+ catch {
118
+ // best-effort
119
+ }
120
+ browser = undefined;
121
+ await ssrfProxy?.close();
122
+ ssrfProxy = undefined;
123
+ throw err;
124
+ }
125
+ connected = true;
126
+ },
127
+ async goto(url) {
128
+ if (!connected)
129
+ throw new ComputerUseDriverError("driver not connected — call connect() first");
130
+ await page.goto(url, {
131
+ waitUntil: "domcontentloaded",
132
+ });
133
+ },
134
+ async screenshot() {
135
+ if (!connected)
136
+ throw new ComputerUseDriverError("driver not connected");
137
+ const buf = (await page.screenshot({
138
+ type: "png",
139
+ }));
140
+ return buf;
141
+ },
142
+ async click(x, y, button = "left") {
143
+ if (!connected)
144
+ throw new ComputerUseDriverError("driver not connected");
145
+ await page.mouse.click(x, y, { button });
146
+ },
147
+ async type(text) {
148
+ if (!connected)
149
+ throw new ComputerUseDriverError("driver not connected");
150
+ await page.keyboard.type(text);
151
+ },
152
+ async key(combo) {
153
+ if (!connected)
154
+ throw new ComputerUseDriverError("driver not connected");
155
+ await page.keyboard.press(combo);
156
+ },
157
+ async scroll(dx, dy) {
158
+ if (!connected)
159
+ throw new ComputerUseDriverError("driver not connected");
160
+ await page.mouse.wheel(dx, dy);
161
+ },
162
+ async getViewport() {
163
+ if (!connected)
164
+ throw new ComputerUseDriverError("driver not connected");
165
+ const vp = page.viewportSize();
166
+ const w = vp?.width ?? opts.viewport?.width ?? 1280;
167
+ const h = vp?.height ?? opts.viewport?.height ?? 720;
168
+ return { width: w, height: h, devicePixelRatio: 1 };
169
+ },
170
+ async domText() {
171
+ if (!connected)
172
+ throw new ComputerUseDriverError("driver not connected");
173
+ return page
174
+ .textContent("body")
175
+ .then((t) => t ?? "");
176
+ },
177
+ async disconnect() {
178
+ if (!connected)
179
+ return;
180
+ try {
181
+ await browser.close();
182
+ }
183
+ catch {
184
+ // best-effort
185
+ }
186
+ await ssrfProxy?.close();
187
+ ssrfProxy = undefined;
188
+ connected = false;
189
+ browser = undefined;
190
+ context = undefined;
191
+ page = undefined;
192
+ },
193
+ };
194
+ }
195
+ // ---------------------------------------------------------------------------
196
+ // Stubs for host + remote backends.
197
+ // ---------------------------------------------------------------------------
198
+ function createHostDriverStub() {
199
+ return {
200
+ backend: "host",
201
+ async connect() {
202
+ throw new ComputerUseDriverError("host backend is not implemented in v0. The kickoff explicitly forbids it for smokes (would drive the dev's actual desktop). Native macOS / Linux / Windows backends land in a follow-up gated on `CREWHAUS_BROW_HOST_SMOKE=1`.");
203
+ },
204
+ async goto() {
205
+ throw new ComputerUseDriverError("host backend not implemented");
206
+ },
207
+ async screenshot() {
208
+ throw new ComputerUseDriverError("host backend not implemented");
209
+ },
210
+ async click() {
211
+ throw new ComputerUseDriverError("host backend not implemented");
212
+ },
213
+ async type() {
214
+ throw new ComputerUseDriverError("host backend not implemented");
215
+ },
216
+ async key() {
217
+ throw new ComputerUseDriverError("host backend not implemented");
218
+ },
219
+ async scroll() {
220
+ throw new ComputerUseDriverError("host backend not implemented");
221
+ },
222
+ async getViewport() {
223
+ throw new ComputerUseDriverError("host backend not implemented");
224
+ },
225
+ async disconnect() { },
226
+ };
227
+ }
228
+ function createRemoteDriverStub() {
229
+ return {
230
+ backend: "remote",
231
+ async connect() {
232
+ throw new ComputerUseDriverError("remote backend (CDP / Browserless) is not implemented in v0");
233
+ },
234
+ async goto() {
235
+ throw new ComputerUseDriverError("remote backend not implemented");
236
+ },
237
+ async screenshot() {
238
+ throw new ComputerUseDriverError("remote backend not implemented");
239
+ },
240
+ async click() {
241
+ throw new ComputerUseDriverError("remote backend not implemented");
242
+ },
243
+ async type() {
244
+ throw new ComputerUseDriverError("remote backend not implemented");
245
+ },
246
+ async key() {
247
+ throw new ComputerUseDriverError("remote backend not implemented");
248
+ },
249
+ async scroll() {
250
+ throw new ComputerUseDriverError("remote backend not implemented");
251
+ },
252
+ async getViewport() {
253
+ throw new ComputerUseDriverError("remote backend not implemented");
254
+ },
255
+ async disconnect() { },
256
+ };
257
+ }
@@ -0,0 +1,26 @@
1
+ export type DnsLookupFn = (host: string) => Promise<{
2
+ readonly address: string;
3
+ readonly family: number;
4
+ }>;
5
+ export type SsrfPinningProxy = {
6
+ /** Proxy URL for the browser's launch options, e.g. `http://127.0.0.1:49321`. */
7
+ readonly url: string;
8
+ readonly port: number;
9
+ close(): Promise<void>;
10
+ };
11
+ export type StartSsrfPinningProxyOptions = {
12
+ /** Test seam: replaces the DNS resolver. */
13
+ readonly _lookup?: DnsLookupFn;
14
+ /** Test seam: replaces the blocked-IP predicate. */
15
+ readonly _isIpBlocked?: (ip: string) => boolean;
16
+ };
17
+ /**
18
+ * Normalise an IPv4 literal to canonical dotted-decimal. Browsers and many
19
+ * HTTP stacks accept octal (`0177.0.0.1`), hex (`0x7f.0.0.1` / `0x7f000001`),
20
+ * and 32-bit integer (`2130706433`) forms — all of which resolve to the same
21
+ * address as `127.0.0.1`. Canonicalising first means `isPrivateIp` classifies
22
+ * them. Returns dotted-decimal, or `null` if `raw` is not an IPv4 literal.
23
+ */
24
+ export declare function normalizeIpv4(raw: string): string | null;
25
+ export declare function isPrivateIp(addr: string): boolean;
26
+ export declare function startSsrfPinningProxy(opts?: StartSsrfPinningProxyOptions): Promise<SsrfPinningProxy>;