@stratusagent/tool-browser 0.6.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 Stratus Labs
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.md ADDED
@@ -0,0 +1,127 @@
1
+ # @stratusagent/tool-browser
2
+
3
+ `browser.goto`, `browser.read`, `browser.screenshot`, `browser.act`: a real
4
+ browser, one per daemon, with a page per conversation and every request
5
+ pinned through a proxy this plugin owns.
6
+
7
+ Reach for [`@stratusagent/tool-web`](../tool-web) first. `web.fetch` is what
8
+ an agent wants twenty times for every once it needs this, and it costs no
9
+ Chromium.
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ npm install @stratusagent/tool-browser
15
+ ```
16
+
17
+ The dependency is **`playwright-core`** — a few megabytes, and it downloads
18
+ no browser. Installing this plugin costs you nothing you did not ask for;
19
+ the browser is one you already have, or one you fetch deliberately:
20
+
21
+ ```bash
22
+ npx playwright install chromium # ~150 MB, if you want a dedicated one
23
+ ```
24
+
25
+ ```jsonc
26
+ // ~/.stratus/config.json — a trusted config only
27
+ {
28
+ "plugins": {
29
+ "@stratusagent/tool-browser": {
30
+ "enabled": true,
31
+ "channel": "chrome" // or "executablePath": "/path/to/chrome"
32
+ }
33
+ }
34
+ }
35
+ ```
36
+
37
+ Nothing here is loaded until a `browser.*` tool is actually called, so a
38
+ daemon with the plugin enabled and no browser installed runs normally and
39
+ fails that one call with a message naming the fix.
40
+
41
+ ## Risk model
42
+
43
+ | Tool | Risk | What approval mode does |
44
+ | --- | --- | --- |
45
+ | `browser.goto` | `gated` | `interactive` asks, `remote` asks in Slack, `headless` refuses. |
46
+ | `browser.read` | `gated` | Same. Navigating and reading reach a service outside Stratus. |
47
+ | `browser.screenshot` | `gated` | Same. Writes a PNG into the agent's workspace and returns it as `file`, which a channel delivers as an attachment. |
48
+ | `browser.act` | `dangerous` | Always a human, in every mode. A click submits, buys, and deletes — navigating somewhere else undoes a `goto`, and nothing undoes a click. |
49
+
50
+ ## Settings
51
+
52
+ | Key | Default | What |
53
+ | --- | --- | --- |
54
+ | `channel` | none | Use an installed browser: `chrome`, `msedge`. |
55
+ | `executablePath` | none | Use the browser at this path. |
56
+ | `headless` | `true` | Set `false` to watch it work. |
57
+ | `allowedHosts` | none | Hosts exempt from the address check. |
58
+ | `allowPrivateAddresses` | `false` | Reach non-global addresses. The trusted-workstation posture. |
59
+ | `idleMs` | `300000` | Close a conversation's context after this much quiet. |
60
+ | `maxContexts` | `4` | Contexts at once; the least recently used goes first. |
61
+ | `maxTextBytes` | `100000` | Cap on `browser.read`, then a `truncated` marker. |
62
+ | `navigationTimeoutMs` | `30000` | Per navigation and per action. |
63
+ | `workspaceRoot` | supplied by the daemon | Where screenshots go: `<root>/<agent-id>/screenshots`. |
64
+
65
+ Address settings apply per agent under `agents`, and they are enforced —
66
+ including when an agent's policy is *narrower* than the default. A proxy is
67
+ chosen when Chromium launches, so a browser can only ever enforce one
68
+ address policy: agents whose policy differs get their own browser, and
69
+ agents that share a policy share one. Interception inside the browser cannot
70
+ substitute, because it does not resolve names and so cannot narrow a
71
+ hostname destination without re-opening the rebinding race the proxy closes.
72
+
73
+ The practical cost is one Chromium per distinct policy. Most deployments
74
+ have exactly one; a config that gives every agent its own posture pays for
75
+ every one of them.
76
+
77
+ ## Why a proxy rather than request interception
78
+
79
+ Playwright can intercept requests, and this plugin does — but interception
80
+ happens *in the browser*, and Chromium resolves names for itself afterwards.
81
+ Validating an address there and letting Chromium resolve it again is DNS
82
+ rebinding with one more process in the way: the attacker's DNS answers
83
+ `93.184.216.34` for the check and `169.254.169.254` for the connection.
84
+
85
+ So every connection goes through a proxy this plugin starts on loopback,
86
+ which resolves the name, checks the address, and dials **that address**.
87
+ Redirects and subresources come back through it too, because every request
88
+ does. The launch carries `--proxy-bypass-list=<-loopback>`, without which
89
+ Chromium would bypass the proxy for exactly the addresses it exists to
90
+ refuse.
91
+
92
+ Interception still does the half a proxy cannot: `file:`, `data:`, and
93
+ other local schemes never reach a proxy at all, and this process can read
94
+ the files they name. Those are refused before navigation and again on every
95
+ request the page makes.
96
+
97
+ The address policy itself is [`@stratusagent/egress`](../egress) — the same
98
+ module `tool-web` uses, tested against the same table of hostile URLs. A
99
+ second copy would not drift into a style difference; the stale one would be
100
+ an SSRF hole.
101
+
102
+ ## Screenshots come back as files, not bytes
103
+
104
+ `browser.screenshot` writes to `<workspaceRoot>/<agent-id>/screenshots/` and
105
+ returns `{ file, url, title }`. The `file` key is the channel contract's
106
+ convention for a result that refers to a local file, so a Slack thread that
107
+ asked for a screenshot gets the image posted into it rather than a path nobody
108
+ can open. Base64 in a tool result would be a transcript no human can read and
109
+ a provider bill nobody wanted.
110
+
111
+ ## Lifecycle
112
+
113
+ One browser per address policy, started on the first call that needs it.
114
+ One **context** per conversation, so two conversations never share a cookie
115
+ jar or a login. A context that has been quiet longer than `idleMs` is
116
+ closed; when a browser's last conversation goes, so does the browser and its
117
+ proxy. `maxContexts` bounds how many contexts can exist at once, dropping
118
+ the least recently used.
119
+
120
+ The sweep runs on the plugin's own timer — unref'd, so it never holds a
121
+ one-shot `stratus run` open, and disarmed once no browser is left. It also
122
+ takes the current time as an argument, which is what makes the lifecycle
123
+ testable without a test that sleeps.
124
+
125
+ Shutdown is the kernel's `dispose` hook, which the gateway calls after
126
+ in-flight turns drain and `stratus run`/`chat` call when they finish. Every
127
+ context, every browser, and every proxy goes with it.
@@ -0,0 +1,82 @@
1
+ import type { OptionalModuleHost } from '@stratusagent/plugins';
2
+ /**
3
+ * The browser, as this pack uses it.
4
+ *
5
+ * A seam rather than a direct Playwright import, for two reasons. The pack
6
+ * must load in a daemon where no browser is installed — every other tool
7
+ * still works, and `browser.*` fails with something an operator can act on
8
+ * — and the lifecycle logic (contexts per session, idle teardown, the
9
+ * concurrency cap) is testable against a stub, where a real Chromium in CI
10
+ * would test the same code and a 300 MB download.
11
+ *
12
+ * Playwright's own objects satisfy these shapes structurally.
13
+ */
14
+ export interface PageLike {
15
+ goto(url: string, options?: {
16
+ timeout?: number;
17
+ waitUntil?: string;
18
+ }): Promise<{
19
+ status(): number;
20
+ } | null>;
21
+ url(): string;
22
+ title(): Promise<string>;
23
+ content(): Promise<string>;
24
+ evaluate(script: string): Promise<unknown>;
25
+ screenshot(options: {
26
+ path?: string;
27
+ fullPage?: boolean;
28
+ }): Promise<Buffer>;
29
+ click(selector: string, options?: {
30
+ timeout?: number;
31
+ }): Promise<void>;
32
+ fill(selector: string, value: string, options?: {
33
+ timeout?: number;
34
+ }): Promise<void>;
35
+ route(pattern: string, handler: (route: RouteLike) => void | Promise<void>): Promise<void>;
36
+ close(): Promise<void>;
37
+ }
38
+ export interface RouteLike {
39
+ request(): {
40
+ url(): string;
41
+ resourceType?: () => string;
42
+ };
43
+ abort(reason?: string): Promise<void>;
44
+ continue(): Promise<void>;
45
+ }
46
+ export interface BrowserContextLike {
47
+ newPage(): Promise<PageLike>;
48
+ close(): Promise<void>;
49
+ }
50
+ export interface BrowserLike {
51
+ newContext(options?: {
52
+ viewport?: {
53
+ width: number;
54
+ height: number;
55
+ };
56
+ }): Promise<BrowserContextLike>;
57
+ close(): Promise<void>;
58
+ }
59
+ export interface LaunchOptions {
60
+ headless: boolean;
61
+ proxy: {
62
+ server: string;
63
+ bypass: string;
64
+ };
65
+ executablePath?: string;
66
+ channel?: string;
67
+ }
68
+ export interface BrowserDriver {
69
+ launch(options: LaunchOptions): Promise<BrowserLike>;
70
+ }
71
+ /**
72
+ * Playwright, loaded only when a browser tool is actually called.
73
+ *
74
+ * `playwright-core` is a few megabytes and downloads no browser, which is
75
+ * the whole reason it is the dependency: installing this plugin costs an
76
+ * operator nothing they did not ask for, and the browser is whatever they
77
+ * already have — an installed Chrome or Edge by `channel`, an
78
+ * `executablePath` they name, or a Chromium they installed with
79
+ * `npx playwright install chromium`.
80
+ */
81
+ export declare const createPlaywrightDriver: (host: OptionalModuleHost) => BrowserDriver;
82
+ //# sourceMappingURL=driver.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["../src/driver.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAGhE;;;;;;;;;;;GAWG;AACH,MAAM,WAAW,QAAQ;IACvB,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAC;QAAC,SAAS,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC;QAAE,MAAM,IAAI,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC,CAAC;IAC5G,GAAG,IAAI,MAAM,CAAC;IACd,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACzB,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAC3B,QAAQ,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3C,UAAU,CAAC,OAAO,EAAE;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5E,KAAK,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,IAAI,CAAC,QAAQ,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACrF,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,CAAC,KAAK,EAAE,SAAS,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC3F,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,MAAM,WAAW,SAAS;IACxB,OAAO,IAAI;QAAE,GAAG,IAAI,MAAM,CAAC;QAAC,YAAY,CAAC,EAAE,MAAM,MAAM,CAAA;KAAE,CAAC;IAC1D,KAAK,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAED,MAAM,WAAW,kBAAkB;IACjC,OAAO,IAAI,OAAO,CAAC,QAAQ,CAAC,CAAC;IAC7B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,UAAU,CAAC,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE;YAAE,KAAK,EAAE,MAAM,CAAC;YAAC,MAAM,EAAE,MAAM,CAAA;SAAE,CAAA;KAAE,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAAC;IACpG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAED,MAAM,WAAW,aAAa;IAC5B,QAAQ,EAAE,OAAO,CAAC;IAClB,KAAK,EAAE;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAA;KAAE,CAAC;IAC1C,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,OAAO,EAAE,aAAa,GAAG,OAAO,CAAC,WAAW,CAAC,CAAC;CACtD;AAED;;;;;;;;;GASG;AACH,eAAO,MAAM,sBAAsB,GAAI,MAAM,kBAAkB,KAAG,aA8BhE,CAAC"}
package/dist/driver.js ADDED
@@ -0,0 +1,37 @@
1
+ import { loadOptionalModule } from '@stratusagent/plugins';
2
+ /**
3
+ * Playwright, loaded only when a browser tool is actually called.
4
+ *
5
+ * `playwright-core` is a few megabytes and downloads no browser, which is
6
+ * the whole reason it is the dependency: installing this plugin costs an
7
+ * operator nothing they did not ask for, and the browser is whatever they
8
+ * already have — an installed Chrome or Edge by `channel`, an
9
+ * `executablePath` they name, or a Chromium they installed with
10
+ * `npx playwright install chromium`.
11
+ */
12
+ export const createPlaywrightDriver = (host) => ({
13
+ async launch(options) {
14
+ const playwright = await loadOptionalModule('playwright-core', host);
15
+ if (!playwright) {
16
+ throw new Error('browser.* needs playwright-core, which is not installed. '
17
+ + 'Install it alongside @stratusagent/tool-browser (npm install playwright-core).');
18
+ }
19
+ try {
20
+ return await playwright.chromium.launch({
21
+ headless: options.headless,
22
+ proxy: options.proxy,
23
+ ...(options.executablePath ? { executablePath: options.executablePath } : {}),
24
+ ...(options.channel ? { channel: options.channel } : {}),
25
+ });
26
+ }
27
+ catch (error) {
28
+ const message = error instanceof Error ? error.message : String(error);
29
+ // The overwhelmingly common failure, and one an operator can fix in a
30
+ // line — worth saying rather than passing Playwright's paragraph on.
31
+ throw new Error(`Could not start a browser: ${message}\n`
32
+ + 'Install one with `npx playwright install chromium`, or point the plugin at a browser you have '
33
+ + '(`"channel": "chrome"` or `"executablePath": "/path/to/chrome"`).');
34
+ }
35
+ },
36
+ });
37
+ //# sourceMappingURL=driver.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"driver.js","sourceRoot":"","sources":["../src/driver.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAsD3D;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,IAAwB,EAAiB,EAAE,CAAC,CAAC;IAClF,KAAK,CAAC,MAAM,CAAC,OAAO;QAClB,MAAM,UAAU,GAAG,MAAM,kBAAkB,CAExC,iBAAiB,EAAE,IAAI,CAAC,CAAC;QAC5B,IAAI,CAAC,UAAU,EAAE,CAAC;YAChB,MAAM,IAAI,KAAK,CACb,2DAA2D;kBACzD,gFAAgF,CACnF,CAAC;QACJ,CAAC;QAED,IAAI,CAAC;YACH,OAAO,MAAM,UAAU,CAAC,QAAQ,CAAC,MAAM,CAAC;gBACtC,QAAQ,EAAE,OAAO,CAAC,QAAQ;gBAC1B,KAAK,EAAE,OAAO,CAAC,KAAK;gBACpB,GAAG,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC,CAAC,EAAE,cAAc,EAAE,OAAO,CAAC,cAAc,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;gBAC7E,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;aACzD,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YACvE,sEAAsE;YACtE,qEAAqE;YACrE,MAAM,IAAI,KAAK,CACb,8BAA8B,OAAO,IAAI;kBACvC,gGAAgG;kBAChG,mEAAmE,CACtE,CAAC;QACJ,CAAC;IACH,CAAC;CACF,CAAC,CAAC"}
@@ -0,0 +1,40 @@
1
+ import type { JsonObject, Plugin } from '@stratusagent/core';
2
+ import { type OptionalModuleHost } from '@stratusagent/plugins';
3
+ import { type BrowserDriver } from './driver.ts';
4
+ export { createPlaywrightDriver, type BrowserContextLike, type BrowserDriver, type BrowserLike, type LaunchOptions, type PageLike, type RouteLike, } from './driver.ts';
5
+ export { BrowserSessionPool, type SessionPoolOptions } from './sessions.ts';
6
+ export interface BrowserPluginConfig extends JsonObject {
7
+ allowPrivateAddresses?: boolean;
8
+ allowedHosts?: string[];
9
+ headless?: boolean;
10
+ executablePath?: string;
11
+ channel?: string;
12
+ idleMs?: number;
13
+ maxContexts?: number;
14
+ maxTextBytes?: number;
15
+ navigationTimeoutMs?: number;
16
+ /** Supplied by the host: `~/.stratus/workspaces`, one directory per agent. */
17
+ workspaceRoot?: string;
18
+ }
19
+ export interface BrowserPluginOptions {
20
+ /** Overrides the Playwright driver. Tests use it; nothing else should. */
21
+ driver?: BrowserDriver;
22
+ /** How this host resolves and imports optional packages. */
23
+ moduleHost?: OptionalModuleHost;
24
+ }
25
+ /**
26
+ * The `browser` toolset.
27
+ *
28
+ * One browser per plugin, a context per conversation, and an idle sweep
29
+ * that closes both — see `BrowserSessionPool`. Every request the browser
30
+ * makes goes through a proxy this plugin owns, which resolves and validates
31
+ * each address itself, because Chromium resolves names for itself no matter
32
+ * what an interception handler decided.
33
+ */
34
+ export declare const createBrowserPlugin: (config?: JsonObject, options?: BrowserPluginOptions) => Plugin & {
35
+ dispose(): Promise<void>;
36
+ sweepIdle(now: number): Promise<string[]>;
37
+ };
38
+ /** The loader's ABI. See `docs/architecture/plugins.md`. */
39
+ export declare const createPlugin: (config?: JsonObject) => Plugin;
40
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,UAAU,EAAa,MAAM,EAAiB,MAAM,oBAAoB,CAAC;AAEvF,OAAO,EAA4B,KAAK,kBAAkB,EAAE,MAAM,uBAAuB,CAAC;AAE1F,OAAO,EAA0B,KAAK,aAAa,EAAiC,MAAM,aAAa,CAAC;AAGxG,OAAO,EACL,sBAAsB,EACtB,KAAK,kBAAkB,EACvB,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,aAAa,EAClB,KAAK,QAAQ,EACb,KAAK,SAAS,GACf,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,kBAAkB,EAAE,KAAK,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAK5E,MAAM,WAAW,mBAAoB,SAAQ,UAAU;IACrD,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,8EAA8E;IAC9E,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AA0PD,MAAM,WAAW,oBAAoB;IACnC,0EAA0E;IAC1E,MAAM,CAAC,EAAE,aAAa,CAAC;IACvB,4DAA4D;IAC5D,UAAU,CAAC,EAAE,kBAAkB,CAAC;CACjC;AAED;;;;;;;;GAQG;AACH,eAAO,MAAM,mBAAmB,GAC9B,SAAQ,UAAe,EACvB,UAAS,oBAAyB,KACjC,MAAM,GAAG;IAAE,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IAAC,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAA;CAgEhF,CAAC;AAEF,4DAA4D;AAC5D,eAAO,MAAM,YAAY,GAAI,SAAQ,UAAe,KAAG,MAAqC,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,293 @@
1
+ var __rewriteRelativeImportExtension = (this && this.__rewriteRelativeImportExtension) || function (path, preserveJsx) {
2
+ if (typeof path === "string" && /^\.\.?\//.test(path)) {
3
+ return path.replace(/\.(tsx)$|((?:\.d)?)((?:\.[^./]+?)?)\.([cm]?)ts$/i, function (m, tsx, d, ext, cm) {
4
+ return tsx ? preserveJsx ? ".jsx" : ".js" : d && (!ext || !cm) ? m : (d + ext + "." + cm.toLowerCase() + "js");
5
+ });
6
+ }
7
+ return path;
8
+ };
9
+ import { mkdir } from 'node:fs/promises';
10
+ import path from 'node:path';
11
+ import { assertRequestAllowed } from '@stratusagent/egress';
12
+ import { resolvePluginAgentConfig } from '@stratusagent/plugins';
13
+ import { createPlaywrightDriver } from "./driver.js";
14
+ import { BrowserSessionPool } from "./sessions.js";
15
+ export { createPlaywrightDriver, } from "./driver.js";
16
+ export { BrowserSessionPool } from "./sessions.js";
17
+ const DEFAULT_NAVIGATION_TIMEOUT_MS = 30_000;
18
+ const DEFAULT_MAX_TEXT_BYTES = 100_000;
19
+ const asNumber = (value, fallback) => typeof value === 'number' && Number.isFinite(value) && value > 0 ? value : fallback;
20
+ /** The address policy a block describes, per-agent or top-level. */
21
+ const policyFrom = (resolved) => ({
22
+ ...(resolved.allowPrivateAddresses === true ? { allowPrivateAddresses: true } : {}),
23
+ ...(Array.isArray(resolved.allowedHosts)
24
+ ? { allowedHosts: resolved.allowedHosts.filter((entry) => typeof entry === 'string') }
25
+ : {}),
26
+ });
27
+ const settingsFor = (config, session) => {
28
+ const resolved = resolvePluginAgentConfig(config, session.agent.id);
29
+ const policy = policyFrom(resolved);
30
+ return {
31
+ policy,
32
+ maxTextBytes: asNumber(resolved.maxTextBytes, DEFAULT_MAX_TEXT_BYTES),
33
+ navigationTimeoutMs: asNumber(resolved.navigationTimeoutMs, DEFAULT_NAVIGATION_TIMEOUT_MS),
34
+ workspaceRoot: typeof resolved.workspaceRoot === 'string' ? resolved.workspaceRoot : undefined,
35
+ };
36
+ };
37
+ /**
38
+ * Text as a reader would see it, extracted in the page.
39
+ *
40
+ * The DOM is right there, so there is nothing to guess at: the furniture is
41
+ * removed from a clone and what is left is `innerText`, which already
42
+ * respects what is visible. This is the one place a browser genuinely beats
43
+ * `web.fetch`, whose extractor has to infer all of it from markup.
44
+ */
45
+ const READABLE_TEXT_SCRIPT = `(() => {
46
+ const doc = document.cloneNode(true);
47
+ for (const element of doc.querySelectorAll('script, style, noscript, svg, nav, header, footer, aside, form')) {
48
+ element.remove();
49
+ }
50
+ const main = doc.querySelector('article, main') ?? doc.body;
51
+ return main ? main.innerText : '';
52
+ })()`;
53
+ /**
54
+ * The request-level half of the address policy, inside the browser.
55
+ *
56
+ * The proxy the browser is launched through owns every connection it makes,
57
+ * which is what defeats rebinding — but a `file:` or `data:` URL never
58
+ * reaches a proxy, and Chromium runs as the daemon user. So schemes are
59
+ * refused here, on every request the page makes: the navigation itself,
60
+ * each redirect, and every subresource.
61
+ */
62
+ const guardRequests = async (page, policy, record) => {
63
+ await page.route('**/*', async (route) => {
64
+ const url = route.request().url();
65
+ try {
66
+ assertRequestAllowed(url, policy);
67
+ await route.continue();
68
+ }
69
+ catch (error) {
70
+ record(`${url} — ${error instanceof Error ? error.message : String(error)}`);
71
+ await route.abort('blockedbyclient');
72
+ }
73
+ });
74
+ };
75
+ /**
76
+ * What was refused while this session's page was loading, from both halves
77
+ * of the policy: the scheme guard inside the browser, and the proxy that
78
+ * owns the connections. Reported to the agent because a page that renders
79
+ * empty because its requests were blocked is otherwise a mystery it retries.
80
+ */
81
+ const refusalsFor = (runtime, session) => [
82
+ ...(runtime.blocked.get(session.id) ?? []),
83
+ ...runtime.pool.refusalsFor(session.id),
84
+ ].slice(-10);
85
+ const truncate = (value, maxBytes) => Buffer.byteLength(value, 'utf8') <= maxBytes
86
+ ? { text: value, truncated: false }
87
+ : { text: `${value.slice(0, maxBytes)}\n… truncated at ${maxBytes} bytes`, truncated: true };
88
+ const navigate = async (page, rawUrl, policy, timeoutMs) => {
89
+ // Before navigation, not after: a `file:///…/credentials.json` must never
90
+ // be handed to the browser at all, because by the time an interception
91
+ // handler sees it the read has a chance to have happened.
92
+ const url = assertRequestAllowed(rawUrl, policy);
93
+ const response = await page.goto(url.href, { timeout: timeoutMs, waitUntil: 'load' });
94
+ return response?.status();
95
+ };
96
+ const screenshotPathFor = async (workspaceRoot, session, now) => {
97
+ if (!workspaceRoot) {
98
+ throw new Error('browser.screenshot has nowhere to write. Set workspaceRoot for @stratusagent/tool-browser, '
99
+ + 'or run through a daemon that supplies it.');
100
+ }
101
+ // One directory per agent, under the workspace root the host resolved:
102
+ // an agent's output is that agent's, and a shared scratch directory is
103
+ // two agents reading each other's screenshots.
104
+ const directory = path.join(workspaceRoot, session.agent.id, 'screenshots');
105
+ await mkdir(directory, { recursive: true });
106
+ return path.join(directory, `${new Date(now).toISOString().replace(/[:.]/g, '-')}.png`);
107
+ };
108
+ const createTools = (config, runtime) => {
109
+ const goto = {
110
+ name: 'browser.goto',
111
+ description: 'Open a URL in this conversation’s browser page.',
112
+ risk: 'gated',
113
+ parameters: {
114
+ type: 'object',
115
+ properties: { url: { type: 'string' } },
116
+ required: ['url'],
117
+ },
118
+ async execute(input, session) {
119
+ const settings = settingsFor(config, session);
120
+ const page = await runtime.pageFor(session);
121
+ const status = await navigate(page, String(input.url ?? ''), settings.policy, settings.navigationTimeoutMs);
122
+ return {
123
+ url: page.url(),
124
+ ...(status === undefined ? {} : { status }),
125
+ title: await page.title(),
126
+ ...(refusalsFor(runtime, session).length > 0
127
+ ? { blockedRequests: refusalsFor(runtime, session) }
128
+ : {}),
129
+ };
130
+ },
131
+ };
132
+ const read = {
133
+ name: 'browser.read',
134
+ description: 'Read the current page as text, optionally navigating first.',
135
+ risk: 'gated',
136
+ parameters: {
137
+ type: 'object',
138
+ properties: { url: { type: 'string' }, maxBytes: { type: 'number' } },
139
+ },
140
+ async execute(input, session) {
141
+ const settings = settingsFor(config, session);
142
+ const page = await runtime.pageFor(session);
143
+ if (typeof input.url === 'string' && input.url.length > 0) {
144
+ await navigate(page, input.url, settings.policy, settings.navigationTimeoutMs);
145
+ }
146
+ const extracted = await page.evaluate(READABLE_TEXT_SCRIPT);
147
+ const { text, truncated } = truncate(typeof extracted === 'string' ? extracted : '', asNumber(input.maxBytes, settings.maxTextBytes));
148
+ return {
149
+ url: page.url(),
150
+ title: await page.title(),
151
+ text,
152
+ truncated,
153
+ ...(refusalsFor(runtime, session).length > 0
154
+ ? { blockedRequests: refusalsFor(runtime, session) }
155
+ : {}),
156
+ };
157
+ },
158
+ };
159
+ const screenshot = {
160
+ name: 'browser.screenshot',
161
+ description: 'Save a screenshot of the current page and return its path.',
162
+ risk: 'gated',
163
+ parameters: {
164
+ type: 'object',
165
+ properties: { url: { type: 'string' }, fullPage: { type: 'boolean' } },
166
+ },
167
+ async execute(input, session) {
168
+ const settings = settingsFor(config, session);
169
+ const page = await runtime.pageFor(session);
170
+ if (typeof input.url === 'string' && input.url.length > 0) {
171
+ await navigate(page, input.url, settings.policy, settings.navigationTimeoutMs);
172
+ }
173
+ const target = await screenshotPathFor(settings.workspaceRoot, session, Date.now());
174
+ await page.screenshot({ path: target, fullPage: input.fullPage === true });
175
+ // `file`, because that is the key a channel already acts on: an ok
176
+ // result carrying `file` (or `files`) is delivered as an attachment,
177
+ // which is how "screenshot example.com and show me" ends with a
178
+ // picture in Slack rather than a path nobody can open. One key rather
179
+ // than a `path` alias beside it — two words for one thing is how a
180
+ // convention stops being one.
181
+ return { file: target, url: page.url(), title: await page.title() };
182
+ },
183
+ };
184
+ const act = {
185
+ name: 'browser.act',
186
+ description: 'Click or type on the current page.',
187
+ // Dangerous, and the only one here that is: navigating and reading can
188
+ // be undone by navigating somewhere else, while a click submits, buys,
189
+ // and deletes. No selector makes that reversible.
190
+ risk: 'dangerous',
191
+ parameters: {
192
+ type: 'object',
193
+ properties: {
194
+ action: { type: 'string', enum: ['click', 'type'] },
195
+ selector: { type: 'string' },
196
+ value: { type: 'string' },
197
+ },
198
+ required: ['action', 'selector'],
199
+ },
200
+ async execute(input, session) {
201
+ const settings = settingsFor(config, session);
202
+ const page = await runtime.pageFor(session);
203
+ const selector = String(input.selector ?? '');
204
+ if (!selector) {
205
+ throw new Error('selector is required.');
206
+ }
207
+ if (input.action === 'type') {
208
+ await page.fill(selector, String(input.value ?? ''), { timeout: settings.navigationTimeoutMs });
209
+ }
210
+ else if (input.action === 'click') {
211
+ await page.click(selector, { timeout: settings.navigationTimeoutMs });
212
+ }
213
+ else {
214
+ throw new Error(`Unsupported action: ${String(input.action)}. Use click or type.`);
215
+ }
216
+ return { action: input.action, selector, url: page.url(), title: await page.title() };
217
+ },
218
+ };
219
+ return [goto, read, screenshot, act];
220
+ };
221
+ /**
222
+ * The `browser` toolset.
223
+ *
224
+ * One browser per plugin, a context per conversation, and an idle sweep
225
+ * that closes both — see `BrowserSessionPool`. Every request the browser
226
+ * makes goes through a proxy this plugin owns, which resolves and validates
227
+ * each address itself, because Chromium resolves names for itself no matter
228
+ * what an interception handler decided.
229
+ */
230
+ export const createBrowserPlugin = (config = {}, options = {}) => {
231
+ const blocked = new Map();
232
+ const resolved = config;
233
+ const pool = new BrowserSessionPool({
234
+ driver: options.driver ?? createPlaywrightDriver(options.moduleHost ?? {
235
+ resolve: (specifier) => import.meta.resolve(specifier),
236
+ import: (specifier) => import(__rewriteRelativeImportExtension(specifier)),
237
+ }),
238
+ ...(resolved.headless === false ? { headless: false } : {}),
239
+ ...(typeof resolved.executablePath === 'string' ? { executablePath: resolved.executablePath } : {}),
240
+ ...(typeof resolved.channel === 'string' ? { channel: resolved.channel } : {}),
241
+ ...(typeof resolved.idleMs === 'number' ? { idleMs: resolved.idleMs } : {}),
242
+ ...(typeof resolved.maxContexts === 'number' ? { maxContexts: resolved.maxContexts } : {}),
243
+ onEvicted: ({ sessionId }) => {
244
+ // The page that recorded them is gone, so its refusals go with it.
245
+ blocked.delete(sessionId);
246
+ },
247
+ });
248
+ const runtime = {
249
+ pool,
250
+ blocked,
251
+ async pageFor(session) {
252
+ const settings = settingsFor(config, session);
253
+ // The agent's own policy decides which browser serves it: a proxy is
254
+ // chosen when Chromium launches, so an agent whose config narrows the
255
+ // address policy needs a browser launched under that policy rather
256
+ // than a context inside somebody else's.
257
+ const page = await pool.pageFor(session.id, Date.now(), settings.policy);
258
+ // Routed once per page. A page created for this session gets the
259
+ // scheme guard before anything is asked of it.
260
+ if (!page.__stratusGuarded) {
261
+ await guardRequests(page, settings.policy, (entry) => {
262
+ blocked.set(session.id, [...(blocked.get(session.id) ?? []), entry].slice(-20));
263
+ });
264
+ page.__stratusGuarded = true;
265
+ }
266
+ return page;
267
+ },
268
+ };
269
+ return {
270
+ name: '@stratusagent/tool-browser',
271
+ setup(context) {
272
+ for (const tool of createTools(config, runtime)) {
273
+ context.tools.register(tool);
274
+ }
275
+ },
276
+ /**
277
+ * The kernel's teardown hook, which is what a host actually calls.
278
+ * Named anything else, a browser and a listening proxy survive both
279
+ * `stratus run` and a daemon shutdown — the second of which is the
280
+ * whole reason `Plugin.dispose` exists.
281
+ */
282
+ dispose: () => pool.close(),
283
+ /**
284
+ * The sweep, for a caller that wants it at a chosen instant. The pool
285
+ * also runs it on its own timer; this is what makes the behaviour
286
+ * testable without a test that sleeps.
287
+ */
288
+ sweepIdle: (now) => pool.sweepIdle(now),
289
+ };
290
+ };
291
+ /** The loader's ABI. See `docs/architecture/plugins.md`. */
292
+ export const createPlugin = (config = {}) => createBrowserPlugin(config);
293
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,95 @@
1
+ import { type EgressPolicy } from '@stratusagent/egress';
2
+ import type { BrowserDriver, PageLike } from './driver.ts';
3
+ export interface SessionPoolOptions {
4
+ driver: BrowserDriver;
5
+ headless?: boolean;
6
+ executablePath?: string;
7
+ channel?: string;
8
+ /** Close a context that has done nothing for this long. */
9
+ idleMs?: number;
10
+ /** How many contexts may exist at once. The oldest goes when a new one would exceed it. */
11
+ maxContexts?: number;
12
+ /** Called when a page is dropped, so a caller can log it. */
13
+ onEvicted?: (event: {
14
+ sessionId: string;
15
+ reason: 'idle' | 'capacity' | 'shutdown' | 'policy';
16
+ }) => void;
17
+ /** Reported rather than thrown: a sweep runs on a timer with nobody waiting on it. */
18
+ onError?: (error: unknown) => void;
19
+ }
20
+ /**
21
+ * Browsers, contexts, and the proxies that own their connections.
22
+ *
23
+ * Three rules, and each of them is a boundary rather than an optimization:
24
+ *
25
+ * - **A browser per policy.** A proxy is chosen when Chromium launches, so
26
+ * a single shared browser can only ever enforce one address policy — and
27
+ * an agent whose config *narrows* it (`allowedHosts: []` over a permissive
28
+ * default) would silently get the permissive one. Contexts cannot fix
29
+ * that: request interception runs inside the browser and does not resolve
30
+ * names, so it cannot narrow a hostname destination without re-opening
31
+ * the DNS-rebinding race the proxy exists to close.
32
+ * - **A context per conversation**, so two conversations never share a
33
+ * cookie jar or a login.
34
+ * - **Nothing left running.** A context that has gone quiet is closed, and
35
+ * a browser whose last conversation went with it is closed too.
36
+ *
37
+ * The sweep takes the time as an argument *and* runs on a timer. A pool that
38
+ * only read the clock itself could only be tested by sleeping; one that only
39
+ * offered the method would leave `idleMs` as a setting nothing honoured.
40
+ */
41
+ export declare class BrowserSessionPool {
42
+ private readonly options;
43
+ private readonly contexts;
44
+ private readonly browsers;
45
+ private readonly launching;
46
+ private sweepTimer;
47
+ /**
48
+ * Admission runs one at a time.
49
+ *
50
+ * The cap is checked and then a context is created, and creating one
51
+ * yields — so without this, every call in a burst sees room, and a pool
52
+ * capped at four opens as many browsers' worth of contexts as there were
53
+ * simultaneous first calls. A queue is enough: admission is short, and
54
+ * the alternative (reserving a slot and unwinding it on failure) is the
55
+ * same serialization with more ways to leak a reservation.
56
+ */
57
+ private admission;
58
+ private closed;
59
+ constructor(options: SessionPoolOptions);
60
+ private browserFor;
61
+ /**
62
+ * The page this session works in, created on first use — under the
63
+ * policy resolved for *this* agent, not for whoever asked first.
64
+ */
65
+ pageFor(sessionId: string, now: number, policy?: EgressPolicy): Promise<PageLike>;
66
+ private admit;
67
+ /**
68
+ * What the proxy refused for this session's browser.
69
+ *
70
+ * Scoped to the browser rather than to the process: an agent under a
71
+ * different policy has a different browser and never sees these. Two
72
+ * agents that share a policy share a browser and can see each other's
73
+ * refused *addresses* — which is the same set their identical policy
74
+ * already told them about.
75
+ */
76
+ refusalsFor(sessionId: string): string[];
77
+ private drop;
78
+ /** Close whatever has been idle longer than the timeout. Returns what went. */
79
+ sweepIdle(now: number): Promise<string[]>;
80
+ /**
81
+ * The timer behind `idleMs`. Unref'd, so a pending sweep never holds a
82
+ * one-shot `stratus run` open, and disarmed as soon as the last browser
83
+ * goes so an idle daemon carries no timer at all.
84
+ */
85
+ private armSweep;
86
+ private disarmSweep;
87
+ private closeBrowser;
88
+ /** Everything, for shutdown. */
89
+ close(): Promise<void>;
90
+ /** How many conversations currently hold a page. */
91
+ get size(): number;
92
+ /** How many browsers are running — one per distinct address policy. */
93
+ get browserCount(): number;
94
+ }
95
+ //# sourceMappingURL=sessions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessions.d.ts","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,YAAY,EAElB,MAAM,sBAAsB,CAAC;AAE9B,OAAO,KAAK,EAAsB,aAAa,EAAe,QAAQ,EAAE,MAAM,aAAa,CAAC;AAE5F,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,aAAa,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,2DAA2D;IAC3D,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2FAA2F;IAC3F,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,6DAA6D;IAC7D,SAAS,CAAC,EAAE,CAAC,KAAK,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,GAAG,UAAU,GAAG,UAAU,GAAG,QAAQ,CAAA;KAAE,KAAK,IAAI,CAAC;IACxG,sFAAsF;IACtF,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACpC;AAqBD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,qBAAa,kBAAkB;IAC7B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAE7C,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAoC;IAE7D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAmC;IAE5D,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA4C;IAEtE,OAAO,CAAC,UAAU,CAA6B;IAE/C;;;;;;;;;OASG;IACH,OAAO,CAAC,SAAS,CAAuC;IAExD,OAAO,CAAC,MAAM,CAAS;gBAEX,OAAO,EAAE,kBAAkB;YAIzB,UAAU;IAqCxB;;;OAGG;IACG,OAAO,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,GAAE,YAAiB,GAAG,OAAO,CAAC,QAAQ,CAAC;YAoB7E,KAAK;IAyDnB;;;;;;;;OAQG;IACH,WAAW,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,EAAE;YAQ1B,IAAI;IAiBlB,+EAA+E;IACzE,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC;IAS/C;;;;OAIG;IACH,OAAO,CAAC,QAAQ;IAYhB,OAAO,CAAC,WAAW;YAOL,YAAY;IAa1B,gCAAgC;IAC1B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAa5B,oDAAoD;IACpD,IAAI,IAAI,IAAI,MAAM,CAEjB;IAED,uEAAuE;IACvE,IAAI,YAAY,IAAI,MAAM,CAEzB;CACF"}
@@ -0,0 +1,253 @@
1
+ import { chromiumProxyOptions, createEgressProxy, policyKeyFor, } from '@stratusagent/egress';
2
+ const DEFAULT_IDLE_MS = 5 * 60_000;
3
+ const DEFAULT_MAX_CONTEXTS = 4;
4
+ const MAX_SWEEP_INTERVAL_MS = 60_000;
5
+ /**
6
+ * Browsers, contexts, and the proxies that own their connections.
7
+ *
8
+ * Three rules, and each of them is a boundary rather than an optimization:
9
+ *
10
+ * - **A browser per policy.** A proxy is chosen when Chromium launches, so
11
+ * a single shared browser can only ever enforce one address policy — and
12
+ * an agent whose config *narrows* it (`allowedHosts: []` over a permissive
13
+ * default) would silently get the permissive one. Contexts cannot fix
14
+ * that: request interception runs inside the browser and does not resolve
15
+ * names, so it cannot narrow a hostname destination without re-opening
16
+ * the DNS-rebinding race the proxy exists to close.
17
+ * - **A context per conversation**, so two conversations never share a
18
+ * cookie jar or a login.
19
+ * - **Nothing left running.** A context that has gone quiet is closed, and
20
+ * a browser whose last conversation went with it is closed too.
21
+ *
22
+ * The sweep takes the time as an argument *and* runs on a timer. A pool that
23
+ * only read the clock itself could only be tested by sleeping; one that only
24
+ * offered the method would leave `idleMs` as a setting nothing honoured.
25
+ */
26
+ export class BrowserSessionPool {
27
+ options;
28
+ contexts = new Map();
29
+ browsers = new Map();
30
+ launching = new Map();
31
+ sweepTimer;
32
+ /**
33
+ * Admission runs one at a time.
34
+ *
35
+ * The cap is checked and then a context is created, and creating one
36
+ * yields — so without this, every call in a burst sees room, and a pool
37
+ * capped at four opens as many browsers' worth of contexts as there were
38
+ * simultaneous first calls. A queue is enough: admission is short, and
39
+ * the alternative (reserving a slot and unwinding it on failure) is the
40
+ * same serialization with more ways to leak a reservation.
41
+ */
42
+ admission = Promise.resolve();
43
+ closed = false;
44
+ constructor(options) {
45
+ this.options = options;
46
+ }
47
+ async browserFor(policy) {
48
+ const key = policyKeyFor(policy);
49
+ const existing = this.browsers.get(key);
50
+ if (existing) {
51
+ return existing;
52
+ }
53
+ // Single-flight per policy: two calls arriving together must not launch
54
+ // two browsers, and the second would leak because only one is kept.
55
+ const pending = this.launching.get(key);
56
+ if (pending) {
57
+ return pending;
58
+ }
59
+ const launch = (async () => {
60
+ const proxy = await createEgressProxy(policy);
61
+ try {
62
+ const browser = await this.options.driver.launch({
63
+ headless: this.options.headless ?? true,
64
+ proxy: chromiumProxyOptions(proxy),
65
+ ...(this.options.executablePath ? { executablePath: this.options.executablePath } : {}),
66
+ ...(this.options.channel ? { channel: this.options.channel } : {}),
67
+ });
68
+ const entry = { key, browser, proxy, sessions: new Set() };
69
+ this.browsers.set(key, entry);
70
+ this.armSweep();
71
+ return entry;
72
+ }
73
+ catch (error) {
74
+ await proxy.close();
75
+ throw error;
76
+ }
77
+ finally {
78
+ this.launching.delete(key);
79
+ }
80
+ })();
81
+ this.launching.set(key, launch);
82
+ return launch;
83
+ }
84
+ /**
85
+ * The page this session works in, created on first use — under the
86
+ * policy resolved for *this* agent, not for whoever asked first.
87
+ */
88
+ async pageFor(sessionId, now, policy = {}) {
89
+ const key = policyKeyFor(policy);
90
+ const existing = this.contexts.get(sessionId);
91
+ if (existing && existing.browserKey === key) {
92
+ // The common path, and the only one that does not queue: a
93
+ // conversation coming back to its own page competes with nobody.
94
+ existing.lastUsedAt = now;
95
+ return existing.page;
96
+ }
97
+ const admitted = this.admission.then(() => this.admit(sessionId, now, policy, key), () => this.admit(sessionId, now, policy, key));
98
+ // The queue advances whether or not this admission succeeded; a failed
99
+ // launch must not block every later call behind it.
100
+ this.admission = admitted.catch(() => undefined);
101
+ return admitted;
102
+ }
103
+ async admit(sessionId, now, policy, key) {
104
+ // Re-read inside the queue: another admission may have created this
105
+ // session's context, or changed what is in the pool, while this one
106
+ // waited its turn.
107
+ const existing = this.contexts.get(sessionId);
108
+ if (existing) {
109
+ if (existing.browserKey === key) {
110
+ existing.lastUsedAt = now;
111
+ return existing.page;
112
+ }
113
+ // The agent's policy changed under a live conversation (an edited
114
+ // config, a soul now resolving different settings). The open context
115
+ // belongs to a browser enforcing the old one, so it goes.
116
+ await this.drop(existing, 'policy');
117
+ }
118
+ const entry = await this.browserFor(policy);
119
+ const maxContexts = this.options.maxContexts ?? DEFAULT_MAX_CONTEXTS;
120
+ while (this.contexts.size >= maxContexts) {
121
+ const oldest = [...this.contexts.values()].sort((left, right) => left.lastUsedAt - right.lastUsedAt)[0];
122
+ if (!oldest) {
123
+ break;
124
+ }
125
+ await this.drop(oldest, 'capacity');
126
+ }
127
+ const context = await entry.browser.newContext();
128
+ let page;
129
+ try {
130
+ page = await context.newPage();
131
+ }
132
+ catch (error) {
133
+ // A context that never got a page is not in `contexts`, so the idle
134
+ // sweep will never look at it — and neither will shutdown, which
135
+ // walks the same map. Closed here or not at all, and the browser
136
+ // goes too when it turns out nothing is using it (which is also how
137
+ // a Chromium that died during page creation stops being handed to
138
+ // the next caller).
139
+ try {
140
+ await context.close();
141
+ }
142
+ catch {
143
+ // Already gone with the browser that owned it.
144
+ }
145
+ if (entry.sessions.size === 0) {
146
+ await this.closeBrowser(entry);
147
+ }
148
+ throw error;
149
+ }
150
+ this.contexts.set(sessionId, { sessionId, context, page, lastUsedAt: now, browserKey: key });
151
+ entry.sessions.add(sessionId);
152
+ return page;
153
+ }
154
+ /**
155
+ * What the proxy refused for this session's browser.
156
+ *
157
+ * Scoped to the browser rather than to the process: an agent under a
158
+ * different policy has a different browser and never sees these. Two
159
+ * agents that share a policy share a browser and can see each other's
160
+ * refused *addresses* — which is the same set their identical policy
161
+ * already told them about.
162
+ */
163
+ refusalsFor(sessionId) {
164
+ const context = this.contexts.get(sessionId);
165
+ if (!context) {
166
+ return [];
167
+ }
168
+ return this.browsers.get(context.browserKey)?.proxy.refusals ?? [];
169
+ }
170
+ async drop(entry, reason) {
171
+ this.contexts.delete(entry.sessionId);
172
+ try {
173
+ await entry.context.close();
174
+ }
175
+ catch {
176
+ // A context that is already gone is the outcome we wanted.
177
+ }
178
+ const browser = this.browsers.get(entry.browserKey);
179
+ browser?.sessions.delete(entry.sessionId);
180
+ if (browser && browser.sessions.size === 0) {
181
+ // The browser goes with its last conversation: a Chromium sitting
182
+ // idle overnight is the leak this pack is most likely to cause.
183
+ await this.closeBrowser(browser);
184
+ }
185
+ this.options.onEvicted?.({ sessionId: entry.sessionId, reason });
186
+ }
187
+ /** Close whatever has been idle longer than the timeout. Returns what went. */
188
+ async sweepIdle(now) {
189
+ const idleMs = this.options.idleMs ?? DEFAULT_IDLE_MS;
190
+ const expired = [...this.contexts.values()].filter((entry) => now - entry.lastUsedAt >= idleMs);
191
+ for (const entry of expired) {
192
+ await this.drop(entry, 'idle');
193
+ }
194
+ return expired.map((entry) => entry.sessionId);
195
+ }
196
+ /**
197
+ * The timer behind `idleMs`. Unref'd, so a pending sweep never holds a
198
+ * one-shot `stratus run` open, and disarmed as soon as the last browser
199
+ * goes so an idle daemon carries no timer at all.
200
+ */
201
+ armSweep() {
202
+ if (this.sweepTimer || this.closed) {
203
+ return;
204
+ }
205
+ const idleMs = this.options.idleMs ?? DEFAULT_IDLE_MS;
206
+ const interval = Math.max(1_000, Math.min(idleMs, MAX_SWEEP_INTERVAL_MS));
207
+ this.sweepTimer = setInterval(() => {
208
+ void this.sweepIdle(Date.now()).catch((error) => this.options.onError?.(error));
209
+ }, interval);
210
+ this.sweepTimer.unref?.();
211
+ }
212
+ disarmSweep() {
213
+ if (this.sweepTimer) {
214
+ clearInterval(this.sweepTimer);
215
+ this.sweepTimer = undefined;
216
+ }
217
+ }
218
+ async closeBrowser(entry) {
219
+ this.browsers.delete(entry.key);
220
+ try {
221
+ await entry.browser.close();
222
+ }
223
+ catch {
224
+ // Same as a context: already gone is fine.
225
+ }
226
+ await entry.proxy.close();
227
+ if (this.browsers.size === 0) {
228
+ this.disarmSweep();
229
+ }
230
+ }
231
+ /** Everything, for shutdown. */
232
+ async close() {
233
+ this.closed = true;
234
+ this.disarmSweep();
235
+ for (const entry of [...this.contexts.values()]) {
236
+ await this.drop(entry, 'shutdown');
237
+ }
238
+ // Anything still open had no contexts to take it down — a browser that
239
+ // launched for a call that then failed, for instance.
240
+ for (const entry of [...this.browsers.values()]) {
241
+ await this.closeBrowser(entry);
242
+ }
243
+ }
244
+ /** How many conversations currently hold a page. */
245
+ get size() {
246
+ return this.contexts.size;
247
+ }
248
+ /** How many browsers are running — one per distinct address policy. */
249
+ get browserCount() {
250
+ return this.browsers.size;
251
+ }
252
+ }
253
+ //# sourceMappingURL=sessions.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sessions.js","sourceRoot":"","sources":["../src/sessions.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,118 @@
1
+ {
2
+ "name": "@stratusagent/tool-browser",
3
+ "version": "0.6.0",
4
+ "type": "module",
5
+ "main": "./dist/index.js",
6
+ "types": "./dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "import": "./dist/index.js"
11
+ },
12
+ "./package.json": "./package.json"
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "engines": {
18
+ "node": ">=22.13 <23 || >=23.4"
19
+ },
20
+ "dependencies": {
21
+ "playwright-core": "^1.49.0",
22
+ "@stratusagent/core": "0.6.0",
23
+ "@stratusagent/egress": "0.6.0",
24
+ "@stratusagent/plugins": "0.6.0"
25
+ },
26
+ "devDependencies": {
27
+ "@types/node": "^24.6.0",
28
+ "typescript": "^5.9.3",
29
+ "@stratusagent/tool-web": "0.6.0"
30
+ },
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/stratuslabs/agent.git",
35
+ "directory": "packages/tool-browser"
36
+ },
37
+ "description": "Browser tools for Stratus agents: navigate, read, screenshot, and act, with every request pinned through a policy-owned proxy",
38
+ "keywords": [
39
+ "stratus",
40
+ "agent",
41
+ "ai",
42
+ "llm",
43
+ "stratus-plugin",
44
+ "tools",
45
+ "browser",
46
+ "playwright"
47
+ ],
48
+ "publishConfig": {
49
+ "access": "public"
50
+ },
51
+ "stratus": {
52
+ "pluginVersion": 1,
53
+ "contributes": {
54
+ "tools": [
55
+ {
56
+ "name": "browser.goto",
57
+ "risk": "gated"
58
+ },
59
+ {
60
+ "name": "browser.read",
61
+ "risk": "gated"
62
+ },
63
+ {
64
+ "name": "browser.screenshot",
65
+ "risk": "gated"
66
+ },
67
+ {
68
+ "name": "browser.act",
69
+ "risk": "dangerous"
70
+ }
71
+ ]
72
+ },
73
+ "config": {
74
+ "type": "object",
75
+ "properties": {
76
+ "allowPrivateAddresses": {
77
+ "type": "boolean"
78
+ },
79
+ "allowedHosts": {
80
+ "type": "array",
81
+ "items": {
82
+ "type": "string"
83
+ }
84
+ },
85
+ "headless": {
86
+ "type": "boolean"
87
+ },
88
+ "executablePath": {
89
+ "type": "string"
90
+ },
91
+ "channel": {
92
+ "type": "string"
93
+ },
94
+ "idleMs": {
95
+ "type": "integer"
96
+ },
97
+ "maxContexts": {
98
+ "type": "integer"
99
+ },
100
+ "maxTextBytes": {
101
+ "type": "integer"
102
+ },
103
+ "navigationTimeoutMs": {
104
+ "type": "integer"
105
+ },
106
+ "workspaceRoot": {
107
+ "type": "string"
108
+ }
109
+ },
110
+ "additionalProperties": false
111
+ }
112
+ },
113
+ "scripts": {
114
+ "build": "tsc -b",
115
+ "typecheck": "tsc --noEmit --project tsconfig.test.json",
116
+ "test": "node --test --experimental-strip-types"
117
+ }
118
+ }