@jrpool/kilotest 39.0.1 → 40.0.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/api/requestFeature.js +5 -5
- package/api/routes.js +1 -1
- package/api/schemas.js +10 -4
- package/api/testSuite.js +9 -5
- package/docs/SERVICE.md +46 -2
- package/index.js +2 -2
- package/openapi.yaml +9 -9
- package/package.json +1 -1
- package/scripts/generate-openapi.js +1 -0
- package/CLAUDE.md +0 -5
package/api/requestFeature.js
CHANGED
|
@@ -12,13 +12,13 @@ const {sendAlert} = require('../alerts');
|
|
|
12
12
|
|
|
13
13
|
// Returns the response body.
|
|
14
14
|
exports.response = async args => {
|
|
15
|
-
const [
|
|
15
|
+
const [feature = ''] = args;
|
|
16
16
|
// Initialize the response content.
|
|
17
17
|
const responseContent = {
|
|
18
18
|
'details about your request': null
|
|
19
19
|
};
|
|
20
|
-
// If the
|
|
21
|
-
if (!
|
|
20
|
+
// If the requested feature or improvement is empty:
|
|
21
|
+
if (!feature) {
|
|
22
22
|
// Add this to the response content.
|
|
23
23
|
responseContent['details about your request'] = {
|
|
24
24
|
error: 'request invalid: request is empty'
|
|
@@ -27,7 +27,7 @@ exports.response = async args => {
|
|
|
27
27
|
// Otherwise, i.e. if it exists:
|
|
28
28
|
else {
|
|
29
29
|
// Notify the manager.
|
|
30
|
-
await sendAlert('MCP feature request received',
|
|
30
|
+
await sendAlert('MCP feature request received', feature);
|
|
31
31
|
// Add the disposition to the response content.
|
|
32
32
|
responseContent['details about your request'] = {
|
|
33
33
|
'date and time received': new Date().toISOString(),
|
|
@@ -43,7 +43,7 @@ exports.response = async args => {
|
|
|
43
43
|
method: 'POST',
|
|
44
44
|
URL: `${thisHost}/api/requestFeature`,
|
|
45
45
|
body: {
|
|
46
|
-
|
|
46
|
+
feature
|
|
47
47
|
},
|
|
48
48
|
'closest ancestor request': null
|
|
49
49
|
},
|
package/api/routes.js
CHANGED
|
@@ -90,7 +90,7 @@ module.exports = [
|
|
|
90
90
|
operationId: 'requestFeature',
|
|
91
91
|
method: 'post',
|
|
92
92
|
path: '/api/requestFeature',
|
|
93
|
-
summary: 'Request a
|
|
93
|
+
summary: 'Request a feature improvement or new feature',
|
|
94
94
|
bodySchema: z.object(requestFeatureSchema),
|
|
95
95
|
responseSchema: requestFeatureResponseSchema
|
|
96
96
|
}
|
package/api/schemas.js
CHANGED
|
@@ -52,7 +52,7 @@ exports.requestRetestSchema = {
|
|
|
52
52
|
|
|
53
53
|
// requestFeature: POST /api/requestFeature
|
|
54
54
|
exports.requestFeatureSchema = {
|
|
55
|
-
|
|
55
|
+
feature: z.string().describe('description of requested feature improvement or new feature')
|
|
56
56
|
};
|
|
57
57
|
|
|
58
58
|
// listReports takes no input; omitted (mcp.js already uses inputSchema: {}).
|
|
@@ -105,11 +105,16 @@ const thisRequestSchema = (method, bodySchema) => z.object({
|
|
|
105
105
|
'closest ancestor request': requestReferenceSchema.nullable()
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
-
const envelope = (
|
|
108
|
+
const envelope = (
|
|
109
|
+
method,
|
|
110
|
+
responseContentSchema,
|
|
111
|
+
bodySchema,
|
|
112
|
+
/** @type {z.ZodType} */ similarWebSchema = similarWebRequestsSchema
|
|
113
|
+
) => z.object({
|
|
109
114
|
'tool collection': toolsFactsSchema,
|
|
110
115
|
'tool name': z.string(),
|
|
111
116
|
'this request': thisRequestSchema(method, bodySchema),
|
|
112
|
-
'URLs of similar requests for web users':
|
|
117
|
+
'URLs of similar requests for web users': similarWebSchema,
|
|
113
118
|
'response metadata': responseMetadataSchema,
|
|
114
119
|
'response content': responseContentSchema
|
|
115
120
|
});
|
|
@@ -293,5 +298,6 @@ exports.requestFeatureResponseSchema = envelope(
|
|
|
293
298
|
disposition: z.string()
|
|
294
299
|
})
|
|
295
300
|
])}),
|
|
296
|
-
z.object(exports.requestFeatureSchema)
|
|
301
|
+
z.object(exports.requestFeatureSchema),
|
|
302
|
+
z.null()
|
|
297
303
|
);
|
package/api/testSuite.js
CHANGED
|
@@ -272,13 +272,17 @@ const requestService = async () => {
|
|
|
272
272
|
return;
|
|
273
273
|
}
|
|
274
274
|
console.log('======================\nRequest: Make a feature request');
|
|
275
|
-
path =
|
|
275
|
+
path = '/api/requestFeature';
|
|
276
|
+
const featureRequest = 'Do the impossible';
|
|
276
277
|
body = await submitRequest(path, method, {
|
|
277
|
-
feature:
|
|
278
|
+
feature: featureRequest
|
|
278
279
|
});
|
|
279
|
-
|
|
280
|
-
requestDetails =
|
|
281
|
-
if (
|
|
280
|
+
const feature = body?.['this request']?.body?.feature ?? {};
|
|
281
|
+
requestDetails = body?.['response content']?.['details about your request'] ?? {};
|
|
282
|
+
if (
|
|
283
|
+
feature !== featureRequest
|
|
284
|
+
|| !requestDetails.disposition?.includes('received and logged')
|
|
285
|
+
) {
|
|
282
286
|
console.log(`requestDetails: ${JSON.stringify(requestDetails, null, 2)}`);
|
|
283
287
|
return;
|
|
284
288
|
}
|
package/docs/SERVICE.md
CHANGED
|
@@ -280,6 +280,50 @@ Experimentation revealed that a high-frequency instance could decrease the elaps
|
|
|
280
280
|
|
|
281
281
|
## Security
|
|
282
282
|
|
|
283
|
+
### Report protection
|
|
284
|
+
|
|
285
|
+
Jobs and reports are not tracked, so there are no duplicates in any other copy of the repository. Jobs are typically ephemeral, but reports typically remain in existence until deemed obsolete and useless even for historical comparison. Therefore, reports risk deletion unless duplicates are made externally. Other files, including application code, dependencies, and `db/reportsExtract.json`, are not at risk, because they can be pushed from the local repository or regenerated.
|
|
286
|
+
|
|
287
|
+
Reports created on the deployed server are currently protected with an external archive at the [Cloudflare R2 object storage service](https://developers.cloudflare.com/r2/). On that service, the current Kilotest maintainer has an account, subscribes to the R2 service, has created two _buckets_ named `kilotest-reports` and `kilotest-hidden-reports`, and has created an API token scoped to those buckets with object read-write permissions, access and secret access keys for S3 clients, and a restriction to the IPv4 and IPv6 addresses of the server.
|
|
288
|
+
|
|
289
|
+
The server host uses `rclone` for file synchronization with external storage locations. Two files on the server host enable `rclone` for use by Kilotest. One file is `/home/linuxuser/.config/rclone/rclone.conf`. Its content, with secrets replaced, is:
|
|
290
|
+
|
|
291
|
+
```ini
|
|
292
|
+
[r2]
|
|
293
|
+
type = s3
|
|
294
|
+
provider = Cloudflare
|
|
295
|
+
access_key_id = <access_key_id>
|
|
296
|
+
secret_access_key = <secret_access_key>
|
|
297
|
+
endpoint = https://<account_id>.r2.cloudflarestorage.com
|
|
298
|
+
acl = private
|
|
299
|
+
no_head = true
|
|
300
|
+
no_check_bucket = true
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
The other file is the `crontab` configuration file for `linuxuser`, which is managed with `crontab -e`. The commands in that file (shown with `crontab -l`) are:
|
|
304
|
+
|
|
305
|
+
```crontab
|
|
306
|
+
11 9 * * * /usr/bin/rclone copy /opt/jpdev/kilotest/db/hiddenReports r2:kilotest-hidden-reports/ -v --backup-dir r2:kilotest-hidden-reports-old/$(date +\%F) > /home/linuxuser/kilotest-to-r2.log 2>&1
|
|
307
|
+
12 9 * * * /usr/bin/rclone copy /opt/jpdev/kilotest/db/reports r2:kilotest-reports/ -v --backup-dir r2:kilotest-reports-old/$(date +\%F) >> /home/linuxuser/kilotest-to-r2.log 2>&1
|
|
308
|
+
```
|
|
309
|
+
|
|
310
|
+
This configuration every morning (UTC) copies reports to R2 as follows:
|
|
311
|
+
|
|
312
|
+
- Reports already on R2 are not copied again.
|
|
313
|
+
- Reports not yet on R2 are copied to it.
|
|
314
|
+
- Reports on R2 that differ from and are older than the same-name reports on the server are moved to the corresponding `-old` directory, and the newer reports are copied to R2.
|
|
315
|
+
|
|
316
|
+
Each daily copy operation is logged in `/home/linuxuser/kilotest-to-r2.log`. That file is replaced with a new one each day documenting only the operation on that day.
|
|
317
|
+
|
|
318
|
+
If restoration becomes necessary, the files can be copied in the opposite direction with:
|
|
319
|
+
|
|
320
|
+
```bash
|
|
321
|
+
/usr/bin/rclone copy r2:kilotest-reports /opt/jpdev/kilotest/db/restored-reports -v
|
|
322
|
+
/usr/bin/rclone copy r2:kilotest-hidden-reports /opt/jpdev/kilotest/db/restored-hidden-reports -v
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
After that the maintainer can inspect the original and restored directories and delete or move report files as needed. After that the `db/reportsExtract.json` file should be deleted, so it will be regenerated when next needed.
|
|
326
|
+
|
|
283
327
|
### Possible future Testaro integration
|
|
284
328
|
|
|
285
329
|
Kilotest uses Testaro to run jobs. In previous versions of Kilotest, Testaro was a dependency. It is currently not a dependenc. Instead, Testaro instances are installed on one or more other hosts, and each instance polls Kilotest to ask for jobs to run.
|
|
@@ -292,7 +336,7 @@ Testaro uses Playwright to launch and control headless browsers, often `chromium
|
|
|
292
336
|
|
|
293
337
|
When either Playwright or Puppeteer launches a `chromium` browser, in most environments it is [sandboxed](https://www.geeksforgeeks.org/ethical-hacking/what-is-browser-sandboxing/). Sandboxing is a security feature that prevents the browser from accessing potentially unsafe system resources. But in the Ubuntu Linux operating system that was installed on the Vultr Cloud Compute host a sandboxed browser requires an [unprivileged user namespace](https://ubuntu.com/blog/ubuntu-23-10-restricted-unprivileged-user-namespaces), and when Ubuntu was installed its configuration disallowed such namespaces. The file `/etc/sysctl.d/99-kilotest-userns.conf` with the content `kernel.apparmor_restrict_unprivileged_userns = 1` prohibited unprivileged user namespaces and thereby made sandboxed browsers unlaunchable.
|
|
294
338
|
|
|
295
|
-
|
|
339
|
+
#### Potential modification
|
|
296
340
|
|
|
297
341
|
One modification to cope with this prohibition on the Vultr Cloud Compute host would be to configure Playwright and Puppeteer to launch `chromium` non-sandboxed. In both cases, launch arguments `'--no-sandbox'` and `'--disable-setuid-sandbox'` are available to specify this.
|
|
298
342
|
|
|
@@ -300,7 +344,7 @@ One modification to cope with this prohibition on the Vultr Cloud Compute host w
|
|
|
300
344
|
- For the `qualWeb` tool, this would be done in the Testaro `tests/qualweb.js` file, where the `qualWeb.start` method is called with an options argument. Its `args` array property would include `'--no-sandbox'` and `'--disable-setuid-sandbox'`.
|
|
301
345
|
- The `ibm` tool, too, could launch a Puppeteer `chromium` browser, if page content instead of a Playwright page were passed to the `accessibilityChecker.getCompliance` method, or if the implementation of the tool were changed in the future. For anticipation of such a case, the Testaro `aceconfig.js` file, of which a copy has been created at the root of the Kilotest project, would be modified. That file defines a `module.exports` object with a `puppeteerArgs` property, and, `--no-sandbox` and `--disable-setuid-sandbox` would be added to its array value.
|
|
302
346
|
|
|
303
|
-
|
|
347
|
+
#### Current modification
|
|
304
348
|
|
|
305
349
|
However, non-sandboxed browsers are less secure than sandboxed ones, particularly when there is no restriction on who can use the service and what web pages they can test with it. Such restrictions are currently in place, but still permit testing of arbitrary web pages and may be relaxed in the future. Therefore, the potential modification described above would introduce nontrivial risk. An alternative solution was adopted instead. In it, the `chromium` configuration was left unchanged.
|
|
306
350
|
|
package/index.js
CHANGED
|
@@ -750,10 +750,10 @@ const requestHandler = async (request, response) => {
|
|
|
750
750
|
}
|
|
751
751
|
// Otherwise, if the service is to receive a feature request:
|
|
752
752
|
else if (segments[0] === 'requestFeature') {
|
|
753
|
-
const {
|
|
753
|
+
const {feature} = postData;
|
|
754
754
|
// Get the response body.
|
|
755
755
|
const responseBody = await require(path.join(__dirname, 'api', 'requestFeature'))
|
|
756
|
-
.response(
|
|
756
|
+
.response([feature]);
|
|
757
757
|
// Send it.
|
|
758
758
|
setHeaders('application/json', null, 'ultra');
|
|
759
759
|
response.end(JSON.stringify(responseBody));
|
package/openapi.yaml
CHANGED
|
@@ -2,7 +2,7 @@ openapi: 3.1.0
|
|
|
2
2
|
info:
|
|
3
3
|
title: Kilotest API
|
|
4
4
|
description: 'Kilotest is an ensemble testing and reporting service for front-end web quality. It uses an ensemble of 10 rule engines to test public web pages for front-end quality (accessibility, usability, and standards conformity). Kilotest acts as a web server for human users, an MCP server for AI platforms, and an API for programmatic access. With this API you can request that a page be tested or retested, discover available reports, incrementally retrieve facts from a report: from what reports are available, to what issues were reported in one report, to what elements violated one issue, to what diagnoses rule engines gave for one violation.'
|
|
5
|
-
version: 39.0.
|
|
5
|
+
version: 39.0.3
|
|
6
6
|
license:
|
|
7
7
|
name: MIT
|
|
8
8
|
identifier: MIT
|
|
@@ -1045,21 +1045,21 @@ paths:
|
|
|
1045
1045
|
/api/requestFeature:
|
|
1046
1046
|
post:
|
|
1047
1047
|
operationId: requestFeature
|
|
1048
|
-
summary: Request a
|
|
1048
|
+
summary: Request a feature improvement or new feature
|
|
1049
1049
|
requestBody:
|
|
1050
1050
|
content:
|
|
1051
1051
|
application/json:
|
|
1052
1052
|
schema:
|
|
1053
1053
|
type: object
|
|
1054
1054
|
properties:
|
|
1055
|
-
|
|
1055
|
+
feature:
|
|
1056
1056
|
type: string
|
|
1057
|
-
description: description of requested
|
|
1057
|
+
description: description of requested feature improvement or new feature
|
|
1058
1058
|
required:
|
|
1059
|
-
-
|
|
1059
|
+
- feature
|
|
1060
1060
|
responses:
|
|
1061
1061
|
'200':
|
|
1062
|
-
description: Request a
|
|
1062
|
+
description: Request a feature improvement or new feature
|
|
1063
1063
|
content:
|
|
1064
1064
|
application/json:
|
|
1065
1065
|
schema:
|
|
@@ -1082,11 +1082,11 @@ paths:
|
|
|
1082
1082
|
body:
|
|
1083
1083
|
type: object
|
|
1084
1084
|
properties:
|
|
1085
|
-
|
|
1085
|
+
feature:
|
|
1086
1086
|
type: string
|
|
1087
|
-
description: description of requested
|
|
1087
|
+
description: description of requested feature improvement or new feature
|
|
1088
1088
|
required:
|
|
1089
|
-
-
|
|
1089
|
+
- feature
|
|
1090
1090
|
additionalProperties: false
|
|
1091
1091
|
closest ancestor request:
|
|
1092
1092
|
anyOf:
|
package/package.json
CHANGED
|
@@ -20,6 +20,7 @@ const outputPath = path.join(__dirname, '..', 'openapi.yaml');
|
|
|
20
20
|
|
|
21
21
|
// Builds the paths object for createDocument from the route table.
|
|
22
22
|
const buildPaths = () => {
|
|
23
|
+
/** @type {import('zod-openapi').ZodOpenApiPathsObject} */
|
|
23
24
|
const paths = {};
|
|
24
25
|
routes.forEach(route => {
|
|
25
26
|
const {method, path: routePath, summary, pathParamsSchema, bodySchema, responseSchema} = route;
|
package/CLAUDE.md
DELETED
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
# Cascade Rules
|
|
2
|
-
|
|
3
|
-
- Ask mode: If the current mode is Ask, answer questions and provide code, diffs, or edit suggestions in code blocks or as plain text. Do not invite the user to switch to Normal or Code mode or any mode that permits writing files.
|
|
4
|
-
- Comply with the current convention to omit line breaks in long lines in `.md` files and in code comments.
|
|
5
|
-
- In conversation responses (not files), when a Markdown table cell would contain a line longer than 120 characters divided by the number of columns, insert `<br>` tags to keep displayed line lengths shorter than that maximum, since raw newlines are not valid inside table cells. If this rule makes any table cumbersome, use a non-tabular format, such as a list, instead.
|