@vostride/agent-qa-plugin-kernel 0.0.1 → 0.3.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.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,137 @@
1
+ # @vostride/agent-qa-plugin-kernel
2
+
3
+ The agent-qa plugin for [Kernel](https://www.kernel.sh) cloud browsers. Before a web test connects,
4
+ it creates a Kernel browser or takes one from a browser pool, and answers agent-qa with the
5
+ browser's CDP URL. After the test it deletes the browser, or returns it to the pool. Kernel runs
6
+ Chromium only.
7
+
8
+ ## Install
9
+
10
+ Install the plugin at exactly the version of agent-qa the project runs:
11
+
12
+ ```bash
13
+ npm install --save-dev --save-exact @vostride/agent-qa-plugin-kernel@<agent-qa version>
14
+ ```
15
+
16
+ `agent-qa plugins list` prints this line, with the version filled in, when the package is missing
17
+ or its version differs from agent-qa's.
18
+
19
+ ## Configure
20
+
21
+ ```yaml
22
+ plugins:
23
+ - name: kernel
24
+ package: "@vostride/agent-qa-plugin-kernel"
25
+ options:
26
+ apiKey: "{{secret:KERNEL_API_KEY}}"
27
+
28
+ registry:
29
+ remotes:
30
+ - name: kernel
31
+ plugin: kernel
32
+ settings:
33
+ headless: true
34
+ timeoutSeconds: 60
35
+ ```
36
+
37
+ A test runs there with `use.browser.remote: kernel` in the config, a suite or the test.
38
+
39
+ ### Options
40
+
41
+ Options belong to the `plugins` entry and describe the account.
42
+
43
+ | Option | Default | Meaning |
44
+ | -------- | -------------------------- | ------------------------------------------------------------------------------ |
45
+ | `apiKey` | required | The Kernel API key, a credential. Write it as a `{{secret:<name>}}` reference. |
46
+ | `apiUrl` | `https://api.onkernel.com` | Kernel's API. It must use `https://` unless it points at this machine. |
47
+
48
+ The key is sent only as the `Authorization: Bearer` header. It never appears in a message, a log or
49
+ an answer.
50
+
51
+ ### Settings
52
+
53
+ Settings belong to each remote and describe one place.
54
+
55
+ | Setting | Default | Meaning |
56
+ | ---------------- | ------- | -------------------------------------------------------------------------------------- |
57
+ | `headless` | `true` | `false` gives a headful browser. Kernel's own default is headful. |
58
+ | `timeoutSeconds` | `60` | Seconds Kernel keeps the browser after agent-qa's connection drops (10 to 259200). |
59
+ | `stealth` | - | Kernel's stealth mode. |
60
+ | `region` | - | The Kernel region, for example `eu-west`. |
61
+ | `proxy` | - | Kernel's proxy object, sent as given. |
62
+ | `viewport` | - | `width`, `height` and optional `refreshRate`, sent as `refresh_rate`. |
63
+ | `pool` | - | A browser pool's id or name to acquire from instead of creating a browser. |
64
+ | `profile` | - | A profile's name, or `{ id: <profile id> }`, loaded read-only. Needs `context: reuse`. |
65
+ | `extra` | - | Any other Kernel request field, merged last. |
66
+
67
+ `agent-qa plugins describe kernel` prints the same tables from the schemas the package ships, and
68
+ `--json` prints them as JSON.
69
+
70
+ ## How the request body is built
71
+
72
+ The create request (`POST /browsers`) is built in three layers, each overriding the one before:
73
+
74
+ 1. the defaults: `headless: true` and `timeout_seconds: 60`;
75
+ 2. the curated settings above, in Kernel's field names (`timeoutSeconds` becomes
76
+ `timeout_seconds`, `viewport.refreshRate` becomes `viewport.refresh_rate`, and `profile`
77
+ becomes `profile: { name | id, save_changes: false }`);
78
+ 3. `extra`, last, so a pass-through key overrides both a default and a curated setting.
79
+
80
+ For example, `settings: { headless: true, extra: { headless: false, kiosk_mode: true } }` creates a
81
+ headful browser in kiosk mode.
82
+
83
+ ## Pools
84
+
85
+ With `pool`, the plugin acquires a browser with `POST /browser_pools/{pool}/acquire`. Kernel holds
86
+ that request open while the pool is empty and answers `204` when it times out. The plugin then asks
87
+ again, and reports `waiting for pool <pool>` to agent-qa after each `204`. Asks are at least a
88
+ second apart, so a short `acquire_timeout_seconds` in `extra` does not become a busy loop. The wait
89
+ ends when a browser is free or when agent-qa cancels it at the remote's `connectTimeout`.
90
+
91
+ - The acquire body holds only `profile` and `extra`. The pool's own configuration sets the browser,
92
+ so `stealth`, `region`, `proxy` and `viewport` are refused on a remote with `pool`, and
93
+ `headless` and `timeoutSeconds` do not apply.
94
+ - On release the plugin posts `{ session_id, reuse }` to `POST /browser_pools/{pool}/release`.
95
+ `reuse` is `true` after a test in a new context, so the browser goes back to the pool. It is
96
+ `false` after a test with `context: reuse`, so the browser is discarded.
97
+ - A browser the pool no longer has counts as released.
98
+
99
+ ## Profiles
100
+
101
+ `profile` loads a Kernel profile with `save_changes: false`, so nothing a test does is saved back.
102
+ A profile lives in the browser's first context, so the remote needs `context: reuse`. Without it,
103
+ the remote is refused with kind `refused` and path `settings.profile`. With `pool`, the profile goes
104
+ into the acquire body, and Kernel discards such a browser after use.
105
+
106
+ ## Failures
107
+
108
+ | Kernel answer | Kind |
109
+ | --------------------- | ------------- |
110
+ | `401`, `403` | `auth` |
111
+ | `429`, `529` | `capacity` |
112
+ | no answer | `unreachable` |
113
+ | any other non-success | `refused` |
114
+
115
+ Each failure is one line naming the status, the request and Kernel's message, with `Retry-After`
116
+ on a `429`. A browser that is already gone when the plugin deletes it counts as released.
117
+
118
+ ## Links and the live view
119
+
120
+ Kernel documents no per-session page, so the plugin returns no session link. A headful browser's
121
+ live view URL carries a token, so it is a credential: the plugin adds it, with the CDP URL, to the
122
+ values agent-qa masks, and never returns it as a link.
123
+
124
+ ## Account check
125
+
126
+ `agent-qa doctor` runs the plugin's `check`, which creates no browser:
127
+
128
+ - `GET /browsers?limit=1` proves the key. A refused key warns `the API key was refused`.
129
+ - `GET /org/limits` reports free sessions, for example `3 of 5 concurrent sessions free`. A key
130
+ that may not read them reads `organization limits not readable with this key`, which does not
131
+ warn.
132
+ - `GET /browser_pools/{pool}` for each pool a remote names reads `pool <pool> found`, or warns
133
+ `pool <pool> not found`.
134
+
135
+ ## License
136
+
137
+ FSL-1.1-ALv2. See `LICENSE.md` and `NOTICE.md` in the package.
package/dist/main.js ADDED
@@ -0,0 +1,7 @@
1
+ import { t as plugin_default } from "./plugin-C2raYjWg.js";
2
+ import { runPlugin } from "@vostride/agent-qa-plugin-kit";
3
+ // src/main.ts
4
+ runPlugin(plugin_default);
5
+ export {};
6
+
7
+ //# sourceMappingURL=main.js.map
@@ -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,21 @@
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 Kernel API key"
9
+ },
10
+ "apiUrl": {
11
+ "default": "https://api.onkernel.com",
12
+ "description": "Kernel's API, https:// unless it points at this machine",
13
+ "type": "string",
14
+ "format": "uri"
15
+ }
16
+ },
17
+ "required": [
18
+ "apiKey"
19
+ ],
20
+ "additionalProperties": false
21
+ }
@@ -0,0 +1,372 @@
1
+ import { PluginError, credential, definePlugin, z } from "@vostride/agent-qa-plugin-kit";
2
+ import { setTimeout } from "node:timers/promises";
3
+ // src/kernel-api.ts
4
+ const KernelBrowserSchema = z.object({
5
+ session_id: z.string().min(1),
6
+ cdp_ws_url: z.string().min(1),
7
+ browser_live_view_url: z.string().min(1).optional()
8
+ });
9
+ function kernelFailureKind(status) {
10
+ if (status === void 0) return "unreachable";
11
+ if (status === 401 || status === 403) return "auth";
12
+ if (status === 429 || status === 529) return "capacity";
13
+ return "refused";
14
+ }
15
+ const MAX_REASON_LENGTH = 200;
16
+ function oneLine(text) {
17
+ const line = text.replace(/\s+/g, " ").trim();
18
+ return line.length > MAX_REASON_LENGTH ? `${line.slice(0, MAX_REASON_LENGTH)}…` : line;
19
+ }
20
+ async function vendorReason(response) {
21
+ const text = await response.text().catch(() => "");
22
+ try {
23
+ const body = JSON.parse(text);
24
+ if (typeof body === "object" && body !== null && "message" in body) {
25
+ const { message } = body;
26
+ if (typeof message === "string" && message.trim() !== "") return `: ${oneLine(message)}`;
27
+ }
28
+ } catch {}
29
+ return "";
30
+ }
31
+ async function statusFailure(response, request) {
32
+ const retryAfter = response.headers.get("retry-after");
33
+ const wait = response.status === 429 && retryAfter !== null ? `, retry after ${retryAfter} s` : "";
34
+ const reason = await vendorReason(response);
35
+ return new PluginError(`Kernel answered ${response.status} to ${request}${reason}${wait}`, { kind: kernelFailureKind(response.status) });
36
+ }
37
+ async function jsonOf(response) {
38
+ const text = await response.text();
39
+ try {
40
+ return JSON.parse(text);
41
+ } catch {
42
+ return;
43
+ }
44
+ }
45
+ function causeText(error) {
46
+ if (!(error instanceof Error)) return String(error);
47
+ const cause = error.cause;
48
+ if (cause instanceof Error) return oneLine("code" in cause && typeof cause.code === "string" ? cause.code : cause.message);
49
+ return oneLine(error.message);
50
+ }
51
+ function createKernelApi(input) {
52
+ const base = input.apiUrl.endsWith("/") ? input.apiUrl : `${input.apiUrl}/`;
53
+ const authorization = `Bearer ${input.apiKey}`;
54
+ const send = async (method, path, init) => {
55
+ const url = new URL(path, base);
56
+ try {
57
+ return await fetch(url, {
58
+ method,
59
+ headers: {
60
+ ...init.headers,
61
+ authorization
62
+ },
63
+ ...init.body === void 0 ? {} : { body: init.body },
64
+ ...init.signal === void 0 ? {} : { signal: init.signal }
65
+ });
66
+ } catch (error) {
67
+ if (init.signal?.aborted === true) throw error;
68
+ throw new PluginError(`Kernel could not be reached at ${url.origin}: ${causeText(error)}`, { kind: "unreachable" });
69
+ }
70
+ };
71
+ const jsonHeaders = {
72
+ "content-type": "application/json",
73
+ accept: "application/json"
74
+ };
75
+ const poolPath = (pool, action = "") => `browser_pools/${encodeURIComponent(pool)}${action === "" ? "" : `/${action}`}`;
76
+ return {
77
+ async createBrowser(body, signal) {
78
+ const response = await send("POST", "browsers", {
79
+ headers: jsonHeaders,
80
+ body: JSON.stringify(body),
81
+ signal
82
+ });
83
+ if (!response.ok) throw await statusFailure(response, "POST /browsers");
84
+ return jsonOf(response);
85
+ },
86
+ async deleteBrowser(id, signal) {
87
+ const response = await send("DELETE", `browsers/${encodeURIComponent(id)}`, { signal });
88
+ if (response.ok || response.status === 404) {
89
+ await response.body?.cancel();
90
+ return;
91
+ }
92
+ throw await statusFailure(response, "DELETE /browsers/{id}");
93
+ },
94
+ async acquireFromPool(pool, body, signal) {
95
+ const response = await send("POST", poolPath(pool, "acquire"), {
96
+ headers: jsonHeaders,
97
+ body: JSON.stringify(body),
98
+ signal
99
+ });
100
+ if (response.status === 204) {
101
+ await response.body?.cancel();
102
+ return { acquired: false };
103
+ }
104
+ if (!response.ok) throw await statusFailure(response, "POST /browser_pools/{pool}/acquire");
105
+ return {
106
+ acquired: true,
107
+ browser: await jsonOf(response)
108
+ };
109
+ },
110
+ async releaseToPool(pool, sessionId, reuse, signal) {
111
+ const response = await send("POST", poolPath(pool, "release"), {
112
+ headers: jsonHeaders,
113
+ body: JSON.stringify({
114
+ session_id: sessionId,
115
+ reuse
116
+ }),
117
+ signal
118
+ });
119
+ if (response.ok || response.status === 404) {
120
+ await response.body?.cancel();
121
+ return;
122
+ }
123
+ throw await statusFailure(response, "POST /browser_pools/{pool}/release");
124
+ },
125
+ async listBrowsers(limit, signal) {
126
+ const response = await send("GET", `browsers?limit=${String(limit)}`, {
127
+ headers: { accept: "application/json" },
128
+ signal
129
+ });
130
+ if (!response.ok) throw await statusFailure(response, "GET /browsers");
131
+ await response.body?.cancel();
132
+ },
133
+ async orgLimits(signal) {
134
+ const response = await send("GET", "org/limits", {
135
+ headers: { accept: "application/json" },
136
+ signal
137
+ });
138
+ if (!response.ok) throw await statusFailure(response, "GET /org/limits");
139
+ return jsonOf(response);
140
+ },
141
+ async getPool(pool, signal) {
142
+ const response = await send("GET", poolPath(pool), {
143
+ headers: { accept: "application/json" },
144
+ signal
145
+ });
146
+ if (response.status === 404) {
147
+ await response.body?.cancel();
148
+ return false;
149
+ }
150
+ if (!response.ok) throw await statusFailure(response, "GET /browser_pools/{pool}");
151
+ await response.body?.cancel();
152
+ return true;
153
+ }
154
+ };
155
+ }
156
+ // src/plugin.ts
157
+ const KERNEL_API_URL = "https://api.onkernel.com";
158
+ const POOL_POLL_FLOOR_MS = 1e3;
159
+ const LOOPBACK_HOSTS = /* @__PURE__ */ new Set(["localhost", "[::1]"]);
160
+ const options = z.strictObject({
161
+ apiKey: credential("The Kernel API key"),
162
+ apiUrl: z.url().default(KERNEL_API_URL).describe("Kernel's API, https:// unless it points at this machine")
163
+ });
164
+ const settings = z.strictObject({
165
+ headless: z.boolean().default(true).describe("Run a headless browser; Kernel offers a live view only for a headful one"),
166
+ timeoutSeconds: z.int().min(10).max(259200).default(60).describe("Seconds with no connection before Kernel deletes the browser"),
167
+ stealth: z.boolean().optional().describe("Kernel's stealth mode"),
168
+ region: z.string().min(1).optional().describe("The Kernel region, for example eu-west"),
169
+ proxy: z.record(z.string(), z.unknown()).optional().describe("Kernel's proxy, sent as given"),
170
+ viewport: z.strictObject({
171
+ width: z.int().positive(),
172
+ height: z.int().positive(),
173
+ refreshRate: z.int().positive().optional()
174
+ }).optional().describe("The browser window size"),
175
+ pool: z.string().min(1).optional().describe("A Kernel browser pool, by id or name, to acquire from instead of creating a browser"),
176
+ profile: z.union([z.string().min(1), z.strictObject({ id: z.string().min(1) })]).optional().describe("A Kernel profile loaded read-only, by name or as { id }; needs context: reuse"),
177
+ extra: z.record(z.string(), z.unknown()).optional().describe("Fields merged last into the create or pool acquire request, overriding the rest")
178
+ });
179
+ function apiUrlAllowed(value) {
180
+ const url = new URL(value);
181
+ if (url.username !== "" || url.password !== "") return false;
182
+ if (url.protocol === "https:") return true;
183
+ if (url.protocol !== "http:") return false;
184
+ return LOOPBACK_HOSTS.has(url.hostname) || /^127(?:\.\d{1,3}){3}$/.test(url.hostname);
185
+ }
186
+ const CREATE_ONLY_SETTINGS = [
187
+ "stealth",
188
+ "region",
189
+ "proxy",
190
+ "viewport"
191
+ ];
192
+ function profileBody(input, session) {
193
+ const { profile } = input;
194
+ if (profile === void 0) return void 0;
195
+ if (session.platform !== "web" || session.context !== "reuse") throw new PluginError("profile loads only into a reused context; set context: reuse on the remote.", {
196
+ kind: "refused",
197
+ path: "settings.profile"
198
+ });
199
+ return {
200
+ ...typeof profile === "string" ? { name: profile } : { id: profile.id },
201
+ save_changes: false
202
+ };
203
+ }
204
+ function createBody(input, profile) {
205
+ const { viewport } = input;
206
+ return {
207
+ headless: input.headless,
208
+ timeout_seconds: input.timeoutSeconds,
209
+ ...input.stealth === void 0 ? {} : { stealth: input.stealth },
210
+ ...input.region === void 0 ? {} : { region: input.region },
211
+ ...input.proxy === void 0 ? {} : { proxy: input.proxy },
212
+ ...viewport === void 0 ? {} : { viewport: {
213
+ width: viewport.width,
214
+ height: viewport.height,
215
+ ...viewport.refreshRate === void 0 ? {} : { refresh_rate: viewport.refreshRate }
216
+ } },
217
+ ...profile === void 0 ? {} : { profile },
218
+ ...input.extra
219
+ };
220
+ }
221
+ const HandleSchema = z.object({
222
+ sessionId: z.string().min(1),
223
+ pool: z.string().min(1).optional(),
224
+ reuse: z.boolean().optional()
225
+ });
226
+ function sessionIdOf(answer) {
227
+ if (typeof answer !== "object" || answer === null || !("session_id" in answer)) return void 0;
228
+ const id = answer.session_id;
229
+ return typeof id === "string" && id !== "" ? id : void 0;
230
+ }
231
+ function cdpAnswer(answer, place) {
232
+ const sessionId = sessionIdOf(answer);
233
+ const handle = sessionId === void 0 ? void 0 : {
234
+ sessionId,
235
+ ...place
236
+ };
237
+ const browser = KernelBrowserSchema.safeParse(answer);
238
+ if (!browser.success) throw new PluginError("Kernel answered a browser without a session_id and a cdp_ws_url.", {
239
+ kind: "refused",
240
+ ...handle === void 0 ? {} : { handle }
241
+ });
242
+ const { cdp_ws_url: url, browser_live_view_url: liveView } = browser.data;
243
+ return {
244
+ handle: {
245
+ sessionId: browser.data.session_id,
246
+ ...place
247
+ },
248
+ protocol: "cdp",
249
+ url,
250
+ mask: liveView === void 0 ? [url] : [url, liveView]
251
+ };
252
+ }
253
+ async function acquireFromPool(api, pool, body, reuse, ctx) {
254
+ const stopped = () => new PluginError(`stopped waiting for pool ${pool}`, { kind: "timeout" });
255
+ for (;;) {
256
+ const began = Date.now();
257
+ let answer;
258
+ try {
259
+ answer = await api.acquireFromPool(pool, body, ctx.signal);
260
+ } catch (error) {
261
+ if (ctx.signal.aborted) throw stopped();
262
+ throw error;
263
+ }
264
+ if (answer.acquired) return cdpAnswer(answer.browser, {
265
+ pool,
266
+ reuse
267
+ });
268
+ if (ctx.signal.aborted) throw stopped();
269
+ ctx.progress({ message: `waiting for pool ${pool}` });
270
+ const rest = POOL_POLL_FLOOR_MS - (Date.now() - began);
271
+ if (rest > 0) await setTimeout(rest, void 0, { signal: ctx.signal }).catch(() => {});
272
+ if (ctx.signal.aborted) throw stopped();
273
+ }
274
+ }
275
+ const OrgLimitsSchema = z.object({
276
+ concurrent_sessions_available: z.int().nonnegative(),
277
+ concurrent_sessions_used: z.int().nonnegative()
278
+ });
279
+ function refusedKey(error) {
280
+ return error instanceof PluginError && error.kind === "auth";
281
+ }
282
+ function failureLine(error) {
283
+ return error instanceof Error ? error.message : String(error);
284
+ }
285
+ async function limitsLine(api, signal) {
286
+ let answer;
287
+ try {
288
+ answer = await api.orgLimits(signal);
289
+ } catch (error) {
290
+ if (refusedKey(error)) return "organization limits not readable with this key";
291
+ return `organization limits not readable: ${failureLine(error)}`;
292
+ }
293
+ const limits = OrgLimitsSchema.safeParse(answer);
294
+ if (!limits.success) return "organization limits not readable: Kernel answered another shape";
295
+ const { concurrent_sessions_available: free, concurrent_sessions_used: used } = limits.data;
296
+ return `${String(free)} of ${String(free + used)} concurrent sessions free`;
297
+ }
298
+ var plugin_default = definePlugin({
299
+ options,
300
+ settings,
301
+ serves: ["cdp"],
302
+ skill: "./skill",
303
+ initialize({ options }) {
304
+ if (!apiUrlAllowed(options.apiUrl)) throw new PluginError("apiUrl must use https:// unless it points at this machine.", {
305
+ kind: "refused",
306
+ path: "apiUrl"
307
+ });
308
+ },
309
+ async check({ options, remotes }, ctx) {
310
+ const api = createKernelApi(options);
311
+ try {
312
+ await api.listBrowsers(1, ctx.signal);
313
+ } catch (error) {
314
+ return {
315
+ state: "warn",
316
+ lines: [refusedKey(error) ? "the API key was refused" : failureLine(error)]
317
+ };
318
+ }
319
+ const lines = ["the API key was accepted", await limitsLine(api, ctx.signal)];
320
+ let state = "ok";
321
+ const pools = new Set(remotes.flatMap(({ settings }) => settings.pool ?? []));
322
+ for (const pool of pools) {
323
+ let found;
324
+ try {
325
+ found = await api.getPool(pool, ctx.signal);
326
+ } catch (error) {
327
+ state = "warn";
328
+ lines.push(`pool ${pool} not readable: ${failureLine(error)}`);
329
+ continue;
330
+ }
331
+ if (!found) state = "warn";
332
+ lines.push(found ? `pool ${pool} found` : `pool ${pool} not found`);
333
+ }
334
+ return {
335
+ state,
336
+ lines
337
+ };
338
+ },
339
+ async acquire({ options, settings, session }, ctx) {
340
+ if (session.platform !== "web" || session.engine !== "chromium") {
341
+ const what = session.platform === "web" ? session.engine : session.platform;
342
+ throw new PluginError(`Kernel runs Chromium web sessions only, not ${what}.`, { kind: "refused" });
343
+ }
344
+ const profile = profileBody(settings, session);
345
+ const api = createKernelApi(options);
346
+ if (settings.pool !== void 0) {
347
+ const fromCreate = CREATE_ONLY_SETTINGS.find((name) => settings[name] !== void 0);
348
+ if (fromCreate !== void 0) throw new PluginError(`${fromCreate} comes from the pool's own configuration; remove it from a remote with pool.`, {
349
+ kind: "refused",
350
+ path: `settings.${fromCreate}`
351
+ });
352
+ const body = {
353
+ ...profile === void 0 ? {} : { profile },
354
+ ...settings.extra
355
+ };
356
+ const reuse = session.context !== "reuse";
357
+ return acquireFromPool(api, settings.pool, body, reuse, ctx);
358
+ }
359
+ return cdpAnswer(await api.createBrowser(createBody(settings, profile)), {});
360
+ },
361
+ async release({ options, handle }, ctx) {
362
+ const parsed = HandleSchema.safeParse(handle);
363
+ if (!parsed.success) throw new PluginError("A Kernel handle names its browser by sessionId.", { kind: "refused" });
364
+ const { sessionId, pool, reuse } = parsed.data;
365
+ const api = createKernelApi(options);
366
+ if (pool === void 0) await api.deleteBrowser(sessionId, ctx.signal);
367
+ else await api.releaseToPool(pool, sessionId, reuse ?? false, ctx.signal);
368
+ }
369
+ });
370
+ export { plugin_default as t };
371
+
372
+ //# sourceMappingURL=plugin-C2raYjWg.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin-C2raYjWg.js","names":["delay"],"sources":["../src/kernel-api.ts","../src/plugin.ts"],"sourcesContent":["import { PluginError, z } from '@vostride/agent-qa-plugin-kit'\n\ntype KernelFailureKind = 'auth' | 'capacity' | 'refused' | 'unreachable'\n\nexport const KernelBrowserSchema = z.object({\n session_id: z.string().min(1),\n cdp_ws_url: z.string().min(1),\n browser_live_view_url: z.string().min(1).optional(),\n})\n\ninterface KernelApiInput {\n apiUrl: string\n apiKey: string\n}\n\nexport type PoolAcquireAnswer = { acquired: true; browser: unknown } | { acquired: false }\n\nexport interface KernelApi {\n createBrowser(body: Record<string, unknown>, signal?: AbortSignal): Promise<unknown>\n deleteBrowser(id: string, signal?: AbortSignal): Promise<void>\n acquireFromPool(\n pool: string,\n body: Record<string, unknown>,\n signal?: AbortSignal,\n ): Promise<PoolAcquireAnswer>\n releaseToPool(pool: string, sessionId: string, reuse: boolean, signal?: AbortSignal): Promise<void>\n listBrowsers(limit: number, signal?: AbortSignal): Promise<void>\n orgLimits(signal?: AbortSignal): Promise<unknown>\n getPool(pool: string, signal?: AbortSignal): Promise<boolean>\n}\n\nexport function kernelFailureKind(status: number | undefined): KernelFailureKind {\n if (status === undefined) return 'unreachable'\n if (status === 401 || status === 403) return 'auth'\n if (status === 429 || status === 529) return 'capacity'\n return 'refused'\n}\n\ninterface SendInit {\n headers?: Record<string, string>\n body?: string\n signal?: AbortSignal | undefined\n}\n\nconst MAX_REASON_LENGTH = 200\n\nfunction oneLine(text: string): string {\n const line = text.replace(/\\s+/g, ' ').trim()\n return line.length > MAX_REASON_LENGTH ? `${line.slice(0, MAX_REASON_LENGTH)}…` : line\n}\n\nasync function vendorReason(response: Response): Promise<string> {\n const text = await response.text().catch(() => '')\n try {\n const body: unknown = JSON.parse(text)\n if (typeof body === 'object' && body !== null && 'message' in body) {\n const { message } = body\n if (typeof message === 'string' && message.trim() !== '') return `: ${oneLine(message)}`\n }\n } catch {\n // a body that is not JSON carries no reason worth showing\n }\n return ''\n}\n\nasync function statusFailure(response: Response, request: string): Promise<PluginError> {\n const retryAfter = response.headers.get('retry-after')\n const wait = response.status === 429 && retryAfter !== null ? `, retry after ${retryAfter} s` : ''\n const reason = await vendorReason(response)\n return new PluginError(`Kernel answered ${response.status} to ${request}${reason}${wait}`, {\n kind: kernelFailureKind(response.status),\n })\n}\n\nasync function jsonOf(response: Response): Promise<unknown> {\n const text = await response.text()\n try {\n return JSON.parse(text) as unknown\n } catch {\n return undefined\n }\n}\n\nfunction causeText(error: unknown): string {\n if (!(error instanceof Error)) return String(error)\n const cause = error.cause\n if (cause instanceof Error) {\n const code = 'code' in cause && typeof cause.code === 'string' ? cause.code : cause.message\n return oneLine(code)\n }\n return oneLine(error.message)\n}\n\nexport function createKernelApi(input: KernelApiInput): KernelApi {\n const base = input.apiUrl.endsWith('/') ? input.apiUrl : `${input.apiUrl}/`\n const authorization = `Bearer ${input.apiKey}`\n\n const send = async (method: string, path: string, init: SendInit): Promise<Response> => {\n const url = new URL(path, base)\n try {\n return await fetch(url, {\n method,\n headers: { ...init.headers, authorization },\n ...(init.body === undefined ? {} : { body: init.body }),\n ...(init.signal === undefined ? {} : { signal: init.signal }),\n })\n } catch (error) {\n if (init.signal?.aborted === true) throw error\n throw new PluginError(`Kernel could not be reached at ${url.origin}: ${causeText(error)}`, {\n kind: 'unreachable',\n })\n }\n }\n\n const jsonHeaders = { 'content-type': 'application/json', accept: 'application/json' }\n const poolPath = (pool: string, action = '') =>\n `browser_pools/${encodeURIComponent(pool)}${action === '' ? '' : `/${action}`}`\n\n return {\n async createBrowser(body, signal) {\n const response = await send('POST', 'browsers', {\n headers: jsonHeaders,\n body: JSON.stringify(body),\n signal,\n })\n if (!response.ok) throw await statusFailure(response, 'POST /browsers')\n return jsonOf(response)\n },\n\n async deleteBrowser(id, signal) {\n const response = await send('DELETE', `browsers/${encodeURIComponent(id)}`, { signal })\n if (response.ok || response.status === 404) {\n await response.body?.cancel()\n return\n }\n throw await statusFailure(response, 'DELETE /browsers/{id}')\n },\n\n async acquireFromPool(pool, body, signal) {\n const response = await send('POST', poolPath(pool, 'acquire'), {\n headers: jsonHeaders,\n body: JSON.stringify(body),\n signal,\n })\n if (response.status === 204) {\n await response.body?.cancel()\n return { acquired: false }\n }\n if (!response.ok) throw await statusFailure(response, 'POST /browser_pools/{pool}/acquire')\n return { acquired: true, browser: await jsonOf(response) }\n },\n\n async releaseToPool(pool, sessionId, reuse, signal) {\n const response = await send('POST', poolPath(pool, 'release'), {\n headers: jsonHeaders,\n body: JSON.stringify({ session_id: sessionId, reuse }),\n signal,\n })\n if (response.ok || response.status === 404) {\n await response.body?.cancel()\n return\n }\n throw await statusFailure(response, 'POST /browser_pools/{pool}/release')\n },\n\n async listBrowsers(limit, signal) {\n const response = await send('GET', `browsers?limit=${String(limit)}`, {\n headers: { accept: 'application/json' },\n signal,\n })\n if (!response.ok) throw await statusFailure(response, 'GET /browsers')\n await response.body?.cancel()\n },\n\n async orgLimits(signal) {\n const response = await send('GET', 'org/limits', {\n headers: { accept: 'application/json' },\n signal,\n })\n if (!response.ok) throw await statusFailure(response, 'GET /org/limits')\n return jsonOf(response)\n },\n\n async getPool(pool, signal) {\n const response = await send('GET', poolPath(pool), {\n headers: { accept: 'application/json' },\n signal,\n })\n if (response.status === 404) {\n await response.body?.cancel()\n return false\n }\n if (!response.ok) throw await statusFailure(response, 'GET /browser_pools/{pool}')\n await response.body?.cancel()\n return true\n },\n }\n}\n","import { setTimeout as delay } from 'node:timers/promises'\nimport {\n credential,\n definePlugin,\n PluginError,\n z,\n type AcquireInput,\n type PluginCallContext,\n} from '@vostride/agent-qa-plugin-kit'\nimport {\n createKernelApi,\n KernelBrowserSchema,\n type KernelApi,\n type PoolAcquireAnswer,\n} from './kernel-api.js'\n\nconst KERNEL_API_URL = 'https://api.onkernel.com'\n// Kernel answers 204 at once when the acquire timeout is short, so a poll waits at least this long.\nconst POOL_POLL_FLOOR_MS = 1_000\nconst LOOPBACK_HOSTS = new Set(['localhost', '[::1]'])\n\nconst options = z.strictObject({\n apiKey: credential('The Kernel API key'),\n apiUrl: z\n .url()\n .default(KERNEL_API_URL)\n .describe(\"Kernel's API, https:// unless it points at this machine\"),\n})\n\nconst settings = z.strictObject({\n headless: z\n .boolean()\n .default(true)\n .describe('Run a headless browser; Kernel offers a live view only for a headful one'),\n timeoutSeconds: z\n .int()\n .min(10)\n .max(259_200)\n .default(60)\n .describe('Seconds with no connection before Kernel deletes the browser'),\n stealth: z.boolean().optional().describe(\"Kernel's stealth mode\"),\n region: z.string().min(1).optional().describe('The Kernel region, for example eu-west'),\n proxy: z.record(z.string(), z.unknown()).optional().describe(\"Kernel's proxy, sent as given\"),\n viewport: z\n .strictObject({\n width: z.int().positive(),\n height: z.int().positive(),\n refreshRate: z.int().positive().optional(),\n })\n .optional()\n .describe('The browser window size'),\n pool: z\n .string()\n .min(1)\n .optional()\n .describe('A Kernel browser pool, by id or name, to acquire from instead of creating a browser'),\n profile: z\n .union([z.string().min(1), z.strictObject({ id: z.string().min(1) })])\n .optional()\n .describe('A Kernel profile loaded read-only, by name or as { id }; needs context: reuse'),\n extra: z\n .record(z.string(), z.unknown())\n .optional()\n .describe('Fields merged last into the create or pool acquire request, overriding the rest'),\n})\n\ntype KernelSettings = z.output<typeof settings>\ntype Session = AcquireInput<unknown, unknown>['session']\n\nfunction apiUrlAllowed(value: string): boolean {\n const url = new URL(value)\n if (url.username !== '' || url.password !== '') return false\n if (url.protocol === 'https:') return true\n if (url.protocol !== 'http:') return false\n return LOOPBACK_HOSTS.has(url.hostname) || /^127(?:\\.\\d{1,3}){3}$/.test(url.hostname)\n}\n\nconst CREATE_ONLY_SETTINGS = ['stealth', 'region', 'proxy', 'viewport'] as const\n\nfunction profileBody(input: KernelSettings, session: Session) {\n const { profile } = input\n if (profile === undefined) return undefined\n if (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 const named = typeof profile === 'string' ? { name: profile } : { id: profile.id }\n return { ...named, save_changes: false }\n}\n\nfunction createBody(input: KernelSettings, profile: object | undefined): Record<string, unknown> {\n const { viewport } = input\n return {\n headless: input.headless,\n timeout_seconds: input.timeoutSeconds,\n ...(input.stealth === undefined ? {} : { stealth: input.stealth }),\n ...(input.region === undefined ? {} : { region: input.region }),\n ...(input.proxy === undefined ? {} : { proxy: input.proxy }),\n ...(viewport === undefined\n ? {}\n : {\n viewport: {\n width: viewport.width,\n height: viewport.height,\n ...(viewport.refreshRate === undefined ? {} : { refresh_rate: viewport.refreshRate }),\n },\n }),\n ...(profile === undefined ? {} : { profile }),\n ...input.extra,\n }\n}\n\nconst HandleSchema = z.object({\n sessionId: z.string().min(1),\n pool: z.string().min(1).optional(),\n reuse: z.boolean().optional(),\n})\ntype KernelHandle = z.output<typeof HandleSchema>\n\nfunction sessionIdOf(answer: unknown): string | undefined {\n if (typeof answer !== 'object' || answer === null || !('session_id' in answer)) return undefined\n const id = answer.session_id\n return typeof id === 'string' && id !== '' ? id : undefined\n}\n\nfunction cdpAnswer(answer: unknown, place: Omit<KernelHandle, 'sessionId'>) {\n const sessionId = sessionIdOf(answer)\n const handle = sessionId === undefined ? undefined : { sessionId, ...place }\n const browser = KernelBrowserSchema.safeParse(answer)\n if (!browser.success) {\n throw new PluginError('Kernel answered a browser without a session_id and a cdp_ws_url.', {\n kind: 'refused',\n ...(handle === undefined ? {} : { handle }),\n })\n }\n const { cdp_ws_url: url, browser_live_view_url: liveView } = browser.data\n return {\n handle: { sessionId: browser.data.session_id, ...place },\n protocol: 'cdp' as const,\n url,\n mask: liveView === undefined ? [url] : [url, liveView],\n }\n}\n\nasync function acquireFromPool(\n api: KernelApi,\n pool: string,\n body: Record<string, unknown>,\n reuse: boolean,\n ctx: PluginCallContext,\n) {\n const stopped = () => new PluginError(`stopped waiting for pool ${pool}`, { kind: 'timeout' })\n // Unlike a create, a wait that ends in 204 holds no browser, so the cancel stops the wait.\n for (;;) {\n const began = Date.now()\n let answer: PoolAcquireAnswer\n try {\n answer = await api.acquireFromPool(pool, body, ctx.signal)\n } catch (error) {\n if (ctx.signal.aborted) throw stopped()\n throw error\n }\n if (answer.acquired) return cdpAnswer(answer.browser, { pool, reuse })\n if (ctx.signal.aborted) throw stopped()\n ctx.progress({ message: `waiting for pool ${pool}` })\n const rest = POOL_POLL_FLOOR_MS - (Date.now() - began)\n if (rest > 0) await delay(rest, undefined, { signal: ctx.signal }).catch(() => {})\n if (ctx.signal.aborted) throw stopped()\n }\n}\n\nconst OrgLimitsSchema = z.object({\n concurrent_sessions_available: z.int().nonnegative(),\n concurrent_sessions_used: z.int().nonnegative(),\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\nasync function limitsLine(api: KernelApi, signal: AbortSignal): Promise<string> {\n let answer: unknown\n try {\n answer = await api.orgLimits(signal)\n } catch (error) {\n if (refusedKey(error)) return 'organization limits not readable with this key'\n return `organization limits not readable: ${failureLine(error)}`\n }\n const limits = OrgLimitsSchema.safeParse(answer)\n if (!limits.success) return 'organization limits not readable: Kernel answered another shape'\n const { concurrent_sessions_available: free, concurrent_sessions_used: used } = limits.data\n return `${String(free)} of ${String(free + used)} concurrent sessions free`\n}\n\nexport default definePlugin({\n options,\n settings,\n serves: ['cdp'],\n skill: './skill',\n\n initialize({ options }) {\n if (!apiUrlAllowed(options.apiUrl)) {\n throw new PluginError('apiUrl must use https:// unless it points at this machine.', {\n kind: 'refused',\n path: 'apiUrl',\n })\n }\n },\n\n async check({ options, remotes }, ctx) {\n const api = createKernelApi(options)\n try {\n await api.listBrowsers(1, ctx.signal)\n } catch (error) {\n const line = refusedKey(error) ? 'the API key was refused' : failureLine(error)\n return { state: 'warn', lines: [line] }\n }\n const lines = ['the API key was accepted', await limitsLine(api, ctx.signal)]\n let state: 'ok' | 'warn' = 'ok'\n const pools = new Set(remotes.flatMap(({ settings }) => settings.pool ?? []))\n for (const pool of pools) {\n let found: boolean\n try {\n found = await api.getPool(pool, ctx.signal)\n } catch (error) {\n state = 'warn'\n lines.push(`pool ${pool} not readable: ${failureLine(error)}`)\n continue\n }\n if (!found) state = 'warn'\n lines.push(found ? `pool ${pool} found` : `pool ${pool} not found`)\n }\n return { state, lines }\n },\n\n async acquire({ options, settings, session }, ctx) {\n if (session.platform !== 'web' || session.engine !== 'chromium') {\n const what = session.platform === 'web' ? session.engine : session.platform\n throw new PluginError(`Kernel runs Chromium web sessions only, not ${what}.`, {\n kind: 'refused',\n })\n }\n const profile = profileBody(settings, session)\n const api = createKernelApi(options)\n if (settings.pool !== undefined) {\n const fromCreate = CREATE_ONLY_SETTINGS.find((name) => settings[name] !== undefined)\n if (fromCreate !== undefined) {\n throw new PluginError(\n `${fromCreate} comes from the pool's own configuration; remove it from a remote with pool.`,\n { kind: 'refused', path: `settings.${fromCreate}` },\n )\n }\n const body = { ...(profile === undefined ? {} : { profile }), ...settings.extra }\n const reuse = session.context !== 'reuse'\n return acquireFromPool(api, settings.pool, body, reuse, ctx)\n }\n // The create is not cancelled: once Kernel has a browser, the handle must come back to be\n // deleted, and the kit releases a handle that arrives after a cancel was answered.\n return cdpAnswer(await api.createBrowser(createBody(settings, profile)), {})\n },\n\n async release({ options, handle }, ctx) {\n const parsed = HandleSchema.safeParse(handle)\n if (!parsed.success) {\n throw new PluginError('A Kernel handle names its browser by sessionId.', { kind: 'refused' })\n }\n const { sessionId, pool, reuse } = parsed.data\n const api = createKernelApi(options)\n if (pool === undefined) await api.deleteBrowser(sessionId, ctx.signal)\n else await api.releaseToPool(pool, sessionId, reuse ?? false, 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-C2raYjWg.js";
@@ -0,0 +1,97 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "type": "object",
4
+ "properties": {
5
+ "headless": {
6
+ "default": true,
7
+ "description": "Run a headless browser; Kernel offers a live view only for a headful one",
8
+ "type": "boolean"
9
+ },
10
+ "timeoutSeconds": {
11
+ "default": 60,
12
+ "description": "Seconds with no connection before Kernel deletes the browser",
13
+ "type": "integer",
14
+ "minimum": 10,
15
+ "maximum": 259200
16
+ },
17
+ "stealth": {
18
+ "description": "Kernel's stealth mode",
19
+ "type": "boolean"
20
+ },
21
+ "region": {
22
+ "description": "The Kernel region, for example eu-west",
23
+ "type": "string",
24
+ "minLength": 1
25
+ },
26
+ "proxy": {
27
+ "description": "Kernel's proxy, sent as given",
28
+ "type": "object",
29
+ "propertyNames": {
30
+ "type": "string"
31
+ },
32
+ "additionalProperties": {}
33
+ },
34
+ "viewport": {
35
+ "description": "The browser window size",
36
+ "type": "object",
37
+ "properties": {
38
+ "width": {
39
+ "type": "integer",
40
+ "exclusiveMinimum": 0,
41
+ "maximum": 9007199254740991
42
+ },
43
+ "height": {
44
+ "type": "integer",
45
+ "exclusiveMinimum": 0,
46
+ "maximum": 9007199254740991
47
+ },
48
+ "refreshRate": {
49
+ "type": "integer",
50
+ "exclusiveMinimum": 0,
51
+ "maximum": 9007199254740991
52
+ }
53
+ },
54
+ "required": [
55
+ "width",
56
+ "height"
57
+ ],
58
+ "additionalProperties": false
59
+ },
60
+ "pool": {
61
+ "description": "A Kernel browser pool, by id or name, to acquire from instead of creating a browser",
62
+ "type": "string",
63
+ "minLength": 1
64
+ },
65
+ "profile": {
66
+ "description": "A Kernel profile loaded read-only, by name or as { id }; needs context: reuse",
67
+ "anyOf": [
68
+ {
69
+ "type": "string",
70
+ "minLength": 1
71
+ },
72
+ {
73
+ "type": "object",
74
+ "properties": {
75
+ "id": {
76
+ "type": "string",
77
+ "minLength": 1
78
+ }
79
+ },
80
+ "required": [
81
+ "id"
82
+ ],
83
+ "additionalProperties": false
84
+ }
85
+ ]
86
+ },
87
+ "extra": {
88
+ "description": "Fields merged last into the create or pool acquire request, overriding the rest",
89
+ "type": "object",
90
+ "propertyNames": {
91
+ "type": "string"
92
+ },
93
+ "additionalProperties": {}
94
+ }
95
+ },
96
+ "additionalProperties": false
97
+ }
package/package.json CHANGED
@@ -1,6 +1,71 @@
1
1
  {
2
2
  "name": "@vostride/agent-qa-plugin-kernel",
3
- "version": "0.0.1",
4
- "main": "index.js",
5
- "files": []
3
+ "version": "0.3.0",
4
+ "private": false,
5
+ "description": "agent-qa plugin for Kernel: creates a Kernel browser before a test connects and deletes it after.",
6
+ "keywords": [
7
+ "agent-qa",
8
+ "agent-qa-plugin",
9
+ "kernel"
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/kernel"
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.0"
46
+ },
47
+ "devDependencies": {
48
+ "@types/node": "~24.19.2",
49
+ "@vostride/agent-qa-arch": "0.3.0",
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,86 @@
1
+ ---
2
+ name: agent-qa-plugin-kernel
3
+ description: Use when configuring agent-qa web tests to run in Kernel cloud browsers through the Kernel plugin, or when a Kernel remote is refused.
4
+ ---
5
+
6
+ # Kernel remotes for agent-qa
7
+
8
+ The Kernel plugin gives an agent-qa web test a Kernel cloud browser. Before the test connects it
9
+ creates a browser, or takes one from a browser pool. After the test it deletes the browser, or
10
+ returns it to the pool. Kernel runs Chromium only.
11
+
12
+ ## Declare the plugin and a remote
13
+
14
+ ```yaml
15
+ plugins:
16
+ - name: kernel
17
+ package: "@vostride/agent-qa-plugin-kernel"
18
+ options:
19
+ apiKey: "{{secret:KERNEL_API_KEY}}"
20
+
21
+ registry:
22
+ remotes:
23
+ - name: kernel
24
+ plugin: kernel
25
+ settings:
26
+ headless: true
27
+ timeoutSeconds: 60
28
+ ```
29
+
30
+ - The `plugins` entry holds the account: `apiKey`, and `apiUrl`, which defaults to Kernel's API.
31
+ Write the key as a `{{secret:<name>}}` reference to a name in the file `workspace.secretsFile`
32
+ 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
+ Kernel settings.
35
+ - `use.browser.remote: kernel` 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-kernel@<agent-qa version>`.
38
+ `agent-qa plugins list` prints that line when the package is missing or the version differs.
39
+
40
+ ## Settings
41
+
42
+ - `headless` (default `true`): `false` gives a headful browser.
43
+ - `timeoutSeconds` (default `60`, 10 to 259200): how long Kernel keeps the browser once agent-qa's
44
+ connection drops.
45
+ - `stealth`, `region`, `proxy`, `viewport` (`width`, `height`, `refreshRate`): Kernel's own create
46
+ fields.
47
+ - `pool`: a browser pool's id or name. The remote then takes a warm browser from the pool instead
48
+ of creating one, and waits while the pool is empty. The pool's configuration sets the browser,
49
+ so `stealth`, `region`, `proxy` and `viewport` are refused on a remote with `pool`.
50
+ - `profile`: a Kernel profile's name, or `{ id: <profile id> }`. It loads read-only: nothing the
51
+ test does is saved back. It needs `context: reuse` on the remote, because the profile lives in
52
+ the browser's first context:
53
+
54
+ ```yaml
55
+ - name: kernel-signed-in
56
+ plugin: kernel
57
+ context: reuse
58
+ settings:
59
+ profile: shop-test-user
60
+ ```
61
+
62
+ - `extra`: any other Kernel request field. It is merged last, so it overrides the settings above.
63
+
64
+ After a test in a new context, a pooled browser goes back to the pool for reuse. After a test with
65
+ `context: reuse`, it is discarded, so the next test gets a clean one.
66
+
67
+ Kernel has no session page, so a Kernel test shows no session link. The live view URL of a headful
68
+ browser carries a credential, so agent-qa masks it and never shows it.
69
+
70
+ ## When a remote is refused
71
+
72
+ - `auth`: Kernel refused the API key. Check the secret's value.
73
+ - `capacity`: Kernel answered 429 or 529. The account has no free sessions, or is rate limited.
74
+ Wait, lower the run's concurrency, or use a pool.
75
+ - `refused` with path `settings.profile`: the remote sets `profile` without `context: reuse`.
76
+ - `refused` with another `settings.<field>` path: that field is wrong for this remote. The line
77
+ says why.
78
+ - `unreachable`: Kernel could not be reached. Check the network.
79
+
80
+ ## Check and describe
81
+
82
+ - `agent-qa doctor` runs the plugin's account check. It reads the browser list to prove the key,
83
+ the organization's free sessions when the key may read them, and each pool a remote names. It
84
+ creates no browser.
85
+ - `agent-qa plugins describe <entry> --json` lists every option and setting with its type,
86
+ default and description.