@anchrd/intel-api 0.9.0 → 0.10.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.
@@ -1,31 +1,77 @@
1
- import Ajv, {} from "ajv";
2
- // Tool schemas come from remote catalogs and are therefore untrusted: allErrors lets a crafted
3
- // schema amplify validation work, and Ajv keeps every compiled schema for the life of the isolate.
4
- const MaxCompiledSchemas = 200;
1
+ import { Validator } from "@cfworker/json-schema";
2
+ import { reportUnexpectedError } from "../../shared/report-unexpected-error/report-unexpected-error.js";
3
+ // ⚠️ This validator must not generate code, and Ajv must never come back. Ajv turns a schema into
4
+ // JavaScript with `new Function`, and workerd answers that with `EvalError: Code generation from
5
+ // strings disallowed for this context` — so `compile` threw for EVERY schema in production, the
6
+ // catch called it a broken schema, and each tool call died with 400 "The cached tool schema is
7
+ // invalid" while nothing was wrong with any schema (#229). Ajv is usable only with schemas
8
+ // precompiled at build time; these arrive from a remote catalog and are unknown until the call.
9
+ // `@cfworker/json-schema` interprets the schema instead, which is what it was written for.
10
+ //
11
+ // ⚠️ The `workerd` test pool does not prove this on its own: it replaces `globalThis.Function` with
12
+ // a proxy onto an unsafe-eval binding, so Ajv compiled happily in every test while failing in every
13
+ // deployment. `json-schema.int.ts` therefore takes that patch away for the length of the test — it
14
+ // is the only thing that turns red when a code-generating validator returns.
15
+ // Tool schemas come from remote catalogs and are therefore untrusted: `ShortCircuit` stops at the
16
+ // first error so a crafted schema cannot amplify the work of one call, and the prepared validators
17
+ // are bounded because each one holds its dereferenced schema for the life of the isolate.
18
+ const MaxPreparedSchemas = 200;
19
+ const ShortCircuit = true;
20
+ const MaxDetailLength = 2_000;
21
+ // A tool schema that names its draft gets that draft: draft-07 ignores keywords sitting beside a
22
+ // `$ref` and the later drafts apply them, so guessing here would validate a foreign catalog against
23
+ // rules its author never wrote. Unnamed means 2020-12, the draft the MCP generators emit when they
24
+ // stay silent.
25
+ const DefaultDraft = "2020-12";
26
+ const DraftsByMetaSchema = [
27
+ ["draft-04", "4"],
28
+ ["draft-07", "7"],
29
+ ["2019-09", "2019-09"],
30
+ ["2020-12", "2020-12"],
31
+ ];
32
+ function draftOf(schema) {
33
+ const declared = schema.$schema;
34
+ if (typeof declared !== "string")
35
+ return DefaultDraft;
36
+ return DraftsByMetaSchema.find(([marker]) => declared.includes(marker))?.[1] ?? DefaultDraft;
37
+ }
5
38
  export function createJsonSchemaValidator() {
6
- const ajv = new Ajv({ allErrors: false, strict: false });
7
- const compiled = new Map();
39
+ const prepared = new Map();
8
40
  return (schema, value) => {
9
- try {
10
- const key = JSON.stringify(schema);
11
- let validate = compiled.get(key);
12
- if (!validate) {
13
- if (compiled.size >= MaxCompiledSchemas) {
14
- compiled.clear();
15
- ajv.removeSchema();
16
- }
17
- validate = ajv.compile(schema);
18
- compiled.set(key, validate);
41
+ const key = JSON.stringify(schema);
42
+ let validator = prepared.get(key);
43
+ if (!validator) {
44
+ try {
45
+ validator = new Validator(schema, draftOf(schema), ShortCircuit);
46
+ }
47
+ catch (error) {
48
+ // A schema this validator cannot even read is still refused by name — but the caller hears
49
+ // that it was the schema, not their arguments, and the reason itself goes to the log. The
50
+ // old wording claimed a broken schema for every failure, which is what hid #229 for weeks.
51
+ reportUnexpectedError(error);
52
+ return { valid: false, detail: "The tool schema could not be read; the reason was logged" };
19
53
  }
20
- if (validate(value))
54
+ if (prepared.size >= MaxPreparedSchemas)
55
+ prepared.clear();
56
+ prepared.set(key, validator);
57
+ }
58
+ try {
59
+ const result = validator.validate(value);
60
+ if (result.valid)
21
61
  return { valid: true };
22
- return {
23
- valid: false,
24
- detail: ajv.errorsText(validate.errors, { separator: "; " }).slice(0, 2_000),
25
- };
62
+ return { valid: false, detail: describe(result) };
26
63
  }
27
- catch {
28
- return { valid: false, detail: "The cached tool schema is invalid" };
64
+ catch (error) {
65
+ // An unresolvable `$ref` only shows up while walking the instance, so this is the same
66
+ // unreadable schema arriving one step later — and never a statement about the arguments.
67
+ reportUnexpectedError(error);
68
+ return { valid: false, detail: "The tool schema could not be read; the reason was logged" };
29
69
  }
30
70
  };
31
71
  }
72
+ function describe(result) {
73
+ return result.errors
74
+ .map((unit) => `${unit.instanceLocation}: ${unit.error}`)
75
+ .join("; ")
76
+ .slice(0, MaxDetailLength);
77
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@anchrd/intel-api",
3
- "version": "0.9.0",
3
+ "version": "0.10.0",
4
4
  "type": "module",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
@@ -44,8 +44,8 @@
44
44
  "dependencies": {
45
45
  "@anchrd/gate-sdk": "^0.7.0",
46
46
  "@anchrd/intel-contract": "^0.7.0",
47
+ "@cfworker/json-schema": "^4.1.1",
47
48
  "@modelcontextprotocol/sdk": "^1.30.0",
48
- "ajv": "^8.20.0",
49
49
  "fflate": "^0.8.3",
50
50
  "hono": "^4.12.32",
51
51
  "openid-client": "^6.8.4",