@vostride/agent-qa-plugin-steel 0.0.1 → 0.3.2
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.md +105 -0
- package/NOTICE.md +11 -0
- package/README.md +193 -0
- package/dist/main.js +7 -0
- package/dist/main.js.map +1 -0
- package/dist/options.schema.json +18 -0
- package/dist/plugin-C8AoyB8z.js +256 -0
- package/dist/plugin-C8AoyB8z.js.map +1 -0
- package/dist/plugin.js +1 -0
- package/dist/settings.schema.json +105 -0
- package/package.json +68 -3
- package/skill/SKILL.md +106 -0
package/LICENSE.md
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# Functional Source License, Version 1.1, ALv2 Future License
|
|
2
|
+
|
|
3
|
+
## Abbreviation
|
|
4
|
+
|
|
5
|
+
FSL-1.1-ALv2
|
|
6
|
+
|
|
7
|
+
## Notice
|
|
8
|
+
|
|
9
|
+
Copyright 2026 Pranshu Chittora
|
|
10
|
+
|
|
11
|
+
## Terms and Conditions
|
|
12
|
+
|
|
13
|
+
### Licensor ("We")
|
|
14
|
+
|
|
15
|
+
The party offering the Software under these Terms and Conditions.
|
|
16
|
+
|
|
17
|
+
### The Software
|
|
18
|
+
|
|
19
|
+
The "Software" is each version of the software that we make available under
|
|
20
|
+
these Terms and Conditions, as indicated by our inclusion of these Terms and
|
|
21
|
+
Conditions with the Software.
|
|
22
|
+
|
|
23
|
+
### License Grant
|
|
24
|
+
|
|
25
|
+
Subject to your compliance with this License Grant and the Patents,
|
|
26
|
+
Redistribution and Trademark clauses below, we hereby grant you the right to
|
|
27
|
+
use, copy, modify, create derivative works, publicly perform, publicly display
|
|
28
|
+
and redistribute the Software for any Permitted Purpose identified below.
|
|
29
|
+
|
|
30
|
+
### Permitted Purpose
|
|
31
|
+
|
|
32
|
+
A Permitted Purpose is any purpose other than a Competing Use. A Competing Use
|
|
33
|
+
means making the Software available to others in a commercial product or
|
|
34
|
+
service that:
|
|
35
|
+
|
|
36
|
+
1. substitutes for the Software;
|
|
37
|
+
|
|
38
|
+
2. substitutes for any other product or service we offer using the Software
|
|
39
|
+
that exists as of the date we make the Software available; or
|
|
40
|
+
|
|
41
|
+
3. offers the same or substantially similar functionality as the Software.
|
|
42
|
+
|
|
43
|
+
Permitted Purposes specifically include using the Software:
|
|
44
|
+
|
|
45
|
+
1. for your internal use and access;
|
|
46
|
+
|
|
47
|
+
2. for non-commercial education;
|
|
48
|
+
|
|
49
|
+
3. for non-commercial research; and
|
|
50
|
+
|
|
51
|
+
4. in connection with professional services that you provide to a licensee
|
|
52
|
+
using the Software in accordance with these Terms and Conditions.
|
|
53
|
+
|
|
54
|
+
### Patents
|
|
55
|
+
|
|
56
|
+
To the extent your use for a Permitted Purpose would necessarily infringe our
|
|
57
|
+
patents, the license grant above includes a license under our patents. If you
|
|
58
|
+
make a claim against any party that the Software infringes or contributes to
|
|
59
|
+
the infringement of any patent, then your patent license to the Software ends
|
|
60
|
+
immediately.
|
|
61
|
+
|
|
62
|
+
### Redistribution
|
|
63
|
+
|
|
64
|
+
The Terms and Conditions apply to all copies, modifications and derivatives of
|
|
65
|
+
the Software.
|
|
66
|
+
|
|
67
|
+
If you redistribute any copies, modifications or derivatives of the Software,
|
|
68
|
+
you must include a copy of or a link to these Terms and Conditions and not
|
|
69
|
+
remove any copyright notices provided in or with the Software.
|
|
70
|
+
|
|
71
|
+
### Disclaimer
|
|
72
|
+
|
|
73
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND WITHOUT WARRANTIES OF ANY KIND, EXPRESS OR
|
|
74
|
+
IMPLIED, INCLUDING WITHOUT LIMITATION WARRANTIES OF FITNESS FOR A PARTICULAR
|
|
75
|
+
PURPOSE, MERCHANTABILITY, TITLE OR NON-INFRINGEMENT.
|
|
76
|
+
|
|
77
|
+
IN NO EVENT WILL WE HAVE ANY LIABILITY TO YOU ARISING OUT OF OR RELATED TO THE
|
|
78
|
+
SOFTWARE, INCLUDING INDIRECT, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES,
|
|
79
|
+
EVEN IF WE HAVE BEEN INFORMED OF THEIR POSSIBILITY IN ADVANCE.
|
|
80
|
+
|
|
81
|
+
### Trademarks
|
|
82
|
+
|
|
83
|
+
Except for displaying the License Details and identifying us as the origin of
|
|
84
|
+
the Software, you have no right under these Terms and Conditions to use our
|
|
85
|
+
trademarks, trade names, service marks or product names.
|
|
86
|
+
|
|
87
|
+
## Grant of Future License
|
|
88
|
+
|
|
89
|
+
We hereby irrevocably grant you an additional license to use the Software under
|
|
90
|
+
the Apache License, Version 2.0 that is effective on the second anniversary of
|
|
91
|
+
the date we make the Software available. On or after that date, you may use the
|
|
92
|
+
Software under the Apache License, Version 2.0, in which case the following
|
|
93
|
+
will apply:
|
|
94
|
+
|
|
95
|
+
Licensed under the Apache License, Version 2.0 (the "License"); you may not use
|
|
96
|
+
this file except in compliance with the License.
|
|
97
|
+
|
|
98
|
+
You may obtain a copy of the License at
|
|
99
|
+
|
|
100
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
101
|
+
|
|
102
|
+
Unless required by applicable law or agreed to in writing, software distributed
|
|
103
|
+
under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR
|
|
104
|
+
CONDITIONS OF ANY KIND, either express or implied. See the License for the
|
|
105
|
+
specific language governing permissions and limitations under the License.
|
package/NOTICE.md
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# agent-qa Notice
|
|
2
|
+
|
|
3
|
+
agent-qa is provided by Pranshu Chittora.
|
|
4
|
+
|
|
5
|
+
Copyright 2026 Pranshu Chittora
|
|
6
|
+
|
|
7
|
+
agent-qa is licensed under the Functional Source License, Version 1.1, ALv2 Future License (FSL-1.1-ALv2). See `LICENSE.md` for the full license terms.
|
|
8
|
+
|
|
9
|
+
Under the Grant of Future License in `LICENSE.md`, each version of the Software is also granted under the Apache License, Version 2.0 effective on the second anniversary of the date that version was made available.
|
|
10
|
+
|
|
11
|
+
Third-party dependencies are distributed under their own license terms. This notice covers agent-qa itself and does not replace or modify notices required by third-party dependency owners.
|
package/README.md
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# @vostride/agent-qa-plugin-steel
|
|
2
|
+
|
|
3
|
+
The agent-qa plugin for [Steel](https://steel.dev) cloud browsers. Before a web test connects, it
|
|
4
|
+
creates a Steel session and answers agent-qa with the session's CDP URL, and after the test it
|
|
5
|
+
releases the session. Steel runs Chromium only.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
Install the plugin at exactly the version of agent-qa the project runs:
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
npm install --save-dev --save-exact @vostride/agent-qa-plugin-steel@<agent-qa version>
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
`agent-qa plugins list` prints this line, with the version filled in, when the package is missing
|
|
16
|
+
or its version differs from agent-qa's.
|
|
17
|
+
|
|
18
|
+
## Configure
|
|
19
|
+
|
|
20
|
+
```yaml
|
|
21
|
+
plugins:
|
|
22
|
+
- name: steel
|
|
23
|
+
package: "@vostride/agent-qa-plugin-steel"
|
|
24
|
+
options:
|
|
25
|
+
apiKey: "{{secret:STEEL_API_KEY}}"
|
|
26
|
+
|
|
27
|
+
registry:
|
|
28
|
+
remotes:
|
|
29
|
+
- name: steel
|
|
30
|
+
plugin: steel
|
|
31
|
+
settings:
|
|
32
|
+
idleMinutes: 5
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
A test runs there with `use.browser.remote: steel` in the config, a suite or the test.
|
|
36
|
+
|
|
37
|
+
### Options
|
|
38
|
+
|
|
39
|
+
Options belong to the `plugins` entry and describe the account.
|
|
40
|
+
|
|
41
|
+
| Option | Default | Meaning |
|
|
42
|
+
| -------- | ----------------------- | --------------------------------------------------------------------------------------------------------------------------- |
|
|
43
|
+
| `apiKey` | - | The Steel API key, a credential. Write it as a `{{secret:<name>}}` reference. Leave it out for a self-hosted steel-browser. |
|
|
44
|
+
| `apiUrl` | `https://api.steel.dev` | Steel's API. It must use `https://` with no user or password, unless it points at this machine. |
|
|
45
|
+
|
|
46
|
+
The key is sent as the `steel-api-key` header and in the connect URL agent-qa receives, which
|
|
47
|
+
agent-qa masks. It never appears in a message, a log or a check line.
|
|
48
|
+
|
|
49
|
+
### Settings
|
|
50
|
+
|
|
51
|
+
Settings belong to each remote and describe one place.
|
|
52
|
+
|
|
53
|
+
| Setting | Default | Meaning |
|
|
54
|
+
| ---------------- | ------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
55
|
+
| `timeoutMinutes` | `1440` | Minutes before Steel ends the session (1 to 1440), sent as `timeout` in milliseconds. Your Steel plan may cap it lower. |
|
|
56
|
+
| `idleMinutes` | `5` | Minutes with no CDP command before Steel ends the session (1 to 60), sent as `inactivityTimeout` in milliseconds. |
|
|
57
|
+
| `solveCaptcha` | `false` | Steel's CAPTCHA solving, sent as `solveCaptcha`. |
|
|
58
|
+
| `blockAds` | `false` | Steel's ad blocking, sent as `blockAds`. |
|
|
59
|
+
| `fingerprint` | `false` | Steel's browser fingerprint injection, sent as `stealthConfig.skipFingerprintInjection`, the opposite value. |
|
|
60
|
+
| `proxy` | `false` | Steel's proxy: `true`, or Steel's proxy object. Sent as `useProxy`. |
|
|
61
|
+
| `proxyUrl` | - | Your own proxy, `http(s)://user:password@host:port`, a credential. Sent as `proxyUrl` instead of Steel's proxy. |
|
|
62
|
+
| `dimensions` | - | The browser window, `{ width, height }` in pixels, sent as `dimensions`. |
|
|
63
|
+
| `region` | - | The Steel region, for example `us-east`, sent as `region`. |
|
|
64
|
+
| `profile` | - | A Steel profile id, loaded read-only and sent as `profileId`. Needs `context: reuse`. |
|
|
65
|
+
| `extensions` | - | Steel extension ids loaded into the browser, sent as `extensionIds`. |
|
|
66
|
+
| `extra` | - | Any other Steel request field, merged last. |
|
|
67
|
+
|
|
68
|
+
`agent-qa plugins describe steel` prints the same tables from the schemas the package ships, and
|
|
69
|
+
`--json` prints them as JSON.
|
|
70
|
+
|
|
71
|
+
## How the request body is built
|
|
72
|
+
|
|
73
|
+
The create request (`POST /v1/sessions`) is built in this order, each part overriding the one
|
|
74
|
+
before:
|
|
75
|
+
|
|
76
|
+
1. `sessionId`, a UUID the plugin chooses, so it can release the session even when the create
|
|
77
|
+
answer never arrives;
|
|
78
|
+
2. the lifetimes, `timeout` and `inactivityTimeout`, from `timeoutMinutes` and `idleMinutes`;
|
|
79
|
+
3. the features that change the page: CAPTCHA solving, ad blocking, Steel's fingerprint injection
|
|
80
|
+
and Steel's proxies. Each is sent off unless a setting turns it on, because a test should see
|
|
81
|
+
the app the way its users do. Steel injects fingerprints unless it is told not to, so the plugin
|
|
82
|
+
always tells it, with `stealthConfig: { skipFingerprintInjection: true }` by default;
|
|
83
|
+
4. the other settings above, in Steel's field names. With `proxyUrl` the plugin sends your proxy
|
|
84
|
+
and no `useProxy`. A remote that sets both `proxyUrl` and `proxy` is refused with path
|
|
85
|
+
`settings.proxyUrl`;
|
|
86
|
+
5. `extra`, last, so a pass-through key overrides both a default and a setting. It cannot set
|
|
87
|
+
`sessionId`, which the plugin releases the session by; a remote whose `extra` sets it is refused
|
|
88
|
+
with path `settings.extra.sessionId`.
|
|
89
|
+
|
|
90
|
+
For example, `settings: { extra: { solveCaptcha: true, headless: true } }` turns CAPTCHA solving on
|
|
91
|
+
and asks for a headless browser.
|
|
92
|
+
|
|
93
|
+
## Lifetime
|
|
94
|
+
|
|
95
|
+
By default the plugin asks for a 1440-minute session that ends after 5 idle minutes.
|
|
96
|
+
|
|
97
|
+
- Steel's plans cap the session lifetime: Launch at 15 minutes, Scale at 60 minutes and Enterprise
|
|
98
|
+
at up to 1440 minutes. When Steel refuses the create with `400` or `422` while more than 15
|
|
99
|
+
minutes were asked for, and its reason mentions a timeout, lifetime, duration, minutes, plan or
|
|
100
|
+
limit, or gives no reason, the failure line ends
|
|
101
|
+
`if your Steel plan caps sessions below <n> minutes, set settings.timeoutMinutes to the cap`.
|
|
102
|
+
Set `timeoutMinutes` to your plan's cap. A refusal whose reason is about another field does not
|
|
103
|
+
name `timeoutMinutes`.
|
|
104
|
+
- When Steel grants a shorter session than was asked for, the plugin writes
|
|
105
|
+
`Steel granted a <n>-minute session; <m> minutes were asked for` to its log.
|
|
106
|
+
- Any CDP command resets the idle time. agent-qa's heartbeat keeps a running test from going idle.
|
|
107
|
+
- A session left behind by a killed job, where agent-qa and the plugin die together, ends within
|
|
108
|
+
the idle time, so it stops billing within minutes.
|
|
109
|
+
|
|
110
|
+
agent-qa releases every session itself after the test; the lifetimes only bound a session that
|
|
111
|
+
agent-qa could not release.
|
|
112
|
+
|
|
113
|
+
## Recording
|
|
114
|
+
|
|
115
|
+
Steel records every session and keeps the replay on its dashboard, behind its login. agent-qa's
|
|
116
|
+
masking does not reach that replay, so a secret a test types may show in it. Steel offers no switch
|
|
117
|
+
to turn recording off, so the plugin has no `recording` setting, and a remote that sets one is
|
|
118
|
+
refused.
|
|
119
|
+
|
|
120
|
+
## Profiles
|
|
121
|
+
|
|
122
|
+
`profile` loads a Steel profile by id, and the plugin never asks Steel to persist it, so nothing a
|
|
123
|
+
test does is saved back. A profile lives in the browser's first context, so the remote needs
|
|
124
|
+
`context: reuse`. Without it, the remote is refused with kind `refused` and path
|
|
125
|
+
`settings.profile`.
|
|
126
|
+
|
|
127
|
+
## Self-hosted steel-browser
|
|
128
|
+
|
|
129
|
+
The plugin also drives a self-hosted [steel-browser](https://github.com/steel-dev/steel-browser):
|
|
130
|
+
|
|
131
|
+
```yaml
|
|
132
|
+
plugins:
|
|
133
|
+
- name: steel-local
|
|
134
|
+
package: "@vostride/agent-qa-plugin-steel"
|
|
135
|
+
options:
|
|
136
|
+
apiUrl: "http://localhost:3000"
|
|
137
|
+
|
|
138
|
+
registry:
|
|
139
|
+
remotes:
|
|
140
|
+
- name: steel-local
|
|
141
|
+
plugin: steel-local
|
|
142
|
+
|
|
143
|
+
use:
|
|
144
|
+
workers: 1
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
- Point `apiUrl` at it. Plain `http://` is allowed only for this machine.
|
|
148
|
+
- Leave out `apiKey`; the plugin then sends no key.
|
|
149
|
+
- Set `use.workers: 1`. A steel-browser serves one session at a time, and releasing a session ends
|
|
150
|
+
whatever session is active, so two tests at once would end each other's browser.
|
|
151
|
+
- Its session page is plain `http://`, so the plugin returns no link for it.
|
|
152
|
+
|
|
153
|
+
## Failures
|
|
154
|
+
|
|
155
|
+
| Steel answer | Kind |
|
|
156
|
+
| --------------------- | ------------- |
|
|
157
|
+
| no answer | `unreachable` |
|
|
158
|
+
| `401` | `auth` |
|
|
159
|
+
| `429` | `capacity` |
|
|
160
|
+
| any other non-success | `refused` |
|
|
161
|
+
|
|
162
|
+
Each failure is one line naming the status, the request and Steel's message, with `Retry-After` on
|
|
163
|
+
a `429`. A create refused with `400` or `422` may also name `settings.timeoutMinutes`, as above.
|
|
164
|
+
|
|
165
|
+
A create Steel refuses with a `4xx` made no session, so it carries no handle and nothing is
|
|
166
|
+
released. Every other create failure (no answer, a `5xx`, or an answer the plugin cannot read)
|
|
167
|
+
carries the session's id, so agent-qa releases whatever Steel may have made.
|
|
168
|
+
|
|
169
|
+
On release, `2xx` and `404` both count as released. On any other answer the plugin reads the
|
|
170
|
+
session back, and counts it released when Steel no longer has it or no longer reports it live.
|
|
171
|
+
|
|
172
|
+
## Links and the debug URL
|
|
173
|
+
|
|
174
|
+
The session link is the session's page in Steel's dashboard, which sits behind Steel's login and
|
|
175
|
+
shows the replay. Steel's debug URL lets anyone who has it watch and drive the session, so it is a
|
|
176
|
+
credential: the plugin adds it, with the connect URL, to the values agent-qa masks, and never
|
|
177
|
+
returns it as the link.
|
|
178
|
+
|
|
179
|
+
## Account check
|
|
180
|
+
|
|
181
|
+
`agent-qa doctor` runs the plugin's `check`, which lists sessions with `GET /v1/sessions` and opens
|
|
182
|
+
none:
|
|
183
|
+
|
|
184
|
+
- An accepted key reads `the API key was accepted`.
|
|
185
|
+
- Without `apiKey`, a steel-browser that answers reads `Steel answered without an API key`, and a
|
|
186
|
+
Steel that asks for a key warns `Steel asked for an API key; set options.apiKey`.
|
|
187
|
+
- A refused key warns `the API key was refused`.
|
|
188
|
+
- Any other failure warns with its one line, for example
|
|
189
|
+
`Steel could not be reached at <apiUrl>`.
|
|
190
|
+
|
|
191
|
+
## License
|
|
192
|
+
|
|
193
|
+
FSL-1.1-ALv2. See `LICENSE.md` and `NOTICE.md` in the package.
|
package/dist/main.js
ADDED
package/dist/main.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"main.js","names":["plugin"],"sources":["../src/main.ts"],"sourcesContent":["import { runPlugin } from '@vostride/agent-qa-plugin-kit'\nimport plugin from './plugin.js'\n\nrunPlugin(plugin)\n"],"mappings":";;;AAGA,UAAUA,cAAM"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"properties": {
|
|
5
|
+
"apiKey": {
|
|
6
|
+
"type": "string",
|
|
7
|
+
"credential": true,
|
|
8
|
+
"description": "The Steel API key; leave it out for a self-hosted steel-browser"
|
|
9
|
+
},
|
|
10
|
+
"apiUrl": {
|
|
11
|
+
"default": "https://api.steel.dev",
|
|
12
|
+
"description": "Steel's API, https:// unless it points at this machine",
|
|
13
|
+
"type": "string",
|
|
14
|
+
"format": "uri"
|
|
15
|
+
}
|
|
16
|
+
},
|
|
17
|
+
"additionalProperties": false
|
|
18
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { PluginError, checkEndpoints, createVendorApi, credential, definePlugin, z } from "@vostride/agent-qa-plugin-kit";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
// src/steel-api.ts
|
|
4
|
+
const SteelSessionSchema = z.object({
|
|
5
|
+
id: z.string().min(1),
|
|
6
|
+
websocketUrl: z.string().min(1),
|
|
7
|
+
debugUrl: z.string().min(1).optional(),
|
|
8
|
+
sessionViewerUrl: z.string().min(1).optional(),
|
|
9
|
+
timeout: z.number().optional()
|
|
10
|
+
});
|
|
11
|
+
const SessionStatusSchema = z.object({ status: z.string() });
|
|
12
|
+
var SteelCreateFailure = class extends PluginError {
|
|
13
|
+
status;
|
|
14
|
+
constructor(failure, status) {
|
|
15
|
+
super(failure.message, { kind: failure.kind });
|
|
16
|
+
this.status = status;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
function createSteelApi(input) {
|
|
20
|
+
const api = createVendorApi({
|
|
21
|
+
vendor: "Steel",
|
|
22
|
+
baseUrl: input.apiUrl,
|
|
23
|
+
headers: input.apiKey === void 0 ? {} : { "steel-api-key": input.apiKey }
|
|
24
|
+
});
|
|
25
|
+
const sessionPath = (id) => `v1/sessions/${encodeURIComponent(id)}`;
|
|
26
|
+
async function session(id, signal) {
|
|
27
|
+
const response = await api.send("GET", sessionPath(id), {
|
|
28
|
+
headers: { accept: "application/json" },
|
|
29
|
+
signal
|
|
30
|
+
});
|
|
31
|
+
if (response.status === 404) {
|
|
32
|
+
await response.body?.cancel();
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
if (!response.ok) throw await api.failure(response, "GET /v1/sessions/{id}");
|
|
36
|
+
return api.json(response);
|
|
37
|
+
}
|
|
38
|
+
async function released(id, signal) {
|
|
39
|
+
let current;
|
|
40
|
+
try {
|
|
41
|
+
current = await session(id, signal);
|
|
42
|
+
} catch {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
if (current === void 0) return true;
|
|
46
|
+
const parsed = SessionStatusSchema.safeParse(current);
|
|
47
|
+
return parsed.success && parsed.data.status !== "live";
|
|
48
|
+
}
|
|
49
|
+
return {
|
|
50
|
+
async createSession(body) {
|
|
51
|
+
const response = await api.send("POST", "v1/sessions", {
|
|
52
|
+
headers: {
|
|
53
|
+
"content-type": "application/json",
|
|
54
|
+
accept: "application/json"
|
|
55
|
+
},
|
|
56
|
+
body: JSON.stringify(body)
|
|
57
|
+
});
|
|
58
|
+
if (!response.ok) throw new SteelCreateFailure(await api.failure(response, "POST /v1/sessions"), response.status);
|
|
59
|
+
return api.json(response);
|
|
60
|
+
},
|
|
61
|
+
async release(id, signal) {
|
|
62
|
+
const response = await api.send("POST", `${sessionPath(id)}/release`, {
|
|
63
|
+
headers: { accept: "application/json" },
|
|
64
|
+
signal
|
|
65
|
+
});
|
|
66
|
+
if (response.ok || response.status === 404) {
|
|
67
|
+
await response.body?.cancel();
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
const failure = await api.failure(response, "POST /v1/sessions/{id}/release");
|
|
71
|
+
if (await released(id, signal)) return;
|
|
72
|
+
throw failure;
|
|
73
|
+
},
|
|
74
|
+
session,
|
|
75
|
+
async listSessions(signal) {
|
|
76
|
+
const response = await api.send("GET", "v1/sessions?limit=1", {
|
|
77
|
+
headers: { accept: "application/json" },
|
|
78
|
+
signal
|
|
79
|
+
});
|
|
80
|
+
if (!response.ok) throw await api.failure(response, "GET /v1/sessions");
|
|
81
|
+
await response.body?.cancel();
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
// src/plugin.ts
|
|
86
|
+
const STEEL_API_URL = "https://api.steel.dev";
|
|
87
|
+
const MINUTE_MS = 6e4;
|
|
88
|
+
const LIFETIME_REFUSALS = [400, 422];
|
|
89
|
+
const SMALLEST_PLAN_CAP_MINUTES = 15;
|
|
90
|
+
const LIFETIME_WORDS = /timeout|lifetime|duration|minute|plan|limit/i;
|
|
91
|
+
const options = z.strictObject({
|
|
92
|
+
apiKey: credential("The Steel API key; leave it out for a self-hosted steel-browser").optional(),
|
|
93
|
+
apiUrl: z.url().default(STEEL_API_URL).describe("Steel's API, https:// unless it points at this machine")
|
|
94
|
+
});
|
|
95
|
+
const settings = z.strictObject({
|
|
96
|
+
timeoutMinutes: z.int().min(1).max(1440).default(1440).describe("Minutes before Steel ends the session; your Steel plan may cap it lower"),
|
|
97
|
+
idleMinutes: z.int().min(1).max(60).default(5).describe("Minutes with no CDP command before Steel ends the session"),
|
|
98
|
+
solveCaptcha: z.boolean().default(false).describe("Steel's CAPTCHA solving"),
|
|
99
|
+
blockAds: z.boolean().default(false).describe("Steel's ad blocking"),
|
|
100
|
+
fingerprint: z.boolean().default(false).describe("Steel's browser fingerprint injection, which changes what pages see"),
|
|
101
|
+
proxy: z.union([z.boolean(), z.record(z.string(), z.unknown())]).default(false).describe("Steel's proxy, true or Steel's proxy object, sent as useProxy"),
|
|
102
|
+
proxyUrl: credential("Your own proxy, http(s)://user:password@host:port, sent instead of Steel's").optional(),
|
|
103
|
+
dimensions: z.strictObject({
|
|
104
|
+
width: z.int().positive(),
|
|
105
|
+
height: z.int().positive()
|
|
106
|
+
}).optional().describe("The browser window size"),
|
|
107
|
+
region: z.string().min(1).optional().describe("The Steel region, for example us-east"),
|
|
108
|
+
profile: z.string().min(1).optional().describe("A Steel profile id loaded read-only; needs context: reuse"),
|
|
109
|
+
extensions: z.array(z.string().min(1)).min(1).optional().describe("Steel extension ids loaded into the browser"),
|
|
110
|
+
extra: z.record(z.string(), z.unknown()).optional().describe("Fields merged last into the session create request, overriding the rest")
|
|
111
|
+
});
|
|
112
|
+
function checkSettings(input, session) {
|
|
113
|
+
if (input.profile !== void 0 && (session.platform !== "web" || session.context !== "reuse")) throw new PluginError("profile loads only into a reused context; set context: reuse on the remote.", {
|
|
114
|
+
kind: "refused",
|
|
115
|
+
path: "settings.profile"
|
|
116
|
+
});
|
|
117
|
+
if (input.extra !== void 0 && Object.hasOwn(input.extra, "sessionId")) throw new PluginError("extra cannot set sessionId; the plugin chooses it to release the session.", {
|
|
118
|
+
kind: "refused",
|
|
119
|
+
path: "settings.extra.sessionId"
|
|
120
|
+
});
|
|
121
|
+
if (input.proxyUrl !== void 0 && input.proxy !== false) throw new PluginError("proxy and proxyUrl both choose a proxy; set one of them.", {
|
|
122
|
+
kind: "refused",
|
|
123
|
+
path: "settings.proxyUrl"
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
function createBody(sessionId, input) {
|
|
127
|
+
return {
|
|
128
|
+
sessionId,
|
|
129
|
+
timeout: input.timeoutMinutes * MINUTE_MS,
|
|
130
|
+
inactivityTimeout: input.idleMinutes * MINUTE_MS,
|
|
131
|
+
solveCaptcha: input.solveCaptcha,
|
|
132
|
+
blockAds: input.blockAds,
|
|
133
|
+
...input.proxyUrl === void 0 ? { useProxy: input.proxy } : {},
|
|
134
|
+
stealthConfig: { skipFingerprintInjection: !input.fingerprint },
|
|
135
|
+
...input.proxyUrl === void 0 ? {} : { proxyUrl: input.proxyUrl },
|
|
136
|
+
...input.dimensions === void 0 ? {} : { dimensions: input.dimensions },
|
|
137
|
+
...input.region === void 0 ? {} : { region: input.region },
|
|
138
|
+
...input.profile === void 0 ? {} : { profileId: input.profile },
|
|
139
|
+
...input.extensions === void 0 ? {} : { extensionIds: input.extensions },
|
|
140
|
+
...input.extra
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
function mayHaveCreated(error) {
|
|
144
|
+
return !(error instanceof SteelCreateFailure) || error.status >= 500;
|
|
145
|
+
}
|
|
146
|
+
function lifetimeMayBeRefused(error, timeoutMinutes) {
|
|
147
|
+
if (!(error instanceof SteelCreateFailure) || !LIFETIME_REFUSALS.includes(error.status)) return false;
|
|
148
|
+
if (timeoutMinutes <= SMALLEST_PLAN_CAP_MINUTES) return false;
|
|
149
|
+
const reason = error.message.split(": ").slice(1).join(": ");
|
|
150
|
+
return reason === "" || LIFETIME_WORDS.test(reason);
|
|
151
|
+
}
|
|
152
|
+
function createFailure(error, sessionId, timeoutMinutes) {
|
|
153
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
154
|
+
const kind = error instanceof PluginError ? error.kind : "refused";
|
|
155
|
+
const text = lifetimeMayBeRefused(error, timeoutMinutes) ? `${message.replace(/[\s.;,]+$/, "")}; if your Steel plan caps sessions below ${String(timeoutMinutes)} minutes, set settings.timeoutMinutes to the cap` : message;
|
|
156
|
+
return new PluginError(text, {
|
|
157
|
+
kind,
|
|
158
|
+
...mayHaveCreated(error) ? { handle: { sessionId } } : {}
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
function minutesOf(ms) {
|
|
162
|
+
return String(Number((ms / MINUTE_MS).toFixed(1)));
|
|
163
|
+
}
|
|
164
|
+
function noteGrantedLifetime(granted, asked) {
|
|
165
|
+
if (granted === void 0 || typeof asked !== "number" || granted >= asked) return;
|
|
166
|
+
process.stderr.write(`Steel granted a ${minutesOf(granted)}-minute session; ${minutesOf(asked)} minutes were asked for\n`);
|
|
167
|
+
}
|
|
168
|
+
function viewerLink(url) {
|
|
169
|
+
if (url === void 0) return void 0;
|
|
170
|
+
return URL.parse(url)?.protocol === "https:" ? url : void 0;
|
|
171
|
+
}
|
|
172
|
+
const HandleSchema = z.object({ sessionId: z.string().min(1) });
|
|
173
|
+
function hasId(answer) {
|
|
174
|
+
if (typeof answer !== "object" || answer === null || !("id" in answer)) return false;
|
|
175
|
+
return typeof answer.id === "string" && answer.id !== "";
|
|
176
|
+
}
|
|
177
|
+
function refusedKey(error) {
|
|
178
|
+
return error instanceof PluginError && error.kind === "auth";
|
|
179
|
+
}
|
|
180
|
+
function failureLine(error) {
|
|
181
|
+
return error instanceof Error ? error.message : String(error);
|
|
182
|
+
}
|
|
183
|
+
var plugin_default = definePlugin({
|
|
184
|
+
options,
|
|
185
|
+
settings,
|
|
186
|
+
serves: ["cdp"],
|
|
187
|
+
skill: "./skill",
|
|
188
|
+
initialize({ options }) {
|
|
189
|
+
checkEndpoints(options, [{
|
|
190
|
+
path: "apiUrl",
|
|
191
|
+
scheme: "https"
|
|
192
|
+
}]);
|
|
193
|
+
},
|
|
194
|
+
async check({ options }, ctx) {
|
|
195
|
+
try {
|
|
196
|
+
await createSteelApi(options).listSessions(ctx.signal);
|
|
197
|
+
} catch (error) {
|
|
198
|
+
if (!refusedKey(error)) return {
|
|
199
|
+
state: "warn",
|
|
200
|
+
lines: [failureLine(error)]
|
|
201
|
+
};
|
|
202
|
+
return {
|
|
203
|
+
state: "warn",
|
|
204
|
+
lines: [options.apiKey === void 0 ? "Steel asked for an API key; set options.apiKey" : "the API key was refused"]
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
state: "ok",
|
|
209
|
+
lines: [options.apiKey === void 0 ? "Steel answered without an API key" : "the API key was accepted"]
|
|
210
|
+
};
|
|
211
|
+
},
|
|
212
|
+
async acquire({ options, settings, session }) {
|
|
213
|
+
if (session.platform !== "web" || session.engine !== "chromium") {
|
|
214
|
+
const what = session.platform === "web" ? session.engine : session.platform;
|
|
215
|
+
throw new PluginError(`Steel runs Chromium web sessions only, not ${what}.`, { kind: "refused" });
|
|
216
|
+
}
|
|
217
|
+
checkSettings(settings, session);
|
|
218
|
+
const api = createSteelApi(options);
|
|
219
|
+
const sessionId = randomUUID();
|
|
220
|
+
const body = createBody(sessionId, settings);
|
|
221
|
+
let created;
|
|
222
|
+
try {
|
|
223
|
+
created = await api.createSession(body);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
throw createFailure(error, sessionId, settings.timeoutMinutes);
|
|
226
|
+
}
|
|
227
|
+
const answer = SteelSessionSchema.safeParse(created);
|
|
228
|
+
const url = answer.success ? URL.parse(answer.data.websocketUrl) : null;
|
|
229
|
+
if (!answer.success || url === null) {
|
|
230
|
+
const missing = hasId(created) ? "a usable websocketUrl" : "an id";
|
|
231
|
+
throw new PluginError(`Steel answered a session without ${missing}.`, {
|
|
232
|
+
kind: "refused",
|
|
233
|
+
handle: { sessionId: answer.success ? answer.data.id : sessionId }
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
const { id, debugUrl, sessionViewerUrl, timeout } = answer.data;
|
|
237
|
+
noteGrantedLifetime(timeout, body.timeout);
|
|
238
|
+
if (options.apiKey !== void 0) url.searchParams.set("apiKey", options.apiKey);
|
|
239
|
+
const link = viewerLink(sessionViewerUrl);
|
|
240
|
+
return {
|
|
241
|
+
handle: { sessionId: id },
|
|
242
|
+
protocol: "cdp",
|
|
243
|
+
url: url.href,
|
|
244
|
+
mask: debugUrl === void 0 ? [url.href] : [url.href, debugUrl],
|
|
245
|
+
...link === void 0 ? {} : { link }
|
|
246
|
+
};
|
|
247
|
+
},
|
|
248
|
+
async release({ options, handle }, ctx) {
|
|
249
|
+
const parsed = HandleSchema.safeParse(handle);
|
|
250
|
+
if (!parsed.success) throw new PluginError("A Steel handle names its session by sessionId.", { kind: "refused" });
|
|
251
|
+
await createSteelApi(options).release(parsed.data.sessionId, ctx.signal);
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
export { plugin_default as t };
|
|
255
|
+
|
|
256
|
+
//# sourceMappingURL=plugin-C8AoyB8z.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin-C8AoyB8z.js","names":[],"sources":["../src/steel-api.ts","../src/plugin.ts"],"sourcesContent":["import { createVendorApi, PluginError, z } from '@vostride/agent-qa-plugin-kit'\n\nexport const SteelSessionSchema = z.object({\n id: z.string().min(1),\n websocketUrl: z.string().min(1),\n debugUrl: z.string().min(1).optional(),\n sessionViewerUrl: z.string().min(1).optional(),\n timeout: z.number().optional(),\n})\n\nconst SessionStatusSchema = z.object({ status: z.string() })\n\n// Keeps the status of a refused create, so the plugin can name the setting that may cause it.\nexport class SteelCreateFailure extends PluginError {\n constructor(\n failure: PluginError,\n readonly status: number,\n ) {\n super(failure.message, { kind: failure.kind })\n }\n}\n\ninterface SteelApiInput {\n apiUrl: string\n apiKey?: string | undefined\n}\n\nexport interface SteelApi {\n createSession(body: Record<string, unknown>): Promise<unknown>\n release(id: string, signal?: AbortSignal): Promise<void>\n session(id: string, signal?: AbortSignal): Promise<unknown>\n listSessions(signal?: AbortSignal): Promise<void>\n}\n\nexport function createSteelApi(input: SteelApiInput): SteelApi {\n const api = createVendorApi({\n vendor: 'Steel',\n baseUrl: input.apiUrl,\n headers: input.apiKey === undefined ? {} : { 'steel-api-key': input.apiKey },\n })\n\n const sessionPath = (id: string) => `v1/sessions/${encodeURIComponent(id)}`\n\n async function session(id: string, signal?: AbortSignal): Promise<unknown> {\n const response = await api.send('GET', sessionPath(id), {\n headers: { accept: 'application/json' },\n signal,\n })\n if (response.status === 404) {\n await response.body?.cancel()\n return undefined\n }\n if (!response.ok) throw await api.failure(response, 'GET /v1/sessions/{id}')\n return api.json(response)\n }\n\n async function released(id: string, signal?: AbortSignal): Promise<boolean> {\n let current: unknown\n try {\n current = await session(id, signal)\n } catch {\n return false\n }\n if (current === undefined) return true\n const parsed = SessionStatusSchema.safeParse(current)\n return parsed.success && parsed.data.status !== 'live'\n }\n\n return {\n async createSession(body) {\n const response = await api.send('POST', 'v1/sessions', {\n headers: { 'content-type': 'application/json', accept: 'application/json' },\n body: JSON.stringify(body),\n })\n if (!response.ok) {\n const failure = await api.failure(response, 'POST /v1/sessions')\n throw new SteelCreateFailure(failure, response.status)\n }\n return api.json(response)\n },\n\n async release(id, signal) {\n const response = await api.send('POST', `${sessionPath(id)}/release`, {\n headers: { accept: 'application/json' },\n signal,\n })\n if (response.ok || response.status === 404) {\n await response.body?.cancel()\n return\n }\n const failure = await api.failure(response, 'POST /v1/sessions/{id}/release')\n if (await released(id, signal)) return\n throw failure\n },\n\n session,\n\n async listSessions(signal) {\n const response = await api.send('GET', 'v1/sessions?limit=1', {\n headers: { accept: 'application/json' },\n signal,\n })\n if (!response.ok) throw await api.failure(response, 'GET /v1/sessions')\n await response.body?.cancel()\n },\n }\n}\n","import { randomUUID } from 'node:crypto'\nimport {\n checkEndpoints,\n credential,\n definePlugin,\n PluginError,\n z,\n type AcquireInput,\n} from '@vostride/agent-qa-plugin-kit'\nimport { createSteelApi, SteelCreateFailure, SteelSessionSchema } from './steel-api.js'\n\nconst STEEL_API_URL = 'https://api.steel.dev'\nconst MINUTE_MS = 60_000\nconst LIFETIME_REFUSALS: readonly number[] = [400, 422]\nconst SMALLEST_PLAN_CAP_MINUTES = 15\nconst LIFETIME_WORDS = /timeout|lifetime|duration|minute|plan|limit/i\n\nconst options = z.strictObject({\n apiKey: credential('The Steel API key; leave it out for a self-hosted steel-browser').optional(),\n apiUrl: z\n .url()\n .default(STEEL_API_URL)\n .describe(\"Steel's API, https:// unless it points at this machine\"),\n})\n\nconst settings = z.strictObject({\n timeoutMinutes: z\n .int()\n .min(1)\n .max(1440)\n .default(1440)\n .describe('Minutes before Steel ends the session; your Steel plan may cap it lower'),\n idleMinutes: z\n .int()\n .min(1)\n .max(60)\n .default(5)\n .describe('Minutes with no CDP command before Steel ends the session'),\n solveCaptcha: z.boolean().default(false).describe(\"Steel's CAPTCHA solving\"),\n blockAds: z.boolean().default(false).describe(\"Steel's ad blocking\"),\n fingerprint: z\n .boolean()\n .default(false)\n .describe(\"Steel's browser fingerprint injection, which changes what pages see\"),\n proxy: z\n .union([z.boolean(), z.record(z.string(), z.unknown())])\n .default(false)\n .describe(\"Steel's proxy, true or Steel's proxy object, sent as useProxy\"),\n proxyUrl: credential(\n \"Your own proxy, http(s)://user:password@host:port, sent instead of Steel's\",\n ).optional(),\n dimensions: z\n .strictObject({ width: z.int().positive(), height: z.int().positive() })\n .optional()\n .describe('The browser window size'),\n region: z.string().min(1).optional().describe('The Steel region, for example us-east'),\n profile: z\n .string()\n .min(1)\n .optional()\n .describe('A Steel profile id loaded read-only; needs context: reuse'),\n extensions: z\n .array(z.string().min(1))\n .min(1)\n .optional()\n .describe('Steel extension ids loaded into the browser'),\n extra: z\n .record(z.string(), z.unknown())\n .optional()\n .describe('Fields merged last into the session create request, overriding the rest'),\n})\n\ntype SteelSettings = z.output<typeof settings>\ntype Session = AcquireInput<unknown, unknown>['session']\n\nfunction checkSettings(input: SteelSettings, session: Session): void {\n if (input.profile !== undefined && (session.platform !== 'web' || session.context !== 'reuse')) {\n throw new PluginError(\n 'profile loads only into a reused context; set context: reuse on the remote.',\n { kind: 'refused', path: 'settings.profile' },\n )\n }\n if (input.extra !== undefined && Object.hasOwn(input.extra, 'sessionId')) {\n throw new PluginError(\n 'extra cannot set sessionId; the plugin chooses it to release the session.',\n { kind: 'refused', path: 'settings.extra.sessionId' },\n )\n }\n if (input.proxyUrl !== undefined && input.proxy !== false) {\n throw new PluginError('proxy and proxyUrl both choose a proxy; set one of them.', {\n kind: 'refused',\n path: 'settings.proxyUrl',\n })\n }\n}\n\nfunction createBody(sessionId: string, input: SteelSettings): Record<string, unknown> {\n return {\n sessionId,\n timeout: input.timeoutMinutes * MINUTE_MS,\n inactivityTimeout: input.idleMinutes * MINUTE_MS,\n solveCaptcha: input.solveCaptcha,\n blockAds: input.blockAds,\n ...(input.proxyUrl === undefined ? { useProxy: input.proxy } : {}),\n stealthConfig: { skipFingerprintInjection: !input.fingerprint },\n ...(input.proxyUrl === undefined ? {} : { proxyUrl: input.proxyUrl }),\n ...(input.dimensions === undefined ? {} : { dimensions: input.dimensions }),\n ...(input.region === undefined ? {} : { region: input.region }),\n ...(input.profile === undefined ? {} : { profileId: input.profile }),\n ...(input.extensions === undefined ? {} : { extensionIds: input.extensions }),\n ...input.extra,\n }\n}\n\n// A create Steel refused with a 4xx made no session. Anything else (no answer, a 5xx, an\n// unreadable answer) may have, so it carries the handle that releases it.\nfunction mayHaveCreated(error: unknown): boolean {\n return !(error instanceof SteelCreateFailure) || error.status >= 500\n}\n\n// How Steel refuses a lifetime above the plan's cap is undocumented, so the hint is added only when\n// the lifetime asked for is above the smallest cap and Steel's reason, if any, may be about it.\nfunction lifetimeMayBeRefused(error: unknown, timeoutMinutes: number): boolean {\n if (!(error instanceof SteelCreateFailure) || !LIFETIME_REFUSALS.includes(error.status)) {\n return false\n }\n if (timeoutMinutes <= SMALLEST_PLAN_CAP_MINUTES) return false\n const reason = error.message.split(': ').slice(1).join(': ')\n return reason === '' || LIFETIME_WORDS.test(reason)\n}\n\nfunction createFailure(error: unknown, sessionId: string, timeoutMinutes: number): PluginError {\n const message = error instanceof Error ? error.message : String(error)\n const kind = error instanceof PluginError ? error.kind : 'refused'\n const text = lifetimeMayBeRefused(error, timeoutMinutes)\n ? `${message.replace(/[\\s.;,]+$/, '')}; if your Steel plan caps sessions below ${String(timeoutMinutes)} minutes, set settings.timeoutMinutes to the cap`\n : message\n return new PluginError(text, {\n kind,\n ...(mayHaveCreated(error) ? { handle: { sessionId } } : {}),\n })\n}\n\nfunction minutesOf(ms: number): string {\n return String(Number((ms / MINUTE_MS).toFixed(1)))\n}\n\nfunction noteGrantedLifetime(granted: number | undefined, asked: unknown): void {\n if (granted === undefined || typeof asked !== 'number' || granted >= asked) return\n process.stderr.write(\n `Steel granted a ${minutesOf(granted)}-minute session; ${minutesOf(asked)} minutes were asked for\\n`,\n )\n}\n\nfunction viewerLink(url: string | undefined): string | undefined {\n if (url === undefined) return undefined\n return URL.parse(url)?.protocol === 'https:' ? url : undefined\n}\n\nconst HandleSchema = z.object({ sessionId: z.string().min(1) })\n\nfunction hasId(answer: unknown): boolean {\n if (typeof answer !== 'object' || answer === null || !('id' in answer)) return false\n return typeof answer.id === 'string' && answer.id !== ''\n}\n\nfunction refusedKey(error: unknown): boolean {\n return error instanceof PluginError && error.kind === 'auth'\n}\n\nfunction failureLine(error: unknown): string {\n return error instanceof Error ? error.message : String(error)\n}\n\nexport default definePlugin({\n options,\n settings,\n serves: ['cdp'],\n skill: './skill',\n\n initialize({ options }) {\n checkEndpoints(options, [{ path: 'apiUrl', scheme: 'https' }])\n },\n\n async check({ options }, ctx) {\n try {\n await createSteelApi(options).listSessions(ctx.signal)\n } catch (error) {\n if (!refusedKey(error)) return { state: 'warn', lines: [failureLine(error)] }\n const line =\n options.apiKey === undefined\n ? 'Steel asked for an API key; set options.apiKey'\n : 'the API key was refused'\n return { state: 'warn', lines: [line] }\n }\n const line =\n options.apiKey === undefined\n ? 'Steel answered without an API key'\n : 'the API key was accepted'\n return { state: 'ok', lines: [line] }\n },\n\n async acquire({ options, settings, session }) {\n if (session.platform !== 'web' || session.engine !== 'chromium') {\n const what = session.platform === 'web' ? session.engine : session.platform\n throw new PluginError(`Steel runs Chromium web sessions only, not ${what}.`, {\n kind: 'refused',\n })\n }\n checkSettings(settings, session)\n const api = createSteelApi(options)\n const sessionId = randomUUID()\n const body = createBody(sessionId, settings)\n let created: unknown\n // The create is not cancelled: once Steel has a session, the handle must come back to be\n // released, and the kit releases a handle that arrives after a cancel was answered.\n try {\n created = await api.createSession(body)\n } catch (error) {\n throw createFailure(error, sessionId, settings.timeoutMinutes)\n }\n const answer = SteelSessionSchema.safeParse(created)\n const url = answer.success ? URL.parse(answer.data.websocketUrl) : null\n if (!answer.success || url === null) {\n const missing = hasId(created) ? 'a usable websocketUrl' : 'an id'\n throw new PluginError(`Steel answered a session without ${missing}.`, {\n kind: 'refused',\n handle: { sessionId: answer.success ? answer.data.id : sessionId },\n })\n }\n const { id, debugUrl, sessionViewerUrl, timeout } = answer.data\n noteGrantedLifetime(timeout, body.timeout)\n if (options.apiKey !== undefined) url.searchParams.set('apiKey', options.apiKey)\n const link = viewerLink(sessionViewerUrl)\n return {\n handle: { sessionId: id },\n protocol: 'cdp' as const,\n url: url.href,\n mask: debugUrl === undefined ? [url.href] : [url.href, debugUrl],\n ...(link === undefined ? {} : { link }),\n }\n },\n\n async release({ options, handle }, ctx) {\n const parsed = HandleSchema.safeParse(handle)\n if (!parsed.success) {\n throw new PluginError('A Steel handle names its session by sessionId.', { kind: 'refused' })\n }\n await createSteelApi(options).release(parsed.data.sessionId, ctx.signal)\n },\n})\n"],"mappings":"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"}
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { t as default } from "./plugin-C8AoyB8z.js";
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
|
3
|
+
"type": "object",
|
|
4
|
+
"properties": {
|
|
5
|
+
"timeoutMinutes": {
|
|
6
|
+
"default": 1440,
|
|
7
|
+
"description": "Minutes before Steel ends the session; your Steel plan may cap it lower",
|
|
8
|
+
"type": "integer",
|
|
9
|
+
"minimum": 1,
|
|
10
|
+
"maximum": 1440
|
|
11
|
+
},
|
|
12
|
+
"idleMinutes": {
|
|
13
|
+
"default": 5,
|
|
14
|
+
"description": "Minutes with no CDP command before Steel ends the session",
|
|
15
|
+
"type": "integer",
|
|
16
|
+
"minimum": 1,
|
|
17
|
+
"maximum": 60
|
|
18
|
+
},
|
|
19
|
+
"solveCaptcha": {
|
|
20
|
+
"default": false,
|
|
21
|
+
"description": "Steel's CAPTCHA solving",
|
|
22
|
+
"type": "boolean"
|
|
23
|
+
},
|
|
24
|
+
"blockAds": {
|
|
25
|
+
"default": false,
|
|
26
|
+
"description": "Steel's ad blocking",
|
|
27
|
+
"type": "boolean"
|
|
28
|
+
},
|
|
29
|
+
"fingerprint": {
|
|
30
|
+
"default": false,
|
|
31
|
+
"description": "Steel's browser fingerprint injection, which changes what pages see",
|
|
32
|
+
"type": "boolean"
|
|
33
|
+
},
|
|
34
|
+
"proxy": {
|
|
35
|
+
"default": false,
|
|
36
|
+
"description": "Steel's proxy, true or Steel's proxy object, sent as useProxy",
|
|
37
|
+
"anyOf": [
|
|
38
|
+
{
|
|
39
|
+
"type": "boolean"
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
"type": "object",
|
|
43
|
+
"propertyNames": {
|
|
44
|
+
"type": "string"
|
|
45
|
+
},
|
|
46
|
+
"additionalProperties": {}
|
|
47
|
+
}
|
|
48
|
+
]
|
|
49
|
+
},
|
|
50
|
+
"proxyUrl": {
|
|
51
|
+
"type": "string",
|
|
52
|
+
"credential": true,
|
|
53
|
+
"description": "Your own proxy, http(s)://user:password@host:port, sent instead of Steel's"
|
|
54
|
+
},
|
|
55
|
+
"dimensions": {
|
|
56
|
+
"description": "The browser window size",
|
|
57
|
+
"type": "object",
|
|
58
|
+
"properties": {
|
|
59
|
+
"width": {
|
|
60
|
+
"type": "integer",
|
|
61
|
+
"exclusiveMinimum": 0,
|
|
62
|
+
"maximum": 9007199254740991
|
|
63
|
+
},
|
|
64
|
+
"height": {
|
|
65
|
+
"type": "integer",
|
|
66
|
+
"exclusiveMinimum": 0,
|
|
67
|
+
"maximum": 9007199254740991
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
"required": [
|
|
71
|
+
"width",
|
|
72
|
+
"height"
|
|
73
|
+
],
|
|
74
|
+
"additionalProperties": false
|
|
75
|
+
},
|
|
76
|
+
"region": {
|
|
77
|
+
"description": "The Steel region, for example us-east",
|
|
78
|
+
"type": "string",
|
|
79
|
+
"minLength": 1
|
|
80
|
+
},
|
|
81
|
+
"profile": {
|
|
82
|
+
"description": "A Steel profile id loaded read-only; needs context: reuse",
|
|
83
|
+
"type": "string",
|
|
84
|
+
"minLength": 1
|
|
85
|
+
},
|
|
86
|
+
"extensions": {
|
|
87
|
+
"description": "Steel extension ids loaded into the browser",
|
|
88
|
+
"minItems": 1,
|
|
89
|
+
"type": "array",
|
|
90
|
+
"items": {
|
|
91
|
+
"type": "string",
|
|
92
|
+
"minLength": 1
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"extra": {
|
|
96
|
+
"description": "Fields merged last into the session create request, overriding the rest",
|
|
97
|
+
"type": "object",
|
|
98
|
+
"propertyNames": {
|
|
99
|
+
"type": "string"
|
|
100
|
+
},
|
|
101
|
+
"additionalProperties": {}
|
|
102
|
+
}
|
|
103
|
+
},
|
|
104
|
+
"additionalProperties": false
|
|
105
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,71 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vostride/agent-qa-plugin-steel",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.3.2",
|
|
4
|
+
"private": false,
|
|
5
|
+
"description": "agent-qa plugin for Steel: creates a Steel browser session before a test connects and releases it after.",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"agent-qa",
|
|
8
|
+
"agent-qa-plugin",
|
|
9
|
+
"steel"
|
|
10
|
+
],
|
|
11
|
+
"license": "FSL-1.1-ALv2",
|
|
12
|
+
"homepage": "https://github.com/vostride/agent-qa#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/vostride/agent-qa/issues"
|
|
15
|
+
},
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/vostride/agent-qa.git",
|
|
19
|
+
"directory": "plugins/steel"
|
|
20
|
+
},
|
|
21
|
+
"engines": {
|
|
22
|
+
"node": ">=24"
|
|
23
|
+
},
|
|
24
|
+
"publishConfig": {
|
|
25
|
+
"access": "public",
|
|
26
|
+
"registry": "https://registry.npmjs.org/"
|
|
27
|
+
},
|
|
28
|
+
"type": "module",
|
|
29
|
+
"exports": {
|
|
30
|
+
"./package.json": "./package.json"
|
|
31
|
+
},
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"skill",
|
|
35
|
+
"LICENSE.md",
|
|
36
|
+
"NOTICE.md"
|
|
37
|
+
],
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsdown && node scripts/emit.mjs",
|
|
40
|
+
"test": "vitest run",
|
|
41
|
+
"typecheck": "tsc --noEmit",
|
|
42
|
+
"clean": "rm -rf dist .turbo"
|
|
43
|
+
},
|
|
44
|
+
"dependencies": {
|
|
45
|
+
"@vostride/agent-qa-plugin-kit": "0.3.2"
|
|
46
|
+
},
|
|
47
|
+
"devDependencies": {
|
|
48
|
+
"@types/node": "~24.19.2",
|
|
49
|
+
"@vostride/agent-qa-arch": "0.3.2",
|
|
50
|
+
"tsdown": "~0.23.4",
|
|
51
|
+
"typescript": "~7.0.2",
|
|
52
|
+
"vitest": "~5.0.5"
|
|
53
|
+
},
|
|
54
|
+
"agent-qa": {
|
|
55
|
+
"plugin": {
|
|
56
|
+
"kind": "remote",
|
|
57
|
+
"runs": "process",
|
|
58
|
+
"protocol": 1,
|
|
59
|
+
"entry": "./dist/main.js",
|
|
60
|
+
"serves": [
|
|
61
|
+
"cdp"
|
|
62
|
+
],
|
|
63
|
+
"methods": [
|
|
64
|
+
"check"
|
|
65
|
+
],
|
|
66
|
+
"options": "./dist/options.schema.json",
|
|
67
|
+
"settings": "./dist/settings.schema.json",
|
|
68
|
+
"skill": "./skill"
|
|
69
|
+
}
|
|
70
|
+
}
|
|
6
71
|
}
|
package/skill/SKILL.md
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: agent-qa-plugin-steel
|
|
3
|
+
description: Use when configuring agent-qa web tests to run in Steel cloud browsers through the Steel plugin, or when a Steel remote is refused.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Steel remotes for agent-qa
|
|
7
|
+
|
|
8
|
+
The Steel plugin gives an agent-qa web test a Steel browser session. Before the test connects it
|
|
9
|
+
creates a session through Steel's API, and after the test it releases the session. Steel runs
|
|
10
|
+
Chromium only. CAPTCHA solving, ad blocking, Steel's proxies and Steel's fingerprint injection are
|
|
11
|
+
off unless a setting turns them on, so a test sees the app the way its users do.
|
|
12
|
+
|
|
13
|
+
## Declare the plugin and a remote
|
|
14
|
+
|
|
15
|
+
```yaml
|
|
16
|
+
plugins:
|
|
17
|
+
- name: steel
|
|
18
|
+
package: "@vostride/agent-qa-plugin-steel"
|
|
19
|
+
options:
|
|
20
|
+
apiKey: "{{secret:STEEL_API_KEY}}"
|
|
21
|
+
|
|
22
|
+
registry:
|
|
23
|
+
remotes:
|
|
24
|
+
- name: steel
|
|
25
|
+
plugin: steel
|
|
26
|
+
settings:
|
|
27
|
+
idleMinutes: 5
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
- The `plugins` entry holds the account: `apiKey`, and `apiUrl`, which defaults to
|
|
31
|
+
`https://api.steel.dev`. Write the key as a `{{secret:<name>}}` reference to a name in the file
|
|
32
|
+
`workspace.secretsFile` names. Never write the key itself in the config.
|
|
33
|
+
- The remote holds one place: `plugin` names the `plugins` entry, and `settings` hold that place's
|
|
34
|
+
Steel settings.
|
|
35
|
+
- `use.browser.remote: steel` in the config, a suite or a test runs the test there.
|
|
36
|
+
- Install the package at the exact version of agent-qa the project runs:
|
|
37
|
+
`npm install --save-dev --save-exact @vostride/agent-qa-plugin-steel@<agent-qa version>`.
|
|
38
|
+
`agent-qa plugins list` prints that line when the package is missing or the version differs.
|
|
39
|
+
- For a self-hosted steel-browser, set `apiUrl` to its loopback `http://` address, leave out
|
|
40
|
+
`apiKey`, and set `use.workers: 1`: it serves one session at a time, and releasing one ends
|
|
41
|
+
whatever session is active. It returns no session link.
|
|
42
|
+
|
|
43
|
+
## Settings
|
|
44
|
+
|
|
45
|
+
- `timeoutMinutes` (default `1440`, 1 to 1440): minutes before Steel ends the session. Steel's
|
|
46
|
+
plans cap it: Launch at 15, Scale at 60, Enterprise at up to 1440.
|
|
47
|
+
- `idleMinutes` (default `5`, 1 to 60): minutes with no CDP command before Steel ends the session.
|
|
48
|
+
agent-qa's heartbeat keeps a running test from going idle.
|
|
49
|
+
- `solveCaptcha` (default `false`), `blockAds` (default `false`): Steel's CAPTCHA solving and ad
|
|
50
|
+
blocking.
|
|
51
|
+
- `fingerprint` (default `false`): Steel's fingerprint injection. The plugin always tells Steel to
|
|
52
|
+
skip it unless this is `true`.
|
|
53
|
+
- `proxy` (default `false`): `true`, or Steel's proxy object, sent as `useProxy`.
|
|
54
|
+
- `proxyUrl`: your own proxy, `http(s)://user:password@host:port`, a credential. Write it as a
|
|
55
|
+
`{{secret:<name>}}` reference. It is sent instead of Steel's proxy, so do not also set `proxy`.
|
|
56
|
+
- `dimensions` (`{ width, height }`), `region` (for example `us-east`), `extensions` (extension
|
|
57
|
+
ids): Steel's own create fields.
|
|
58
|
+
- `profile`: a Steel profile id. It loads read-only: nothing the test does is saved back. It needs
|
|
59
|
+
`context: reuse` on the remote, because the profile lives in the browser's first context:
|
|
60
|
+
|
|
61
|
+
```yaml
|
|
62
|
+
- name: steel-signed-in
|
|
63
|
+
plugin: steel
|
|
64
|
+
context: reuse
|
|
65
|
+
settings:
|
|
66
|
+
profile: <Steel profile id>
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
- `extra`: any other Steel request field. It is merged last, so it overrides the settings above.
|
|
70
|
+
It cannot set `sessionId`, which the plugin chooses to release the session.
|
|
71
|
+
|
|
72
|
+
## When a remote is refused
|
|
73
|
+
|
|
74
|
+
- `auth`: Steel refused the API key. Check the secret's value.
|
|
75
|
+
- `capacity`: Steel answered 429. The account has no free sessions, or is rate limited. Wait, or
|
|
76
|
+
lower the run's concurrency.
|
|
77
|
+
- `refused` whose line ends `set settings.timeoutMinutes to the cap`: Steel refused the create with
|
|
78
|
+
`400` or `422`, and its reason may be the plan capping sessions below the lifetime asked for. Set
|
|
79
|
+
`timeoutMinutes` to the plan's cap, for example `15` on Launch. A `400` or `422` whose reason
|
|
80
|
+
names another field ends without this, so fix the field it names.
|
|
81
|
+
- `refused` with path `settings.profile`: the remote sets `profile` without `context: reuse`. Add
|
|
82
|
+
`context: reuse` to the remote.
|
|
83
|
+
- `refused` with path `settings.extra.sessionId`: `extra` sets `sessionId`. Remove it.
|
|
84
|
+
- `refused` with path `settings.proxyUrl`: the remote sets both `proxy` and `proxyUrl`. Keep one.
|
|
85
|
+
- `refused` naming a browser or platform: Steel runs Chromium web tests only. Use
|
|
86
|
+
`use.browser.name: chromium`, or another remote.
|
|
87
|
+
- `refused` with `Unrecognized key: "recording"`: Steel has no switch to turn recording off, so
|
|
88
|
+
the plugin has no `recording` setting. Remove it.
|
|
89
|
+
- `refused` at `options.apiUrl`: the endpoint must use `https://` with no user or password, unless
|
|
90
|
+
it points at this machine.
|
|
91
|
+
- `unreachable`: Steel could not be reached. Check the network, or that a self-hosted
|
|
92
|
+
steel-browser is running.
|
|
93
|
+
|
|
94
|
+
## Recording
|
|
95
|
+
|
|
96
|
+
Steel records every session and keeps the replay on its dashboard, behind its login. agent-qa's
|
|
97
|
+
masking does not reach that replay, so a secret a test types may show in it. Steel offers no switch
|
|
98
|
+
to turn recording off. The session link opens the session's page in Steel's dashboard. Steel's
|
|
99
|
+
debug URL lets anyone who has it drive the session, so agent-qa masks it and never shows it.
|
|
100
|
+
|
|
101
|
+
## Check and describe
|
|
102
|
+
|
|
103
|
+
- `agent-qa doctor` runs the plugin's account check. It lists sessions to prove the key and opens
|
|
104
|
+
none.
|
|
105
|
+
- `agent-qa plugins describe steel --json` lists every option and setting with its type, default
|
|
106
|
+
and description.
|