@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 +21 -0
- package/README.md +127 -0
- package/dist/driver.d.ts +82 -0
- package/dist/driver.d.ts.map +1 -0
- package/dist/driver.js +37 -0
- package/dist/driver.js.map +1 -0
- package/dist/index.d.ts +40 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +293 -0
- package/dist/index.js.map +1 -0
- package/dist/sessions.d.ts +95 -0
- package/dist/sessions.d.ts.map +1 -0
- package/dist/sessions.js +253 -0
- package/dist/sessions.js.map +1 -0
- package/package.json +118 -0
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.
|
package/dist/driver.d.ts
ADDED
|
@@ -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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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"}
|
package/dist/sessions.js
ADDED
|
@@ -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
|
+
}
|