@cat-factory/executor-harness 1.98.0 → 1.100.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/dist/agent-capabilities.js +25 -11
- package/package.json +4 -4
- package/src/agent-capabilities.ts +22 -13
|
@@ -277,19 +277,33 @@ export const MCP_TOOL_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
|
277
277
|
* reached the container by any other route is held to the rule too.
|
|
278
278
|
*/
|
|
279
279
|
export function isAllowedMcpHttpUrl(raw) {
|
|
280
|
-
|
|
281
|
-
|
|
280
|
+
// ASCII control characters and the space are refused ANYWHERE rather than canonicalised: the
|
|
281
|
+
// WHATWG parser trims leading/trailing C0-and-space and removes tab, LF and CR from anywhere, so
|
|
282
|
+
// a url carrying one parses to something other than what it reads as, and this url is written
|
|
283
|
+
// VERBATIM into the CLI's MCP config below.
|
|
284
|
+
for (let i = 0; i < raw.length; i += 1)
|
|
285
|
+
if (raw.charCodeAt(i) <= 0x20)
|
|
286
|
+
return false;
|
|
287
|
+
let parsed;
|
|
288
|
+
try {
|
|
289
|
+
parsed = new URL(raw);
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
// silent-catch-ok: an unparseable url is exactly what this predicate refuses.
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:')
|
|
282
296
|
return false;
|
|
283
|
-
if (
|
|
297
|
+
if (parsed.protocol === 'https:')
|
|
284
298
|
return true;
|
|
285
|
-
// Plain http from here: the host must be loopback.
|
|
286
|
-
//
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
299
|
+
// Plain http from here: the host must be loopback. `new URL` rather than a hand-written parse
|
|
300
|
+
// because it is the parser the CLI resolves the request with, so the host ruled on is the host
|
|
301
|
+
// the credential header travels to. It strips userinfo from the LAST `@` (or
|
|
302
|
+
// `http://127.0.0.1@evil.example` reads as loopback while the request goes to evil.example), and
|
|
303
|
+
// it terminates the authority at a backslash as well as at `/?#` (or
|
|
304
|
+
// `http://evil.example\@127.0.0.1` reads as loopback the same way).
|
|
305
|
+
const hostname = parsed.hostname;
|
|
306
|
+
const host = hostname.startsWith('[') ? hostname.slice(1, -1) : hostname;
|
|
293
307
|
return host === 'localhost' || host === '::1' || /^127\.\d+\.\d+\.\d+$/.test(host);
|
|
294
308
|
}
|
|
295
309
|
/** A string→string record, dropping any non-string entry. Undefined when nothing survives. */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cat-factory/executor-harness",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.100.0",
|
|
4
4
|
"description": "Container payload: a thin TypeScript wrapper that runs the Pi coding agent against a cloned repo and opens a PR. Runs in the Cloudflare Container (and, in local native mode, as a host process); carries no secrets.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
"hono": "^4.13.0",
|
|
31
31
|
"typescript": "7.0.2",
|
|
32
32
|
"vitest": "^4.1.10",
|
|
33
|
-
"@cat-factory/kernel": "0.
|
|
34
|
-
"@cat-factory/server": "0.
|
|
35
|
-
"@cat-factory/spend": "0.15.
|
|
33
|
+
"@cat-factory/kernel": "0.272.0",
|
|
34
|
+
"@cat-factory/server": "0.252.0",
|
|
35
|
+
"@cat-factory/spend": "0.15.40"
|
|
36
36
|
},
|
|
37
37
|
"scripts": {
|
|
38
38
|
"build": "tsc -p tsconfig.json",
|
|
@@ -382,19 +382,28 @@ export const MCP_TOOL_NAME_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/
|
|
|
382
382
|
* reached the container by any other route is held to the rule too.
|
|
383
383
|
*/
|
|
384
384
|
export function isAllowedMcpHttpUrl(raw: string): boolean {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
//
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
385
|
+
// ASCII control characters and the space are refused ANYWHERE rather than canonicalised: the
|
|
386
|
+
// WHATWG parser trims leading/trailing C0-and-space and removes tab, LF and CR from anywhere, so
|
|
387
|
+
// a url carrying one parses to something other than what it reads as, and this url is written
|
|
388
|
+
// VERBATIM into the CLI's MCP config below.
|
|
389
|
+
for (let i = 0; i < raw.length; i += 1) if (raw.charCodeAt(i) <= 0x20) return false
|
|
390
|
+
let parsed: URL
|
|
391
|
+
try {
|
|
392
|
+
parsed = new URL(raw)
|
|
393
|
+
} catch {
|
|
394
|
+
// silent-catch-ok: an unparseable url is exactly what this predicate refuses.
|
|
395
|
+
return false
|
|
396
|
+
}
|
|
397
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') return false
|
|
398
|
+
if (parsed.protocol === 'https:') return true
|
|
399
|
+
// Plain http from here: the host must be loopback. `new URL` rather than a hand-written parse
|
|
400
|
+
// because it is the parser the CLI resolves the request with, so the host ruled on is the host
|
|
401
|
+
// the credential header travels to. It strips userinfo from the LAST `@` (or
|
|
402
|
+
// `http://127.0.0.1@evil.example` reads as loopback while the request goes to evil.example), and
|
|
403
|
+
// it terminates the authority at a backslash as well as at `/?#` (or
|
|
404
|
+
// `http://evil.example\@127.0.0.1` reads as loopback the same way).
|
|
405
|
+
const hostname = parsed.hostname
|
|
406
|
+
const host = hostname.startsWith('[') ? hostname.slice(1, -1) : hostname
|
|
398
407
|
return host === 'localhost' || host === '::1' || /^127\.\d+\.\d+\.\d+$/.test(host)
|
|
399
408
|
}
|
|
400
409
|
|