@vantageos/mcp-doctor 0.8.0 → 0.8.1
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/CHANGELOG.md +82 -0
- package/README.md +31 -2
- package/dist/doctor.d.ts +64 -1
- package/dist/doctor.js +1288 -322
- package/package.json +6 -4
package/dist/doctor.js
CHANGED
|
@@ -6,7 +6,7 @@ import { dirname as dirname17, join as join27, resolve as resolve3 } from "path"
|
|
|
6
6
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
7
7
|
|
|
8
8
|
// src/scan.ts
|
|
9
|
-
import { existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
9
|
+
import { constants, accessSync, existsSync, readFileSync, readdirSync, statSync } from "fs";
|
|
10
10
|
import { basename, dirname, join, relative, sep } from "path";
|
|
11
11
|
var SKIP_DIR_INFO = {
|
|
12
12
|
node_modules: { reason: "vendored dependencies", authored: false },
|
|
@@ -46,11 +46,12 @@ function isCommentLine(raw) {
|
|
|
46
46
|
}
|
|
47
47
|
var UNSCANNED_RE = /\.(?:m?js|cjs|jsx|mts|cts)$/;
|
|
48
48
|
var NOT_PRODUCT_NAME_RE = /\.d\.[mc]?ts$|\.(?:test|spec)\.[mc]?[jt]sx?$|\.config\.[mc]?[jt]s$|\.min\./;
|
|
49
|
-
function walk(dir, out, onSkip, unscanned = []) {
|
|
49
|
+
function walk(dir, out, onSkip, unscanned = [], seen = { files: 0 }) {
|
|
50
50
|
let entries;
|
|
51
51
|
try {
|
|
52
52
|
entries = readdirSync(dir);
|
|
53
|
-
} catch {
|
|
53
|
+
} catch (err) {
|
|
54
|
+
onSkip(dir, `could not be listed (${fsErrorCode(err)}) \u2014 not read`);
|
|
54
55
|
return;
|
|
55
56
|
}
|
|
56
57
|
for (const name of entries) {
|
|
@@ -58,7 +59,8 @@ function walk(dir, out, onSkip, unscanned = []) {
|
|
|
58
59
|
let st;
|
|
59
60
|
try {
|
|
60
61
|
st = statSync(full);
|
|
61
|
-
} catch {
|
|
62
|
+
} catch (err) {
|
|
63
|
+
onSkip(full, `could not be read (${fsErrorCode(err)}) \u2014 not read`);
|
|
62
64
|
continue;
|
|
63
65
|
}
|
|
64
66
|
if (st.isDirectory()) {
|
|
@@ -66,8 +68,21 @@ function walk(dir, out, onSkip, unscanned = []) {
|
|
|
66
68
|
onSkip(full);
|
|
67
69
|
continue;
|
|
68
70
|
}
|
|
69
|
-
walk(full, out, onSkip, unscanned);
|
|
70
|
-
|
|
71
|
+
walk(full, out, onSkip, unscanned, seen);
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (!st.isFile()) {
|
|
75
|
+
onSkip(full, "not a regular file (FIFO, socket or device) \u2014 not read");
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
accessSync(full, constants.R_OK);
|
|
80
|
+
} catch (err) {
|
|
81
|
+
onSkip(full, `could not be read (${fsErrorCode(err)}) \u2014 not read`);
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
seen.files++;
|
|
85
|
+
if (UNSCANNED_RE.test(name)) {
|
|
71
86
|
if (!NOT_PRODUCT_NAME_RE.test(name)) unscanned.push(full);
|
|
72
87
|
} else if (name.endsWith(".ts") || name.endsWith(".tsx")) {
|
|
73
88
|
if (name.endsWith(".d.ts") || name.endsWith(".test.ts")) continue;
|
|
@@ -75,6 +90,22 @@ function walk(dir, out, onSkip, unscanned = []) {
|
|
|
75
90
|
}
|
|
76
91
|
}
|
|
77
92
|
}
|
|
93
|
+
function fsErrorCode(err) {
|
|
94
|
+
const code = err?.code;
|
|
95
|
+
return typeof code === "string" ? code : String(err);
|
|
96
|
+
}
|
|
97
|
+
var SubjectUnreadableError = class extends Error {
|
|
98
|
+
target;
|
|
99
|
+
form;
|
|
100
|
+
constructor(target, form) {
|
|
101
|
+
super(
|
|
102
|
+
`mcp-doctor: could not judge ${target} \u2014 ${form}. Nothing was read, so no verdict is given (exit 2).`
|
|
103
|
+
);
|
|
104
|
+
this.name = "SubjectUnreadableError";
|
|
105
|
+
this.target = target;
|
|
106
|
+
this.form = form;
|
|
107
|
+
}
|
|
108
|
+
};
|
|
78
109
|
function loadFile(path, root) {
|
|
79
110
|
const raw = readFileSync(path, "utf8");
|
|
80
111
|
const lines = raw.split("\n").map((r, i) => ({ n: i + 1, raw: r, comment: isCommentLine(r) }));
|
|
@@ -103,6 +134,9 @@ function findPackageJson(startDir) {
|
|
|
103
134
|
const candidate = join(dir, "package.json");
|
|
104
135
|
if (existsSync(candidate)) {
|
|
105
136
|
try {
|
|
137
|
+
if (!statSync(candidate).isFile()) {
|
|
138
|
+
return { path: candidate, json: {}, unreadable: "not a regular file" };
|
|
139
|
+
}
|
|
106
140
|
const parsed = JSON.parse(readFileSync(candidate, "utf8"));
|
|
107
141
|
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
108
142
|
return { path: candidate, json: {}, unreadable: "not a JSON object" };
|
|
@@ -119,16 +153,67 @@ function findPackageJson(startDir) {
|
|
|
119
153
|
return null;
|
|
120
154
|
}
|
|
121
155
|
function loadTree(root, backendRoot) {
|
|
122
|
-
|
|
156
|
+
let st;
|
|
157
|
+
try {
|
|
158
|
+
st = statSync(root);
|
|
159
|
+
} catch (err) {
|
|
160
|
+
const code = fsErrorCode(err);
|
|
161
|
+
throw new SubjectUnreadableError(
|
|
162
|
+
root,
|
|
163
|
+
code === "ENOENT" ? "the path does not exist" : `the path cannot be read (${code})`
|
|
164
|
+
);
|
|
165
|
+
}
|
|
123
166
|
const baseDir = st.isDirectory() ? root : dirname(root);
|
|
124
167
|
const filePaths = [];
|
|
125
168
|
const tree = { root, files: [], packageJson: null, skippedDirs: [] };
|
|
126
169
|
if (backendRoot !== void 0) tree.backendRoot = backendRoot;
|
|
127
170
|
const unscanned = [];
|
|
128
|
-
if (st.isDirectory())
|
|
129
|
-
|
|
171
|
+
if (st.isDirectory()) {
|
|
172
|
+
let entries;
|
|
173
|
+
try {
|
|
174
|
+
entries = readdirSync(root);
|
|
175
|
+
} catch (err) {
|
|
176
|
+
throw new SubjectUnreadableError(
|
|
177
|
+
root,
|
|
178
|
+
`the directory cannot be listed (${fsErrorCode(err)})`
|
|
179
|
+
);
|
|
180
|
+
}
|
|
181
|
+
if (entries.length === 0) {
|
|
182
|
+
throw new SubjectUnreadableError(root, "the directory is empty (0 entries)");
|
|
183
|
+
}
|
|
184
|
+
const seen = { files: 0 };
|
|
185
|
+
walk(root, filePaths, (d, reason) => recordSkip(tree, d, root, reason), unscanned, seen);
|
|
186
|
+
for (const p of filePaths) {
|
|
187
|
+
try {
|
|
188
|
+
tree.files.push(loadFile(p, baseDir));
|
|
189
|
+
} catch (err) {
|
|
190
|
+
recordSkip(tree, p, root, `could not be read (${fsErrorCode(err)}) \u2014 not read`);
|
|
191
|
+
seen.files--;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
if (seen.files === 0) {
|
|
195
|
+
const passed = tree.skippedDirs.map((d) => `${d.path} (${d.reason})`).join(", ");
|
|
196
|
+
throw new SubjectUnreadableError(
|
|
197
|
+
root,
|
|
198
|
+
`the walk reached no readable file (${entries.length} entr${entries.length === 1 ? "y" : "ies"}, none a readable file${passed ? `; passed over: ${passed}` : ""})`
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
} else if (st.isFile()) {
|
|
202
|
+
if (!root.endsWith(".ts") && !root.endsWith(".tsx")) {
|
|
203
|
+
throw new SubjectUnreadableError(
|
|
204
|
+
root,
|
|
205
|
+
"a file the scan does not read: a target is a source directory or a single .ts/.tsx file"
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
tree.files.push(loadFile(root, baseDir));
|
|
210
|
+
} catch (err) {
|
|
211
|
+
throw new SubjectUnreadableError(root, `the file cannot be read (${fsErrorCode(err)})`);
|
|
212
|
+
}
|
|
213
|
+
} else {
|
|
214
|
+
throw new SubjectUnreadableError(root, "neither a directory nor a regular file");
|
|
215
|
+
}
|
|
130
216
|
tree.unscannedSource = unscanned.map((p) => relative(baseDir, p).split(sep).join("/"));
|
|
131
|
-
tree.files = filePaths.map((p) => loadFile(p, baseDir));
|
|
132
217
|
tree.packageJson = findPackageJson(baseDir);
|
|
133
218
|
return tree;
|
|
134
219
|
}
|
|
@@ -477,8 +562,180 @@ function guardUnreadablePackageJson(inner, opts) {
|
|
|
477
562
|
};
|
|
478
563
|
}
|
|
479
564
|
|
|
565
|
+
// src/detectors/siteScan.ts
|
|
566
|
+
import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
567
|
+
import { join as join2, relative as relative2, sep as sep2 } from "path";
|
|
568
|
+
function codeSites(tree, re) {
|
|
569
|
+
const out = [];
|
|
570
|
+
const g = new RegExp(re.source, re.flags.includes("g") ? re.flags : `${re.flags}g`);
|
|
571
|
+
for (const file of tree.files) {
|
|
572
|
+
for (const line of file.lines) {
|
|
573
|
+
if (line.comment) continue;
|
|
574
|
+
for (const m of line.raw.matchAll(g)) {
|
|
575
|
+
const col = m.index ?? 0;
|
|
576
|
+
if (isCodeAt(file, line.n, col)) {
|
|
577
|
+
out.push({ file, line, col, match: m, anchor: `${file.rel}:${line.n}` });
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return out;
|
|
583
|
+
}
|
|
584
|
+
var uncommentedCache = /* @__PURE__ */ new WeakMap();
|
|
585
|
+
function uncommentedLines(file) {
|
|
586
|
+
const hit = uncommentedCache.get(file);
|
|
587
|
+
if (hit) return hit;
|
|
588
|
+
const rows = file.lines.map((l) => {
|
|
589
|
+
let s = "";
|
|
590
|
+
for (let c = 0; c < l.raw.length; c++) {
|
|
591
|
+
s += l.comment || isCommentAt(file, l.n, c) ? " " : l.raw[c];
|
|
592
|
+
}
|
|
593
|
+
return s;
|
|
594
|
+
});
|
|
595
|
+
uncommentedCache.set(file, rows);
|
|
596
|
+
return rows;
|
|
597
|
+
}
|
|
598
|
+
function balancedBlock(file, line, col, open, close) {
|
|
599
|
+
const rows = uncommentedLines(file);
|
|
600
|
+
let depth = 0;
|
|
601
|
+
let text = "";
|
|
602
|
+
for (let n = line; n <= file.lines.length; n++) {
|
|
603
|
+
const raw = rows[n - 1];
|
|
604
|
+
const from = n === line ? col : 0;
|
|
605
|
+
for (let c = from; c < raw.length; c++) {
|
|
606
|
+
const ch = raw[c];
|
|
607
|
+
if (ch === open && isCodeAt(file, n, c)) depth++;
|
|
608
|
+
else if (ch === close && isCodeAt(file, n, c)) depth--;
|
|
609
|
+
text += ch;
|
|
610
|
+
if (depth === 0 && ch === close) {
|
|
611
|
+
return { startLine: line, endLine: n, text };
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
text += "\n";
|
|
615
|
+
}
|
|
616
|
+
return null;
|
|
617
|
+
}
|
|
618
|
+
function nextOpener(file, line, col, openers = "{[(", maxLines = 3) {
|
|
619
|
+
const rows = uncommentedLines(file);
|
|
620
|
+
for (let n = line; n <= Math.min(file.lines.length, line + maxLines); n++) {
|
|
621
|
+
const raw = rows[n - 1];
|
|
622
|
+
for (let c = n === line ? col : 0; c < raw.length; c++) {
|
|
623
|
+
if (openers.includes(raw[c]) && isCodeAt(file, n, c)) return { line: n, col: c, ch: raw[c] };
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
return null;
|
|
627
|
+
}
|
|
628
|
+
function isRecord(v) {
|
|
629
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
630
|
+
}
|
|
631
|
+
function baseOf(tree) {
|
|
632
|
+
try {
|
|
633
|
+
return statSync2(tree.root).isDirectory() ? tree.root : join2(tree.root, "..");
|
|
634
|
+
} catch {
|
|
635
|
+
return tree.root;
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
function findDiskFiles(tree, pred) {
|
|
639
|
+
const out = [];
|
|
640
|
+
const base = baseOf(tree);
|
|
641
|
+
const walk2 = (dir) => {
|
|
642
|
+
let entries;
|
|
643
|
+
try {
|
|
644
|
+
entries = readdirSync2(dir);
|
|
645
|
+
} catch {
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
for (const name of entries.sort()) {
|
|
649
|
+
const full = join2(dir, name);
|
|
650
|
+
let st;
|
|
651
|
+
try {
|
|
652
|
+
st = statSync2(full);
|
|
653
|
+
} catch {
|
|
654
|
+
continue;
|
|
655
|
+
}
|
|
656
|
+
if (st.isDirectory()) {
|
|
657
|
+
if (SKIP_DIRS.has(name)) {
|
|
658
|
+
recordSkip(tree, full, base);
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
walk2(full);
|
|
662
|
+
} else {
|
|
663
|
+
const rel = relative2(base, full).split(sep2).join("/");
|
|
664
|
+
if (pred(name, rel)) out.push({ path: full, rel, name });
|
|
665
|
+
}
|
|
666
|
+
}
|
|
667
|
+
};
|
|
668
|
+
walk2(base);
|
|
669
|
+
return out;
|
|
670
|
+
}
|
|
671
|
+
function readText(path) {
|
|
672
|
+
try {
|
|
673
|
+
return readFileSync3(path, "utf8");
|
|
674
|
+
} catch {
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
function readJsonRecord(path) {
|
|
679
|
+
const text = readText(path);
|
|
680
|
+
if (text === null) return null;
|
|
681
|
+
try {
|
|
682
|
+
const v = JSON.parse(text);
|
|
683
|
+
return isRecord(v) ? v : null;
|
|
684
|
+
} catch {
|
|
685
|
+
return null;
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
function proseWordCount(markdown) {
|
|
689
|
+
const noFences = markdown.replace(/^(```|~~~)[^\n]*\n[\s\S]*?^\1[ \t]*$/gm, " ");
|
|
690
|
+
const noInline = noFences.replace(/`[^`\n]*`/g, " ");
|
|
691
|
+
return (noInline.match(/[\p{L}\p{N}][\p{L}\p{N}'’-]*/gu) ?? []).length;
|
|
692
|
+
}
|
|
693
|
+
function lineHits(tree, re) {
|
|
694
|
+
const out = [];
|
|
695
|
+
for (const file of tree.files) {
|
|
696
|
+
const rows = uncommentedLines(file);
|
|
697
|
+
rows.forEach((text, i) => {
|
|
698
|
+
if (re.test(text)) {
|
|
699
|
+
out.push({ file, line: i + 1, anchor: `${file.rel}:${i + 1}`, text });
|
|
700
|
+
}
|
|
701
|
+
});
|
|
702
|
+
}
|
|
703
|
+
return out;
|
|
704
|
+
}
|
|
705
|
+
function enclosingBlock(file, line, col, levels = 0) {
|
|
706
|
+
const rows = uncommentedLines(file);
|
|
707
|
+
let curLine = line;
|
|
708
|
+
let curCol = col;
|
|
709
|
+
let result = null;
|
|
710
|
+
for (let level = 0; level <= levels; level++) {
|
|
711
|
+
let depth = 0;
|
|
712
|
+
let found = null;
|
|
713
|
+
outer: for (let n = curLine; n >= 1; n--) {
|
|
714
|
+
const raw = rows[n - 1];
|
|
715
|
+
for (let c = (n === curLine ? curCol : raw.length) - 1; c >= 0; c--) {
|
|
716
|
+
if (!isCodeAt(file, n, c)) continue;
|
|
717
|
+
if (raw[c] === "}") depth++;
|
|
718
|
+
else if (raw[c] === "{") {
|
|
719
|
+
if (depth === 0) {
|
|
720
|
+
found = { l: n, c };
|
|
721
|
+
break outer;
|
|
722
|
+
}
|
|
723
|
+
depth--;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
if (!found) return result;
|
|
728
|
+
const b = balancedBlock(file, found.l, found.c, "{", "}");
|
|
729
|
+
if (!b) return null;
|
|
730
|
+
result = { ...b, openLine: found.l, openCol: found.c };
|
|
731
|
+
curLine = found.l;
|
|
732
|
+
curCol = found.c;
|
|
733
|
+
}
|
|
734
|
+
return result;
|
|
735
|
+
}
|
|
736
|
+
|
|
480
737
|
// src/detectors/standardPin.ts
|
|
481
|
-
var STANDARD_PIN = "
|
|
738
|
+
var STANDARD_PIN = "6d463a1ecc5256cd59dd1afe5aa5e335accacc90";
|
|
482
739
|
|
|
483
740
|
// src/detectors/dLocalIdentity.ts
|
|
484
741
|
var CLOUD_IDENTITY_PACKAGE = "@vantageos/cloud-identity";
|
|
@@ -601,21 +858,155 @@ function literalHoldsIdentity(m) {
|
|
|
601
858
|
const arg = unfold(m[2]);
|
|
602
859
|
return m[1].split(",").map((e) => unfold(e.trim())).some((e) => e !== "" && !isLiteral(e) && !isLiteral(arg) && comparesIdentity(e, arg));
|
|
603
860
|
}
|
|
604
|
-
function
|
|
861
|
+
function scopesOf(ctx) {
|
|
862
|
+
const out = [];
|
|
863
|
+
for (let level = 0; level < 6; level++) {
|
|
864
|
+
const b = enclosingBlock(ctx.file, ctx.line.n, ctx.idx, level);
|
|
865
|
+
if (!b || out.length > 0 && b.text === out[out.length - 1]) break;
|
|
866
|
+
out.push(b.text);
|
|
867
|
+
}
|
|
868
|
+
return out;
|
|
869
|
+
}
|
|
870
|
+
var escRe = (x) => x.replace(/[$]/g, "\\$&");
|
|
871
|
+
function exprUntilSemicolon(text, from) {
|
|
872
|
+
let depth = 0;
|
|
873
|
+
for (let i = from; i < text.length; i++) {
|
|
874
|
+
const c = text[i];
|
|
875
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
876
|
+
for (i++; i < text.length && text[i] !== c; i++) if (text[i] === "\\") i++;
|
|
877
|
+
continue;
|
|
878
|
+
}
|
|
879
|
+
if (c === "(" || c === "[" || c === "{") depth++;
|
|
880
|
+
else if (c === ")" || c === "]" || c === "}") {
|
|
881
|
+
if (depth === 0) return text.slice(from, i).trim();
|
|
882
|
+
depth--;
|
|
883
|
+
} else if (c === ";" && depth === 0) return text.slice(from, i).trim();
|
|
884
|
+
}
|
|
885
|
+
return text.slice(from).trim();
|
|
886
|
+
}
|
|
887
|
+
function bindingOf(name, scopes) {
|
|
888
|
+
const n = escRe(name);
|
|
889
|
+
for (const text of scopes) {
|
|
890
|
+
const decl = new RegExp(String.raw`\b(?:const|let|var)\s+${n}\s*(?::[^=;]+)?=\s*`).exec(text);
|
|
891
|
+
if (decl) return { kind: "value", expr: exprUntilSemicolon(text, decl.index + decl[0].length) };
|
|
892
|
+
const loop = new RegExp(String.raw`\bfor\s*\(\s*(?:const|let|var)\s+${n}\s+of\s+`).exec(text);
|
|
893
|
+
if (loop) {
|
|
894
|
+
const inner = exprUntilSemicolon(text, loop.index + loop[0].length);
|
|
895
|
+
return { kind: "element", expr: inner };
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
return null;
|
|
899
|
+
}
|
|
900
|
+
function functionBody(file, name) {
|
|
901
|
+
const text = uncommentedLines(file).join("\n");
|
|
902
|
+
const decl = new RegExp(String.raw`\bfunction\s+${escRe(name)}\s*\(`).exec(text);
|
|
903
|
+
if (!decl) return null;
|
|
904
|
+
const params = callArgs(text, decl.index + decl[0].length);
|
|
905
|
+
if (!params) return null;
|
|
906
|
+
return blockFrom(text, params.end);
|
|
907
|
+
}
|
|
908
|
+
function isLocalListing(e, ctx, scopes, depth) {
|
|
909
|
+
if (depth > 4) return false;
|
|
910
|
+
const x = e.trim().replace(/^await\s+/, "");
|
|
911
|
+
if (/^(?:[\w$]+\s*\.\s*)*(?:readdirSync|readdir)\s*\(/.test(x)) return true;
|
|
912
|
+
const call = x.match(/^([A-Za-z_$][\w$]*)\s*\(/);
|
|
913
|
+
if (call) {
|
|
914
|
+
const body = functionBody(ctx.file, call[1]);
|
|
915
|
+
return body !== null && /\b(?:readdirSync|readdir)\s*\(/.test(body);
|
|
916
|
+
}
|
|
917
|
+
if (/^[A-Za-z_$][\w$]*$/.test(x)) {
|
|
918
|
+
const b = bindingOf(x, scopes);
|
|
919
|
+
return b !== null && b.kind === "value" && isLocalListing(b.expr, ctx, scopes, depth + 1);
|
|
920
|
+
}
|
|
921
|
+
return false;
|
|
922
|
+
}
|
|
923
|
+
function templateParts(e) {
|
|
924
|
+
if (!/^`[\s\S]*`$/.test(e)) return null;
|
|
925
|
+
const parts = [];
|
|
926
|
+
for (let i = 1; i < e.length - 1; i++) {
|
|
927
|
+
if (e[i] === "\\") {
|
|
928
|
+
i++;
|
|
929
|
+
continue;
|
|
930
|
+
}
|
|
931
|
+
if (e[i] === "$" && e[i + 1] === "{") {
|
|
932
|
+
let depth = 0;
|
|
933
|
+
let j = i + 1;
|
|
934
|
+
for (; j < e.length; j++) {
|
|
935
|
+
if (e[j] === "{") depth++;
|
|
936
|
+
else if (e[j] === "}" && --depth === 0) break;
|
|
937
|
+
}
|
|
938
|
+
parts.push(e.slice(i + 2, j));
|
|
939
|
+
i = j;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
return parts;
|
|
943
|
+
}
|
|
944
|
+
function isLocalContent(e, ctx, scopes, depth) {
|
|
945
|
+
if (depth > 4) return false;
|
|
946
|
+
const x = e.trim().replace(/^await\s+/, "");
|
|
947
|
+
if (isStaticInput(x, ctx.statics)) return true;
|
|
948
|
+
const fsRead = x.match(/^(?:[\w$]+\s*\.\s*)*(?:readFileSync|readFile)\s*\(/);
|
|
949
|
+
if (fsRead) {
|
|
950
|
+
const a = callArgs(x, fsRead[0].length);
|
|
951
|
+
return a !== null && a.end === x.length;
|
|
952
|
+
}
|
|
953
|
+
const wrapped = x.match(/^(?:new\s+TextEncoder\s*\(\s*\)\s*\.\s*encode|Buffer\s*\.\s*from)\s*\(/);
|
|
954
|
+
if (wrapped) {
|
|
955
|
+
const a = callArgs(x, wrapped[0].length);
|
|
956
|
+
return a !== null && a.end === x.length && isLocalContent(a.args[0], ctx, scopes, depth + 1);
|
|
957
|
+
}
|
|
958
|
+
const tpl = templateParts(x);
|
|
959
|
+
if (tpl) return tpl.every((p) => isLocalContent(p, ctx, scopes, depth + 1));
|
|
960
|
+
const ch = x.match(/^(?:[\w$]+\s*\.\s*)?createHash\s*\(/);
|
|
961
|
+
if (ch) {
|
|
962
|
+
const c = callArgs(x, ch[0].length);
|
|
963
|
+
const up = c ? x.slice(c.end).match(/^\s*\.\s*update\s*\(/) : null;
|
|
964
|
+
const d = c && up ? callArgs(x, c.end + up[0].length) : null;
|
|
965
|
+
return d !== null && isLocalContent(d.args[0], ctx, scopes, depth + 1);
|
|
966
|
+
}
|
|
967
|
+
const sd = x.match(/^(?:[\w$]+\s*\.\s*)*subtle\s*\.\s*digest\s*\(/);
|
|
968
|
+
if (sd) {
|
|
969
|
+
const c = callArgs(x, sd[0].length);
|
|
970
|
+
return c !== null && c.args.length >= 2 && isLocalContent(c.args[1], ctx, scopes, depth + 1);
|
|
971
|
+
}
|
|
972
|
+
if (/^[A-Za-z_$][\w$]*$/.test(x)) {
|
|
973
|
+
const b = bindingOf(x, scopes);
|
|
974
|
+
if (!b) return false;
|
|
975
|
+
return b.kind === "element" ? isLocalListing(b.expr, ctx, scopes, depth + 1) : isLocalContent(b.expr, ctx, scopes, depth + 1);
|
|
976
|
+
}
|
|
977
|
+
return false;
|
|
978
|
+
}
|
|
979
|
+
function digestsNonCredentialInput(m, ctx) {
|
|
605
980
|
if (!ctx) return false;
|
|
606
981
|
const text = textFrom(ctx);
|
|
607
982
|
const open = text.indexOf("(");
|
|
608
983
|
if (open < 0) return false;
|
|
609
984
|
const call = callArgs(text, open + 1);
|
|
610
985
|
if (!call) return false;
|
|
986
|
+
const scopes = scopesOf(ctx);
|
|
987
|
+
const local = (e) => isLocalContent(e, ctx, scopes, 0);
|
|
611
988
|
if (/^subtle/.test(m[0])) {
|
|
612
|
-
return m[1] === "digest" && call.args.length >= 2 &&
|
|
989
|
+
return m[1] === "digest" && call.args.length >= 2 && local(call.args[1]);
|
|
613
990
|
}
|
|
614
991
|
if (!/^createHash/.test(m[0])) return false;
|
|
615
992
|
const update = text.slice(call.end).match(/^\s*\.\s*update\s*\(/);
|
|
616
|
-
if (
|
|
617
|
-
|
|
618
|
-
|
|
993
|
+
if (update) {
|
|
994
|
+
const data = callArgs(text, call.end + update[0].length);
|
|
995
|
+
return data !== null && local(data.args[0]);
|
|
996
|
+
}
|
|
997
|
+
const held = ctx.line.raw.slice(0, ctx.idx).match(/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*$/);
|
|
998
|
+
if (!held || scopes.length === 0) return false;
|
|
999
|
+
const v = escRe(held[1]);
|
|
1000
|
+
const scope = scopes[0];
|
|
1001
|
+
const updates = [...scope.matchAll(new RegExp(String.raw`\b${v}\s*\.\s*update\s*\(`, "g"))];
|
|
1002
|
+
if (updates.length === 0) return false;
|
|
1003
|
+
const digests = [...scope.matchAll(new RegExp(String.raw`\b${v}\s*\.\s*digest\s*\(`, "g"))];
|
|
1004
|
+
const uses = [...scope.matchAll(new RegExp(String.raw`(?<![\w$.])${v}(?![\w$])`, "g"))];
|
|
1005
|
+
if (uses.length !== 1 + updates.length + digests.length) return false;
|
|
1006
|
+
return updates.every((u) => {
|
|
1007
|
+
const a = callArgs(scope, (u.index ?? 0) + u[0].length);
|
|
1008
|
+
return a !== null && local(a.args[0]);
|
|
1009
|
+
});
|
|
619
1010
|
}
|
|
620
1011
|
var RENAMEABLE = [
|
|
621
1012
|
"createHash",
|
|
@@ -856,10 +1247,11 @@ var MATCHERS = [
|
|
|
856
1247
|
// Any digest of a presented credential, whatever the algorithm.
|
|
857
1248
|
id: "local-digest",
|
|
858
1249
|
category: "validates-credential",
|
|
859
|
-
// A digest over static content (a literal, or a constant made of literals)
|
|
860
|
-
//
|
|
1250
|
+
// A digest over static content (a literal, or a constant made of literals) or over local
|
|
1251
|
+
// content (file bytes, a listing, a digest of them: a content fingerprint of the tree) is not a
|
|
1252
|
+
// digest of a credential. HMAC and sign/verify always take a key: a site.
|
|
861
1253
|
re: /\bcreateHash\s*\(|\bcreateHmac\s*\(|\bsubtle\s*\.\s*(digest|sign|verify)\s*\(/,
|
|
862
|
-
accept: (m, ctx) => !
|
|
1254
|
+
accept: (m, ctx) => !digestsNonCredentialInput(m, ctx),
|
|
863
1255
|
sample: 'const h = await crypto.subtle.digest("SHA-256", data);'
|
|
864
1256
|
},
|
|
865
1257
|
// ── Minting / signing: shown, not counted (category "mints-token") ──────────
|
|
@@ -1188,178 +1580,6 @@ function makeLocalIdentity(opts = {}) {
|
|
|
1188
1580
|
}
|
|
1189
1581
|
var localIdentity = makeLocalIdentity();
|
|
1190
1582
|
|
|
1191
|
-
// src/detectors/siteScan.ts
|
|
1192
|
-
import { existsSync as existsSync2, readFileSync as readFileSync3, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
1193
|
-
import { join as join2, relative as relative2, sep as sep2 } from "path";
|
|
1194
|
-
function codeSites(tree, re) {
|
|
1195
|
-
const out = [];
|
|
1196
|
-
const g = new RegExp(re.source, re.flags.includes("g") ? re.flags : `${re.flags}g`);
|
|
1197
|
-
for (const file of tree.files) {
|
|
1198
|
-
for (const line of file.lines) {
|
|
1199
|
-
if (line.comment) continue;
|
|
1200
|
-
for (const m of line.raw.matchAll(g)) {
|
|
1201
|
-
const col = m.index ?? 0;
|
|
1202
|
-
if (isCodeAt(file, line.n, col)) {
|
|
1203
|
-
out.push({ file, line, col, match: m, anchor: `${file.rel}:${line.n}` });
|
|
1204
|
-
}
|
|
1205
|
-
}
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
return out;
|
|
1209
|
-
}
|
|
1210
|
-
var uncommentedCache = /* @__PURE__ */ new WeakMap();
|
|
1211
|
-
function uncommentedLines(file) {
|
|
1212
|
-
const hit = uncommentedCache.get(file);
|
|
1213
|
-
if (hit) return hit;
|
|
1214
|
-
const rows = file.lines.map((l) => {
|
|
1215
|
-
let s = "";
|
|
1216
|
-
for (let c = 0; c < l.raw.length; c++) {
|
|
1217
|
-
s += l.comment || isCommentAt(file, l.n, c) ? " " : l.raw[c];
|
|
1218
|
-
}
|
|
1219
|
-
return s;
|
|
1220
|
-
});
|
|
1221
|
-
uncommentedCache.set(file, rows);
|
|
1222
|
-
return rows;
|
|
1223
|
-
}
|
|
1224
|
-
function balancedBlock(file, line, col, open, close) {
|
|
1225
|
-
const rows = uncommentedLines(file);
|
|
1226
|
-
let depth = 0;
|
|
1227
|
-
let text = "";
|
|
1228
|
-
for (let n = line; n <= file.lines.length; n++) {
|
|
1229
|
-
const raw = rows[n - 1];
|
|
1230
|
-
const from = n === line ? col : 0;
|
|
1231
|
-
for (let c = from; c < raw.length; c++) {
|
|
1232
|
-
const ch = raw[c];
|
|
1233
|
-
if (ch === open && isCodeAt(file, n, c)) depth++;
|
|
1234
|
-
else if (ch === close && isCodeAt(file, n, c)) depth--;
|
|
1235
|
-
text += ch;
|
|
1236
|
-
if (depth === 0 && ch === close) {
|
|
1237
|
-
return { startLine: line, endLine: n, text };
|
|
1238
|
-
}
|
|
1239
|
-
}
|
|
1240
|
-
text += "\n";
|
|
1241
|
-
}
|
|
1242
|
-
return null;
|
|
1243
|
-
}
|
|
1244
|
-
function nextOpener(file, line, col, openers = "{[(", maxLines = 3) {
|
|
1245
|
-
const rows = uncommentedLines(file);
|
|
1246
|
-
for (let n = line; n <= Math.min(file.lines.length, line + maxLines); n++) {
|
|
1247
|
-
const raw = rows[n - 1];
|
|
1248
|
-
for (let c = n === line ? col : 0; c < raw.length; c++) {
|
|
1249
|
-
if (openers.includes(raw[c]) && isCodeAt(file, n, c)) return { line: n, col: c, ch: raw[c] };
|
|
1250
|
-
}
|
|
1251
|
-
}
|
|
1252
|
-
return null;
|
|
1253
|
-
}
|
|
1254
|
-
function isRecord(v) {
|
|
1255
|
-
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1256
|
-
}
|
|
1257
|
-
function baseOf(tree) {
|
|
1258
|
-
try {
|
|
1259
|
-
return statSync2(tree.root).isDirectory() ? tree.root : join2(tree.root, "..");
|
|
1260
|
-
} catch {
|
|
1261
|
-
return tree.root;
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
function findDiskFiles(tree, pred) {
|
|
1265
|
-
const out = [];
|
|
1266
|
-
const base = baseOf(tree);
|
|
1267
|
-
const walk2 = (dir) => {
|
|
1268
|
-
let entries;
|
|
1269
|
-
try {
|
|
1270
|
-
entries = readdirSync2(dir);
|
|
1271
|
-
} catch {
|
|
1272
|
-
return;
|
|
1273
|
-
}
|
|
1274
|
-
for (const name of entries.sort()) {
|
|
1275
|
-
const full = join2(dir, name);
|
|
1276
|
-
let st;
|
|
1277
|
-
try {
|
|
1278
|
-
st = statSync2(full);
|
|
1279
|
-
} catch {
|
|
1280
|
-
continue;
|
|
1281
|
-
}
|
|
1282
|
-
if (st.isDirectory()) {
|
|
1283
|
-
if (SKIP_DIRS.has(name)) {
|
|
1284
|
-
recordSkip(tree, full, base);
|
|
1285
|
-
continue;
|
|
1286
|
-
}
|
|
1287
|
-
walk2(full);
|
|
1288
|
-
} else {
|
|
1289
|
-
const rel = relative2(base, full).split(sep2).join("/");
|
|
1290
|
-
if (pred(name, rel)) out.push({ path: full, rel, name });
|
|
1291
|
-
}
|
|
1292
|
-
}
|
|
1293
|
-
};
|
|
1294
|
-
walk2(base);
|
|
1295
|
-
return out;
|
|
1296
|
-
}
|
|
1297
|
-
function readText(path) {
|
|
1298
|
-
try {
|
|
1299
|
-
return readFileSync3(path, "utf8");
|
|
1300
|
-
} catch {
|
|
1301
|
-
return null;
|
|
1302
|
-
}
|
|
1303
|
-
}
|
|
1304
|
-
function readJsonRecord(path) {
|
|
1305
|
-
const text = readText(path);
|
|
1306
|
-
if (text === null) return null;
|
|
1307
|
-
try {
|
|
1308
|
-
const v = JSON.parse(text);
|
|
1309
|
-
return isRecord(v) ? v : null;
|
|
1310
|
-
} catch {
|
|
1311
|
-
return null;
|
|
1312
|
-
}
|
|
1313
|
-
}
|
|
1314
|
-
function proseWordCount(markdown) {
|
|
1315
|
-
const noFences = markdown.replace(/^(```|~~~)[^\n]*\n[\s\S]*?^\1[ \t]*$/gm, " ");
|
|
1316
|
-
const noInline = noFences.replace(/`[^`\n]*`/g, " ");
|
|
1317
|
-
return (noInline.match(/[\p{L}\p{N}][\p{L}\p{N}'’-]*/gu) ?? []).length;
|
|
1318
|
-
}
|
|
1319
|
-
function lineHits(tree, re) {
|
|
1320
|
-
const out = [];
|
|
1321
|
-
for (const file of tree.files) {
|
|
1322
|
-
const rows = uncommentedLines(file);
|
|
1323
|
-
rows.forEach((text, i) => {
|
|
1324
|
-
if (re.test(text)) {
|
|
1325
|
-
out.push({ file, line: i + 1, anchor: `${file.rel}:${i + 1}`, text });
|
|
1326
|
-
}
|
|
1327
|
-
});
|
|
1328
|
-
}
|
|
1329
|
-
return out;
|
|
1330
|
-
}
|
|
1331
|
-
function enclosingBlock(file, line, col, levels = 0) {
|
|
1332
|
-
const rows = uncommentedLines(file);
|
|
1333
|
-
let curLine = line;
|
|
1334
|
-
let curCol = col;
|
|
1335
|
-
let result = null;
|
|
1336
|
-
for (let level = 0; level <= levels; level++) {
|
|
1337
|
-
let depth = 0;
|
|
1338
|
-
let found = null;
|
|
1339
|
-
outer: for (let n = curLine; n >= 1; n--) {
|
|
1340
|
-
const raw = rows[n - 1];
|
|
1341
|
-
for (let c = (n === curLine ? curCol : raw.length) - 1; c >= 0; c--) {
|
|
1342
|
-
if (!isCodeAt(file, n, c)) continue;
|
|
1343
|
-
if (raw[c] === "}") depth++;
|
|
1344
|
-
else if (raw[c] === "{") {
|
|
1345
|
-
if (depth === 0) {
|
|
1346
|
-
found = { l: n, c };
|
|
1347
|
-
break outer;
|
|
1348
|
-
}
|
|
1349
|
-
depth--;
|
|
1350
|
-
}
|
|
1351
|
-
}
|
|
1352
|
-
}
|
|
1353
|
-
if (!found) return result;
|
|
1354
|
-
const b = balancedBlock(file, found.l, found.c, "{", "}");
|
|
1355
|
-
if (!b) return null;
|
|
1356
|
-
result = { ...b, openLine: found.l, openCol: found.c };
|
|
1357
|
-
curLine = found.l;
|
|
1358
|
-
curCol = found.c;
|
|
1359
|
-
}
|
|
1360
|
-
return result;
|
|
1361
|
-
}
|
|
1362
|
-
|
|
1363
1583
|
// src/detectors/a2-declared-domains.ts
|
|
1364
1584
|
function stripComments(text) {
|
|
1365
1585
|
return text.replace(/\/\*[\s\S]*?\*\//g, " ").replace(/(^|[^:"'`])\/\/.*$/gm, "$1");
|
|
@@ -1937,46 +2157,74 @@ function findUnresolvedToolSites(tree) {
|
|
|
1937
2157
|
return [...kept, ...wrappers.unresolved];
|
|
1938
2158
|
}
|
|
1939
2159
|
function findListReadBuilders(tree) {
|
|
2160
|
+
const bindings = resolveNameBindings(tree);
|
|
2161
|
+
const extents = [];
|
|
2162
|
+
const endOf = (startLine, startCol, text) => {
|
|
2163
|
+
const lines = text.split("\n");
|
|
2164
|
+
const last = lines.length - 1;
|
|
2165
|
+
return [startLine + last, last === 0 ? startCol + text.length - 1 : lines[last].length - 1];
|
|
2166
|
+
};
|
|
2167
|
+
for (const site of codeSites(tree, /(?:\.tool|\bregisterTool)\s*\(/g)) {
|
|
2168
|
+
const open = site.col + site.match[0].lastIndexOf("(");
|
|
2169
|
+
const b = balancedBlock(site.file, site.line.n, open, "(", ")");
|
|
2170
|
+
if (!b) continue;
|
|
2171
|
+
const first = firstArgument(b.text.slice(1, -1));
|
|
2172
|
+
const lit = first.match(/^["'`]([^"'`]+)["'`]$/);
|
|
2173
|
+
const ident = first.match(/^([A-Za-z_$][\w$]*)(?:\.name)?$/);
|
|
2174
|
+
const name = lit ? lit[1] : ident ? bindings.get(ident[1]) ?? null : null;
|
|
2175
|
+
extents.push({
|
|
2176
|
+
file: site.file,
|
|
2177
|
+
name,
|
|
2178
|
+
nameLine: site.line.n,
|
|
2179
|
+
from: [site.line.n, open],
|
|
2180
|
+
to: endOf(site.line.n, open, b.text)
|
|
2181
|
+
});
|
|
2182
|
+
}
|
|
2183
|
+
for (const site of codeSites(tree, new RegExp(DESCRIPTOR_NAME_RE.source, "g"))) {
|
|
2184
|
+
const b = enclosingBlock(site.file, site.line.n, site.col);
|
|
2185
|
+
if (!b) continue;
|
|
2186
|
+
extents.push({
|
|
2187
|
+
file: site.file,
|
|
2188
|
+
name: site.match[1],
|
|
2189
|
+
nameLine: site.line.n,
|
|
2190
|
+
from: [b.openLine, b.openCol],
|
|
2191
|
+
to: endOf(b.openLine, b.openCol, b.text)
|
|
2192
|
+
});
|
|
2193
|
+
}
|
|
2194
|
+
const before = (a, b) => a[0] < b[0] || a[0] === b[0] && a[1] <= b[1];
|
|
1940
2195
|
const out = [];
|
|
1941
|
-
for (const
|
|
1942
|
-
|
|
1943
|
-
let
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
if (
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
} else if (awaitingName) {
|
|
1956
|
-
const bare = raw.match(BARE_NAME_RE);
|
|
1957
|
-
if (bare) {
|
|
1958
|
-
currentTool = bare[1];
|
|
1959
|
-
currentToolLine = line.n;
|
|
1960
|
-
}
|
|
1961
|
-
awaitingName = false;
|
|
1962
|
-
}
|
|
1963
|
-
const desc = raw.match(DESCRIPTOR_NAME_RE);
|
|
1964
|
-
if (desc) {
|
|
1965
|
-
currentTool = desc[1];
|
|
1966
|
-
currentToolLine = line.n;
|
|
1967
|
-
}
|
|
1968
|
-
if (CONTENT_RE.test(raw) && currentTool && LIST_READ_RE.test(currentTool)) {
|
|
1969
|
-
out.push({
|
|
1970
|
-
file,
|
|
1971
|
-
toolName: currentTool,
|
|
1972
|
-
toolNameLine: currentToolLine,
|
|
1973
|
-
contentLine: line.n
|
|
1974
|
-
});
|
|
1975
|
-
}
|
|
2196
|
+
for (const site of codeSites(tree, new RegExp(CONTENT_RE.source, "g"))) {
|
|
2197
|
+
const at = [site.line.n, site.col];
|
|
2198
|
+
let owner = null;
|
|
2199
|
+
for (const e of extents) {
|
|
2200
|
+
if (e.file !== site.file || !before(e.from, at) || !before(at, e.to)) continue;
|
|
2201
|
+
if (!owner || before(owner.from, e.from)) owner = e;
|
|
2202
|
+
}
|
|
2203
|
+
if (owner?.name && LIST_READ_RE.test(owner.name)) {
|
|
2204
|
+
out.push({
|
|
2205
|
+
file: site.file,
|
|
2206
|
+
toolName: owner.name,
|
|
2207
|
+
toolNameLine: owner.nameLine,
|
|
2208
|
+
contentLine: site.line.n
|
|
2209
|
+
});
|
|
1976
2210
|
}
|
|
1977
2211
|
}
|
|
1978
2212
|
return out;
|
|
1979
2213
|
}
|
|
2214
|
+
function firstArgument(inner) {
|
|
2215
|
+
let depth = 0;
|
|
2216
|
+
for (let i = 0; i < inner.length; i++) {
|
|
2217
|
+
const c = inner[i];
|
|
2218
|
+
if (c === '"' || c === "'" || c === "`") {
|
|
2219
|
+
for (i++; i < inner.length && inner[i] !== c; i++) if (inner[i] === "\\") i++;
|
|
2220
|
+
continue;
|
|
2221
|
+
}
|
|
2222
|
+
if (c === "(" || c === "[" || c === "{") depth++;
|
|
2223
|
+
else if (c === ")" || c === "]" || c === "}") depth--;
|
|
2224
|
+
else if (c === "," && depth === 0) return inner.slice(0, i).trim();
|
|
2225
|
+
}
|
|
2226
|
+
return inner.trim();
|
|
2227
|
+
}
|
|
1980
2228
|
var SDK_FIRST_RE = /\bregisterTool\s*\(|\.tool\s*\(|new\s+McpServer\s*\(/;
|
|
1981
2229
|
function sdkFirstAnchor(tree) {
|
|
1982
2230
|
for (const file of tree.files) {
|
|
@@ -4224,6 +4472,7 @@ var VERB_SET = /* @__PURE__ */ new Set([
|
|
|
4224
4472
|
"get",
|
|
4225
4473
|
"search",
|
|
4226
4474
|
"aggregate",
|
|
4475
|
+
"prepare",
|
|
4227
4476
|
"create",
|
|
4228
4477
|
"update",
|
|
4229
4478
|
"delete",
|
|
@@ -4239,6 +4488,7 @@ function c9PositiveControls() {
|
|
|
4239
4488
|
return {
|
|
4240
4489
|
fieldMatcher: field,
|
|
4241
4490
|
setAcceptsCreate: VERB_SET.has("create"),
|
|
4491
|
+
setAcceptsPrepare: VERB_SET.has("prepare"),
|
|
4242
4492
|
setRejectsRpc: !VERB_SET.has("rpc")
|
|
4243
4493
|
};
|
|
4244
4494
|
}
|
|
@@ -4253,9 +4503,9 @@ var c9VerbDeclared = {
|
|
|
4253
4503
|
`C9 positive control FAILED: pattern /${controls.fieldMatcher.pattern}/ did not match sample \`${controls.fieldMatcher.sample}\` \u2014 matcher broken, verdict untrustworthy`
|
|
4254
4504
|
);
|
|
4255
4505
|
}
|
|
4256
|
-
if (!controls.setAcceptsCreate || !controls.setRejectsRpc) {
|
|
4506
|
+
if (!controls.setAcceptsCreate || !controls.setAcceptsPrepare || !controls.setRejectsRpc) {
|
|
4257
4507
|
throw new Error(
|
|
4258
|
-
"C9 closed-set control FAILED: VERB_SET must accept
|
|
4508
|
+
"C9 closed-set control FAILED: VERB_SET must accept the legitimate verbs create and prepare and reject the banned rpc sentinel \u2014 verdict untrustworthy"
|
|
4259
4509
|
);
|
|
4260
4510
|
}
|
|
4261
4511
|
const unresolved = findUnresolvedToolSites(tree);
|
|
@@ -4302,7 +4552,7 @@ var c9VerbDeclared = {
|
|
|
4302
4552
|
id: this.id,
|
|
4303
4553
|
anchor: t.anchor,
|
|
4304
4554
|
state: "non-conforming",
|
|
4305
|
-
verdict: `tool "${t.name}" declares verb "${t.verb}", outside the closed set (positive-control OK) -> use
|
|
4555
|
+
verdict: `tool "${t.name}" declares verb "${t.verb}", outside the closed set (positive-control OK) -> use one of ${[...VERB_SET].join("/")} (C-9)`
|
|
4306
4556
|
};
|
|
4307
4557
|
}
|
|
4308
4558
|
}
|
|
@@ -4475,17 +4725,14 @@ var c11Pagination = {
|
|
|
4475
4725
|
};
|
|
4476
4726
|
},
|
|
4477
4727
|
/**
|
|
4478
|
-
* DYNAMIC —
|
|
4479
|
-
*
|
|
4480
|
-
*
|
|
4481
|
-
*
|
|
4482
|
-
* schema and no `callTool` verb — that half needs `Session.callTool` with a
|
|
4483
|
-
* cursor argument (documented gap, types.ts `Session`). The verdict names
|
|
4484
|
-
* this explicitly rather than folding it into a silent full "present".
|
|
4728
|
+
* DYNAMIC — the invalid-cursor round trip, on each surface where a cursor arrives (C-11 (a) and
|
|
4729
|
+
* (b)). Red on any probe answered in the wrong form; could-not-judge on a probe that got no
|
|
4730
|
+
* answer, was not sent, or recorded an outcome this code cannot read; present only when every
|
|
4731
|
+
* probe answered in its surface's form.
|
|
4485
4732
|
*/
|
|
4486
4733
|
runDynamic(runtime) {
|
|
4487
4734
|
if (runtime.launchError) {
|
|
4488
|
-
const reason = `server could not be started/introspected \u2014
|
|
4735
|
+
const reason = `server could not be started/introspected \u2014 the invalid-cursor round trip could not run: ${runtime.launchError} (C-11)`;
|
|
4489
4736
|
return {
|
|
4490
4737
|
id: this.id,
|
|
4491
4738
|
anchor: `${runtime.root} (runtime)`,
|
|
@@ -4494,24 +4741,117 @@ var c11Pagination = {
|
|
|
4494
4741
|
verdict: `could not judge: ${reason}`
|
|
4495
4742
|
};
|
|
4496
4743
|
}
|
|
4497
|
-
const paginable = runtime.tools.filter((t) => PAGINABLE_RE.test(t.name));
|
|
4744
|
+
const paginable = runtime.tools.filter((t) => PAGINABLE_RE.test(t.name) || t.cursorArgument);
|
|
4498
4745
|
if (paginable.length === 0) {
|
|
4499
4746
|
return {
|
|
4500
4747
|
id: this.id,
|
|
4501
4748
|
anchor: `${runtime.root} (runtime tools/list)`,
|
|
4502
4749
|
state: "absent",
|
|
4503
|
-
verdict: "server advertises no list/read/get/search tool at runtime; pagination check n/a"
|
|
4750
|
+
verdict: "server advertises no list/read/get/search tool and no tool taking a cursor at runtime; pagination check n/a"
|
|
4751
|
+
};
|
|
4752
|
+
}
|
|
4753
|
+
const anchor = `${runtime.root} (runtime invalid-cursor probes)`;
|
|
4754
|
+
const probes = runtime.cursorProbes;
|
|
4755
|
+
if (!probes || probes.length === 0) {
|
|
4756
|
+
const reason = "no invalid-cursor round trip was recorded for this run: the -32602 / invalid_cursor answers are unread (C-11)";
|
|
4757
|
+
return {
|
|
4758
|
+
id: this.id,
|
|
4759
|
+
anchor,
|
|
4760
|
+
state: "could-not-judge",
|
|
4761
|
+
reason,
|
|
4762
|
+
verdict: `could not judge: ${reason}`
|
|
4763
|
+
};
|
|
4764
|
+
}
|
|
4765
|
+
const red = [];
|
|
4766
|
+
const unread = [];
|
|
4767
|
+
const ok = [];
|
|
4768
|
+
for (const p of probes) {
|
|
4769
|
+
const j = judgeCursorProbe(p);
|
|
4770
|
+
if (j.kind === "red") red.push(j.text);
|
|
4771
|
+
else if (j.kind === "unread") unread.push(j.text);
|
|
4772
|
+
else ok.push(j.text);
|
|
4773
|
+
}
|
|
4774
|
+
if (red.length > 0) {
|
|
4775
|
+
return {
|
|
4776
|
+
id: this.id,
|
|
4777
|
+
anchor,
|
|
4778
|
+
state: "non-conforming",
|
|
4779
|
+
verdict: `${red.join("; ")} -> an invalid cursor on a protocol list method is -32602; passed as a tool argument it is a tool result with isError: true and structuredContent.error.code "invalid_cursor" (SEP-1303) (C-11)`
|
|
4780
|
+
};
|
|
4781
|
+
}
|
|
4782
|
+
if (unread.length > 0) {
|
|
4783
|
+
const reason = `${unread.join("; ")} (C-11)`;
|
|
4784
|
+
return {
|
|
4785
|
+
id: this.id,
|
|
4786
|
+
anchor,
|
|
4787
|
+
state: "could-not-judge",
|
|
4788
|
+
reason,
|
|
4789
|
+
verdict: `could not judge: ${reason}`
|
|
4790
|
+
};
|
|
4791
|
+
}
|
|
4792
|
+
const toolProbes = probes.filter((p) => p.surface === "tool-argument").length;
|
|
4793
|
+
return {
|
|
4794
|
+
id: this.id,
|
|
4795
|
+
anchor,
|
|
4796
|
+
state: "present",
|
|
4797
|
+
verdict: `${ok.join("; ")}${toolProbes === 0 ? "; no served tool takes a cursor argument, so C-11 (b) has no object on this server" : ""} (C-11)`
|
|
4798
|
+
};
|
|
4799
|
+
}
|
|
4800
|
+
};
|
|
4801
|
+
function judgeCursorProbe(p) {
|
|
4802
|
+
const where = p.surface === "list-method" ? `${p.target} (protocol list method)` : `tool "${p.target}" (cursor argument)`;
|
|
4803
|
+
const o = p.outcome;
|
|
4804
|
+
switch (o.kind) {
|
|
4805
|
+
case "no-answer":
|
|
4806
|
+
return { kind: "unread", text: `${where}: no answer to the invalid cursor (${o.reason})` };
|
|
4807
|
+
case "not-sent":
|
|
4808
|
+
return { kind: "unread", text: `${where}: probe not sent (${o.reason})` };
|
|
4809
|
+
case "error":
|
|
4810
|
+
if (p.surface === "list-method") {
|
|
4811
|
+
return o.code === -32602 ? { kind: "ok", text: `${where}: -32602 on an invalid cursor` } : {
|
|
4812
|
+
kind: "red",
|
|
4813
|
+
text: `${where}: answered an invalid cursor with error ${String(o.code)}, not -32602`
|
|
4814
|
+
};
|
|
4815
|
+
}
|
|
4816
|
+
return {
|
|
4817
|
+
kind: "red",
|
|
4818
|
+
text: `${where}: answered an invalid cursor argument with a protocol error ${String(o.code)} \u2014 a tool input error is a tool result, never a protocol error`
|
|
4819
|
+
};
|
|
4820
|
+
case "result":
|
|
4821
|
+
if (p.surface === "list-method") {
|
|
4822
|
+
return {
|
|
4823
|
+
kind: "red",
|
|
4824
|
+
text: `${where}: answered an invalid cursor with a result (a silent page), not -32602`
|
|
4825
|
+
};
|
|
4826
|
+
}
|
|
4827
|
+
if (!o.isError) {
|
|
4828
|
+
return {
|
|
4829
|
+
kind: "red",
|
|
4830
|
+
text: `${where}: answered an invalid cursor argument with a silent page (no isError)`
|
|
4831
|
+
};
|
|
4832
|
+
}
|
|
4833
|
+
if (o.errorCode === void 0) {
|
|
4834
|
+
return {
|
|
4835
|
+
kind: "red",
|
|
4836
|
+
text: `${where}: isError: true with no structuredContent.error.code (no stable machine code)`
|
|
4837
|
+
};
|
|
4838
|
+
}
|
|
4839
|
+
return o.errorCode === "invalid_cursor" ? {
|
|
4840
|
+
kind: "ok",
|
|
4841
|
+
text: `${where}: isError: true, structuredContent.error.code "invalid_cursor"`
|
|
4842
|
+
} : {
|
|
4843
|
+
kind: "red",
|
|
4844
|
+
text: `${where}: isError: true with structuredContent.error.code ${JSON.stringify(o.errorCode)}, not "invalid_cursor"`
|
|
4845
|
+
};
|
|
4846
|
+
default: {
|
|
4847
|
+
const unknown = o;
|
|
4848
|
+
return {
|
|
4849
|
+
kind: "unread",
|
|
4850
|
+
text: `${where}: an outcome this code cannot read (${JSON.stringify(unknown.kind)})`
|
|
4504
4851
|
};
|
|
4505
4852
|
}
|
|
4506
|
-
const names = paginable.map((t) => t.name).join(", ");
|
|
4507
|
-
return {
|
|
4508
|
-
id: this.id,
|
|
4509
|
-
anchor: `${runtime.root} (runtime tools/list)`,
|
|
4510
|
-
state: "present",
|
|
4511
|
-
verdict: `runtime confirms paginable tool(s) actually served: ${names}. The invalid-cursor -> -32602 round trip is NOT performed by this snapshot surface (needs Session.callTool with a cursor argument, not yet exposed \u2014 see types.ts Session doc); this dynamic half asserts service reachability, not the full cursor-handling guarantee.`
|
|
4512
|
-
};
|
|
4513
4853
|
}
|
|
4514
|
-
}
|
|
4854
|
+
}
|
|
4515
4855
|
|
|
4516
4856
|
// src/detectors/c12-no-duplicates.ts
|
|
4517
4857
|
function tripletKey(t) {
|
|
@@ -4607,37 +4947,162 @@ var c12NoDuplicates = {
|
|
|
4607
4947
|
};
|
|
4608
4948
|
|
|
4609
4949
|
// src/detectors/c13-context-ceiling.ts
|
|
4610
|
-
var
|
|
4611
|
-
var
|
|
4612
|
-
|
|
4950
|
+
var SERVER_MANIFEST_NAMES = /* @__PURE__ */ new Set(["mcp.json", "mcp-server.json"]);
|
|
4951
|
+
var NAME_RE = /\b(?:CONTEXT_CEILING|contextCeiling)\b/g;
|
|
4952
|
+
var NUM = String.raw`(\d[\d_]*)`;
|
|
4953
|
+
var halfRe = (key) => new RegExp(String.raw`(?:^|[{,\s])["'\x60]?${key}["'\x60]?\s*:\s*([^,}\s][^,}]*)`);
|
|
4954
|
+
function readObjectText(text) {
|
|
4955
|
+
const halves = {};
|
|
4956
|
+
for (const key of ["global", "perTool"]) {
|
|
4957
|
+
const m = text.match(halfRe(key));
|
|
4958
|
+
if (!m) halves[key] = "missing";
|
|
4959
|
+
else {
|
|
4960
|
+
const v = m[1].trim().replace(/\s+as\s+const$/, "");
|
|
4961
|
+
const n = v.match(new RegExp(`^${NUM}$`));
|
|
4962
|
+
halves[key] = n ? Number.parseInt(n[1].replace(/_/g, ""), 10) : v;
|
|
4963
|
+
}
|
|
4964
|
+
}
|
|
4965
|
+
const odd = Object.entries(halves).filter(([, v]) => typeof v === "string" && v !== "missing");
|
|
4966
|
+
if (odd.length > 0) {
|
|
4967
|
+
return { kind: "unreadable", why: odd.map(([k, v]) => `${k}: ${v}`).join(", ") };
|
|
4968
|
+
}
|
|
4969
|
+
const missing = Object.entries(halves).filter(([, v]) => v === "missing").map(([k]) => k);
|
|
4970
|
+
if (missing.length === 2 && /\.\.\./.test(text)) {
|
|
4971
|
+
return { kind: "unreadable", why: `a spread with no literal halves (${text.slice(0, 60)})` };
|
|
4972
|
+
}
|
|
4973
|
+
if (missing.length > 0) return { kind: "partial", missing };
|
|
4974
|
+
return { kind: "pair", global: halves.global, perTool: halves.perTool };
|
|
4975
|
+
}
|
|
4976
|
+
function braceBlock(text) {
|
|
4977
|
+
let depth = 0;
|
|
4978
|
+
for (let i = 0; i < text.length; i++) {
|
|
4979
|
+
if (text[i] === "{") depth++;
|
|
4980
|
+
else if (text[i] === "}" && --depth === 0) return text.slice(0, i + 1);
|
|
4981
|
+
}
|
|
4982
|
+
return null;
|
|
4983
|
+
}
|
|
4984
|
+
function sameFileConstObject(file, name) {
|
|
4985
|
+
const text = uncommentedLines(file).map(codeOnly).join("\n");
|
|
4986
|
+
const m = new RegExp(
|
|
4987
|
+
String.raw`\bconst\s+${name.replace(/\$/g, "\\$")}\s*(?::[^=]+)?=\s*\{`
|
|
4988
|
+
).exec(text);
|
|
4989
|
+
if (!m) return null;
|
|
4990
|
+
return braceBlock(text.slice(m.index + m[0].length - 1));
|
|
4991
|
+
}
|
|
4992
|
+
function ceilingCensus(tree) {
|
|
4993
|
+
const census = { readable: [], partial: [], unreadable: [] };
|
|
4994
|
+
for (const f of findDiskFiles(tree, (n) => SERVER_MANIFEST_NAMES.has(n))) {
|
|
4995
|
+
const text = readText(f.path);
|
|
4996
|
+
if (text === null) {
|
|
4997
|
+
census.unreadable.push({ anchor: f.rel, reason: `${f.rel} is not readable as text` });
|
|
4998
|
+
continue;
|
|
4999
|
+
}
|
|
5000
|
+
let json;
|
|
5001
|
+
try {
|
|
5002
|
+
json = JSON.parse(text);
|
|
5003
|
+
} catch (e) {
|
|
5004
|
+
census.unreadable.push({
|
|
5005
|
+
anchor: f.rel,
|
|
5006
|
+
reason: `${f.rel} does not parse as JSON (${e instanceof Error ? e.message : String(e)}): a contextCeiling it may declare is unread`
|
|
5007
|
+
});
|
|
5008
|
+
continue;
|
|
5009
|
+
}
|
|
5010
|
+
if (!isRecord(json) || !("contextCeiling" in json)) continue;
|
|
5011
|
+
const anchor = `${f.rel}:${lineOf(text, /"contextCeiling"/)}`;
|
|
5012
|
+
const c = json.contextCeiling;
|
|
5013
|
+
if (!isRecord(c)) {
|
|
5014
|
+
census.partial.push({
|
|
5015
|
+
anchor,
|
|
5016
|
+
form: "manifest",
|
|
5017
|
+
missing: ["global", "perTool (contextCeiling is not an object)"]
|
|
5018
|
+
});
|
|
5019
|
+
continue;
|
|
5020
|
+
}
|
|
5021
|
+
const ok = (v) => typeof v === "number" && Number.isFinite(v) && v > 0;
|
|
5022
|
+
const missing = [ok(c.global) ? "" : "global", ok(c.perTool) ? "" : "perTool"].filter(Boolean);
|
|
5023
|
+
if (missing.length > 0) census.partial.push({ anchor, form: "manifest", missing });
|
|
5024
|
+
else
|
|
5025
|
+
census.readable.push({
|
|
5026
|
+
global: c.global,
|
|
5027
|
+
perTool: c.perTool,
|
|
5028
|
+
anchor,
|
|
5029
|
+
form: "manifest"
|
|
5030
|
+
});
|
|
5031
|
+
}
|
|
4613
5032
|
for (const file of tree.files) {
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
const
|
|
4618
|
-
const
|
|
4619
|
-
const
|
|
4620
|
-
const
|
|
4621
|
-
const
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
|
|
4627
|
-
|
|
5033
|
+
const rows = uncommentedLines(file).map(codeOnly);
|
|
5034
|
+
rows.forEach((row, i) => {
|
|
5035
|
+
for (const hit of row.matchAll(NAME_RE)) {
|
|
5036
|
+
const at = hit.index ?? 0;
|
|
5037
|
+
const anchor = `${file.rel}:${i + 1}`;
|
|
5038
|
+
const before = row.slice(0, at);
|
|
5039
|
+
const window = [row.slice(at + hit[0].length), ...rows.slice(i + 1, i + 9)].join("\n");
|
|
5040
|
+
const rest = window.replace(/^["'\x60]/, "");
|
|
5041
|
+
let value = null;
|
|
5042
|
+
if (/\b(?:const|let|var)\s+$/.test(before)) {
|
|
5043
|
+
const m = rest.match(/^\s*(?::[^=]+?)?=(?![=>])\s*/);
|
|
5044
|
+
if (m) value = rest.slice(m[0].length);
|
|
5045
|
+
} else {
|
|
5046
|
+
const m = rest.match(/^\s*\??:(?!:)\s*/) ?? rest.match(/^\s*=(?![=>])\s*/);
|
|
5047
|
+
if (m) value = rest.slice(m[0].length);
|
|
5048
|
+
}
|
|
5049
|
+
if (value === null) continue;
|
|
5050
|
+
if (/^[A-Z][a-z]\w*\s*(?:[;,)|>=]|$)/.test(value)) continue;
|
|
5051
|
+
let obj = null;
|
|
5052
|
+
if (value.startsWith("{")) obj = braceBlock(value);
|
|
5053
|
+
else {
|
|
5054
|
+
const id = value.match(/^([A-Za-z_$][\w$]*)\s*(?:[;,)}\n]|as\b|$)/);
|
|
5055
|
+
if (id) {
|
|
5056
|
+
obj = sameFileConstObject(file, id[1]);
|
|
5057
|
+
if (obj === null) {
|
|
5058
|
+
census.unreadable.push({
|
|
5059
|
+
anchor,
|
|
5060
|
+
reason: `${anchor} declares ${hit[0]} as \`${id[1]}\`, which is not a same-file const object literal`
|
|
5061
|
+
});
|
|
5062
|
+
continue;
|
|
5063
|
+
}
|
|
5064
|
+
}
|
|
5065
|
+
}
|
|
5066
|
+
if (obj === null) {
|
|
5067
|
+
census.unreadable.push({
|
|
5068
|
+
anchor,
|
|
5069
|
+
reason: `${anchor} declares ${hit[0]} as a computed value (\`${value.split("\n")[0].trim().slice(0, 50)}\`): its numbers are not readable statically`
|
|
5070
|
+
});
|
|
5071
|
+
continue;
|
|
4628
5072
|
}
|
|
5073
|
+
const r = readObjectText(obj);
|
|
5074
|
+
if (r.kind === "partial" && r.missing.length === 2 && /;/.test(obj)) continue;
|
|
5075
|
+
if (r.kind === "pair")
|
|
5076
|
+
census.readable.push({ global: r.global, perTool: r.perTool, anchor, form: "source" });
|
|
5077
|
+
else if (r.kind === "partial")
|
|
5078
|
+
census.partial.push({ anchor, form: "source", missing: r.missing });
|
|
5079
|
+
else
|
|
5080
|
+
census.unreadable.push({
|
|
5081
|
+
anchor,
|
|
5082
|
+
reason: `${anchor} declares ${hit[0]} with ${r.why}: not numeric literals`
|
|
5083
|
+
});
|
|
4629
5084
|
}
|
|
4630
|
-
}
|
|
5085
|
+
});
|
|
4631
5086
|
}
|
|
4632
|
-
return
|
|
5087
|
+
return census;
|
|
5088
|
+
}
|
|
5089
|
+
function agreed(census) {
|
|
5090
|
+
const [first, ...rest] = census.readable;
|
|
5091
|
+
if (!first) return null;
|
|
5092
|
+
return rest.every((c) => c.global === first.global && c.perTool === first.perTool) ? first : null;
|
|
5093
|
+
}
|
|
5094
|
+
function findCeiling(tree) {
|
|
5095
|
+
const c = agreed(ceilingCensus(tree));
|
|
5096
|
+
return c ? { global: c.global, perTool: c.perTool, anchor: c.anchor } : null;
|
|
4633
5097
|
}
|
|
4634
5098
|
function c13PositiveControls() {
|
|
4635
|
-
const complete = "
|
|
4636
|
-
const
|
|
5099
|
+
const complete = readObjectText("{ global: 8000, perTool: 800 }");
|
|
5100
|
+
const quoted = readObjectText('{ "global": 1200, "perTool": 700 }');
|
|
5101
|
+
const partial = readObjectText("{ global: 8000 }");
|
|
4637
5102
|
return {
|
|
4638
5103
|
tokensMatcher: toolAnnotationFieldControls().tokensCtx,
|
|
4639
|
-
ceilingAcceptsComplete:
|
|
4640
|
-
ceilingRejectsPartial:
|
|
5104
|
+
ceilingAcceptsComplete: complete.kind === "pair" && quoted.kind === "pair",
|
|
5105
|
+
ceilingRejectsPartial: partial.kind === "partial"
|
|
4641
5106
|
};
|
|
4642
5107
|
}
|
|
4643
5108
|
var c13ContextCeiling = {
|
|
@@ -4653,7 +5118,7 @@ var c13ContextCeiling = {
|
|
|
4653
5118
|
}
|
|
4654
5119
|
if (!controls.ceilingAcceptsComplete || !controls.ceilingRejectsPartial) {
|
|
4655
5120
|
throw new Error(
|
|
4656
|
-
"C13 ceiling control FAILED: must accept a complete (global, perTool) declaration and reject a partial one \u2014 verdict untrustworthy"
|
|
5121
|
+
"C13 ceiling control FAILED: must accept a complete (global, perTool) declaration, bare or quoted keys, and reject a partial one \u2014 verdict untrustworthy"
|
|
4657
5122
|
);
|
|
4658
5123
|
}
|
|
4659
5124
|
const unresolved = findUnresolvedToolSites(tree);
|
|
@@ -4678,13 +5143,44 @@ var c13ContextCeiling = {
|
|
|
4678
5143
|
}
|
|
4679
5144
|
const opaque = opaqueAnnotationsAbstain(this.id, "C-13", tools);
|
|
4680
5145
|
if (opaque) return opaque;
|
|
4681
|
-
const
|
|
4682
|
-
|
|
5146
|
+
const census = ceilingCensus(tree);
|
|
5147
|
+
const unreadNote = census.unreadable.length > 0 ? `; also seen and not read: ${census.unreadable.map((u) => u.reason).join("; ")}` : "";
|
|
5148
|
+
if (census.readable.length === 0) {
|
|
5149
|
+
if (census.partial.length > 0) {
|
|
5150
|
+
const p = census.partial[0];
|
|
5151
|
+
return {
|
|
5152
|
+
id: this.id,
|
|
5153
|
+
anchor: p.anchor,
|
|
5154
|
+
state: "non-conforming",
|
|
5155
|
+
verdict: `${census.partial.map((x) => `${x.anchor} declares a context ceiling without ${x.missing.join(" and ")}`).join("; ")} (positive-control OK) -> declare both a global and a per-tool ceiling, as positive numbers (C-13)${unreadNote}`
|
|
5156
|
+
};
|
|
5157
|
+
}
|
|
5158
|
+
if (census.unreadable.length > 0) {
|
|
5159
|
+
const reason = `the context ceiling is declared in a form this run cannot read: ${census.unreadable.map((u) => u.reason).join("; ")} (C-13)`;
|
|
5160
|
+
return {
|
|
5161
|
+
id: this.id,
|
|
5162
|
+
anchor: census.unreadable[0].anchor,
|
|
5163
|
+
state: "could-not-judge",
|
|
5164
|
+
reason,
|
|
5165
|
+
verdict: `could not judge: ${reason}`
|
|
5166
|
+
};
|
|
5167
|
+
}
|
|
4683
5168
|
return {
|
|
4684
5169
|
id: this.id,
|
|
4685
5170
|
anchor: tools[0].anchor,
|
|
4686
5171
|
state: "non-conforming",
|
|
4687
|
-
verdict: `${tools.length} tool(s) registered but no (global, perTool) context ceiling declared anywhere (positive-control OK) -> declare
|
|
5172
|
+
verdict: `${tools.length} tool(s) registered but no (global, perTool) context ceiling declared anywhere \u2014 read: the server manifest's contextCeiling (mcp.json / mcp-server.json) and a CONTEXT_CEILING / contextCeiling const or field in source (positive-control OK) -> declare contextCeiling: { global, perTool } in the server manifest (C-13)`
|
|
5173
|
+
};
|
|
5174
|
+
}
|
|
5175
|
+
const ceiling = agreed(census);
|
|
5176
|
+
if (!ceiling) {
|
|
5177
|
+
const reason = `${census.readable.length} declared context ceilings disagree (${census.readable.map((c) => `${c.anchor} global ${c.global} perTool ${c.perTool}`).join("; ")}): which one governs is unknown (C-13)`;
|
|
5178
|
+
return {
|
|
5179
|
+
id: this.id,
|
|
5180
|
+
anchor: census.readable[0].anchor,
|
|
5181
|
+
state: "could-not-judge",
|
|
5182
|
+
reason,
|
|
5183
|
+
verdict: `could not judge: ${reason}`
|
|
4688
5184
|
};
|
|
4689
5185
|
}
|
|
4690
5186
|
let sum = 0;
|
|
@@ -4719,7 +5215,7 @@ var c13ContextCeiling = {
|
|
|
4719
5215
|
id: this.id,
|
|
4720
5216
|
anchor: ceiling.anchor,
|
|
4721
5217
|
state: "present",
|
|
4722
|
-
verdict: `${tools.length} tool(s) each declare tokensEstimeContexte within the declared per-tool ceiling (${ceiling.perTool}); sum ${sum} within global ceiling ${ceiling.global} (positive-control OK)`
|
|
5218
|
+
verdict: `${tools.length} tool(s) each declare tokensEstimeContexte within the declared per-tool ceiling (${ceiling.perTool}); sum ${sum} within global ceiling ${ceiling.global}, declared at ${ceiling.anchor} (positive-control OK)${unreadNote}`
|
|
4723
5219
|
};
|
|
4724
5220
|
}
|
|
4725
5221
|
};
|
|
@@ -4826,7 +5322,7 @@ var c14CatalogRegistration = guardUnreadablePackageJson(
|
|
|
4826
5322
|
);
|
|
4827
5323
|
|
|
4828
5324
|
// src/detectors/c15-annotations-untrusted.ts
|
|
4829
|
-
var READ_VERBS = /* @__PURE__ */ new Set(["list", "get", "search", "aggregate"]);
|
|
5325
|
+
var READ_VERBS = /* @__PURE__ */ new Set(["list", "get", "search", "aggregate", "prepare"]);
|
|
4830
5326
|
var WRITE_VERBS = /* @__PURE__ */ new Set([
|
|
4831
5327
|
"create",
|
|
4832
5328
|
"update",
|
|
@@ -5110,8 +5606,8 @@ var RECORD_LINE_RE = /^[\s>*_\-+]*Provenance\s*:\s*(.*)$/i;
|
|
|
5110
5606
|
var SHASUM_HEX_RE = /\bshasum\b[^\n]*?\b([0-9a-f]{40})\b/i;
|
|
5111
5607
|
function judgeProvenanceRecord(line, name, version) {
|
|
5112
5608
|
const body = line.match(RECORD_LINE_RE)?.[1] ?? "";
|
|
5113
|
-
const
|
|
5114
|
-
const named = new RegExp(`(?:^|[\\s(])${
|
|
5609
|
+
const esc3 = (x) => x.replace(/[.+*?^$()[\]{}|\\/]/g, "\\$&");
|
|
5610
|
+
const named = new RegExp(`(?:^|[\\s(])${esc3(name)}@v?${esc3(version)}(?![\\w.-]*\\d)`);
|
|
5115
5611
|
if (!named.test(body)) {
|
|
5116
5612
|
return { ok: false, why: `it does not name ${name}@${version}, the artifact being published` };
|
|
5117
5613
|
}
|
|
@@ -5302,7 +5798,7 @@ function manifestPath(root) {
|
|
|
5302
5798
|
if (existsSync6(extPath)) return { kind: "extension", path: extPath };
|
|
5303
5799
|
return { kind: "server", path: join7(root, "mcp.json") };
|
|
5304
5800
|
}
|
|
5305
|
-
var
|
|
5801
|
+
var SERVER_MANIFEST_NAMES2 = ["mcp.json", "mcp-server.json"];
|
|
5306
5802
|
function isObject(v) {
|
|
5307
5803
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
5308
5804
|
}
|
|
@@ -5316,7 +5812,7 @@ function classifyRootJson(json) {
|
|
|
5316
5812
|
}
|
|
5317
5813
|
function rootCandidates(root) {
|
|
5318
5814
|
const out = [];
|
|
5319
|
-
for (const name of
|
|
5815
|
+
for (const name of SERVER_MANIFEST_NAMES2) {
|
|
5320
5816
|
const path = join7(root, name);
|
|
5321
5817
|
if (!existsSync6(path)) continue;
|
|
5322
5818
|
const json = readJson(path);
|
|
@@ -14355,6 +14851,7 @@ var TRANSPORT_RE2 = /\bStreamableHTTPServerTransport\b|\bWebStandardStreamableHT
|
|
|
14355
14851
|
var ORIGIN_READ_RE = /headers?\.get\(\s*["'`]origin["'`]|headers?\[\s*["'`]origin["'`]\s*\]|headers?\.origin\b|\.header\(\s*["'`]origin["'`]\s*\)/i;
|
|
14356
14852
|
var PROTECT_FLAG_RE = /enableDnsRebindingProtection\s*:\s*true/;
|
|
14357
14853
|
var ALLOWED_ORIGINS_RE = /\ballowedOrigins\s*:\s*/g;
|
|
14854
|
+
var TYPE_ANNOTATION_RE = /^(?:readonly\s+)?(?:string\s*\[\s*\]|(?:Readonly)?Array\s*<\s*string\s*>)\s*(?:\|\s*undefined\s*)?[;,]?\s*$/;
|
|
14358
14855
|
var DISABLED_RE = /enableDnsRebindingProtection\s*:\s*false/;
|
|
14359
14856
|
var REFUSE_RE = /\b403\b|Forbidden/;
|
|
14360
14857
|
var WIDE_BIND_RE = /\.listen\s*\([^)]*["'`]0\.0\.0\.0["'`]|\bhost(?:name)?\s*:\s*["'`]0\.0\.0\.0["'`]/;
|
|
@@ -14462,12 +14959,14 @@ var s22OriginValidation = {
|
|
|
14462
14959
|
);
|
|
14463
14960
|
}
|
|
14464
14961
|
};
|
|
14465
|
-
function resolveConstLiteral(tree, name) {
|
|
14962
|
+
function resolveConstLiteral(tree, name, file) {
|
|
14466
14963
|
const re = new RegExp(
|
|
14467
14964
|
`\\b(?:const|let|var)\\s+${name.replace(/[$]/g, "\\$&")}\\b[^=\\n]*=\\s*`,
|
|
14468
14965
|
"g"
|
|
14469
14966
|
);
|
|
14470
|
-
|
|
14967
|
+
const sites = codeSites(tree, re);
|
|
14968
|
+
const ordered = file ? [...sites.filter((x) => x.file === file), ...sites.filter((x) => x.file !== file)] : sites;
|
|
14969
|
+
for (const site of ordered) {
|
|
14471
14970
|
const after = site.col + site.match[0].length;
|
|
14472
14971
|
const ch = site.line.raw[after];
|
|
14473
14972
|
if (ch === "[") {
|
|
@@ -14483,10 +14982,11 @@ function classifyAllowedOrigins(tree) {
|
|
|
14483
14982
|
const out = { sites: 0, first: "", ok: [], reds: [], unresolved: [] };
|
|
14484
14983
|
for (const site of codeSites(tree, ALLOWED_ORIGINS_RE)) {
|
|
14485
14984
|
if (!isProductSource(site.file.rel)) continue;
|
|
14486
|
-
out.sites++;
|
|
14487
|
-
out.first ||= site.anchor;
|
|
14488
14985
|
const after = site.col + site.match[0].length;
|
|
14489
14986
|
const rest = site.line.raw.slice(after);
|
|
14987
|
+
if (TYPE_ANNOTATION_RE.test(rest)) continue;
|
|
14988
|
+
out.sites++;
|
|
14989
|
+
out.first ||= site.anchor;
|
|
14490
14990
|
let value;
|
|
14491
14991
|
if (rest.startsWith("[")) {
|
|
14492
14992
|
const b = balancedBlock(site.file, site.line.n, after, "[", "]");
|
|
@@ -14494,7 +14994,7 @@ function classifyAllowedOrigins(tree) {
|
|
|
14494
14994
|
} else {
|
|
14495
14995
|
value = (rest.match(/^(?:"[^"]*"|'[^']*'|`[^`]*`|[A-Za-z_$][\w$.]*)/) ?? [rest.trim()])[0];
|
|
14496
14996
|
}
|
|
14497
|
-
const verdict = classifyOriginValue(tree, value);
|
|
14997
|
+
const verdict = classifyOriginValue(tree, value, site.file);
|
|
14498
14998
|
if (verdict.kind === "wildcard") {
|
|
14499
14999
|
out.reds.push({
|
|
14500
15000
|
anchor: site.anchor,
|
|
@@ -14513,31 +15013,42 @@ function classifyAllowedOrigins(tree) {
|
|
|
14513
15013
|
}
|
|
14514
15014
|
return out;
|
|
14515
15015
|
}
|
|
14516
|
-
function
|
|
14517
|
-
const
|
|
14518
|
-
const
|
|
14519
|
-
|
|
14520
|
-
|
|
14521
|
-
|
|
14522
|
-
|
|
14523
|
-
|
|
14524
|
-
|
|
14525
|
-
for (const el of
|
|
14526
|
-
const
|
|
14527
|
-
|
|
14528
|
-
|
|
14529
|
-
continue;
|
|
14530
|
-
}
|
|
14531
|
-
if (lit[2].includes("*")) return { kind: "wildcard", detail: lit[0] };
|
|
15016
|
+
function leavesOf(tree, v, file, depth) {
|
|
15017
|
+
const out = { strings: [], unresolved: "" };
|
|
15018
|
+
const t = v.trim();
|
|
15019
|
+
const str = t.match(/^(["'`])(.*)\1$/s);
|
|
15020
|
+
if (str) {
|
|
15021
|
+
out.strings.push(str[0]);
|
|
15022
|
+
return out;
|
|
15023
|
+
}
|
|
15024
|
+
if (t.startsWith("[")) {
|
|
15025
|
+
for (const el of splitTopLevel(t.slice(1, -1))) {
|
|
15026
|
+
const inner = leavesOf(tree, el.startsWith("...") ? el.slice(3) : el, file, depth);
|
|
15027
|
+
out.strings.push(...inner.strings);
|
|
15028
|
+
out.unresolved ||= inner.unresolved;
|
|
14532
15029
|
}
|
|
14533
|
-
return
|
|
15030
|
+
return out;
|
|
14534
15031
|
}
|
|
14535
|
-
if (/^[A-Za-z_$][\w$]*$/.test(
|
|
14536
|
-
const lit = resolveConstLiteral(tree,
|
|
14537
|
-
if (lit === null)
|
|
14538
|
-
|
|
15032
|
+
if (/^[A-Za-z_$][\w$]*$/.test(t) && depth < 3) {
|
|
15033
|
+
const lit = resolveConstLiteral(tree, t, file);
|
|
15034
|
+
if (lit === null) {
|
|
15035
|
+
out.unresolved = t;
|
|
15036
|
+
return out;
|
|
15037
|
+
}
|
|
15038
|
+
return leavesOf(tree, lit, file, depth + 1);
|
|
14539
15039
|
}
|
|
14540
|
-
|
|
15040
|
+
out.unresolved = t;
|
|
15041
|
+
return out;
|
|
15042
|
+
}
|
|
15043
|
+
function classifyOriginValue(tree, value, file) {
|
|
15044
|
+
const v = value.trim();
|
|
15045
|
+
const leaves = leavesOf(tree, v, file, 0);
|
|
15046
|
+
const wild = leaves.strings.find((x) => x.slice(1, -1).includes("*"));
|
|
15047
|
+
if (wild) return { kind: "wildcard", detail: wild };
|
|
15048
|
+
if (leaves.unresolved) return { kind: "unresolved", detail: leaves.unresolved };
|
|
15049
|
+
if (leaves.strings.length === 0)
|
|
15050
|
+
return { kind: "empty", detail: v.startsWith("[") ? v : `${v} (an empty list)` };
|
|
15051
|
+
return { kind: "ok", detail: v };
|
|
14541
15052
|
}
|
|
14542
15053
|
|
|
14543
15054
|
// src/detectors/s23-protocol-version-header.ts
|
|
@@ -15086,8 +15597,8 @@ function readDoors(backendRoot) {
|
|
|
15086
15597
|
return doors;
|
|
15087
15598
|
}
|
|
15088
15599
|
function definitionOf(name, files) {
|
|
15089
|
-
const
|
|
15090
|
-
const re = new RegExp(`(?:\\b(?:const|let|var)\\s+${
|
|
15600
|
+
const esc3 = name.replace(/\$/g, "\\$");
|
|
15601
|
+
const re = new RegExp(`(?:\\b(?:const|let|var)\\s+${esc3}\\b|\\bfunction\\s+${esc3}\\b)`);
|
|
15091
15602
|
for (const f of files) {
|
|
15092
15603
|
const hit = re.exec(f.m);
|
|
15093
15604
|
if (!hit) continue;
|
|
@@ -15654,6 +16165,329 @@ var s34AccessRecheck = {
|
|
|
15654
16165
|
}
|
|
15655
16166
|
};
|
|
15656
16167
|
|
|
16168
|
+
// src/detectors/s35-scope-in-read.ts
|
|
16169
|
+
var CALL_RE2 = /\b([A-Za-z_$][\w$]*)\s*\.\s*(query|action)\s*\(/g;
|
|
16170
|
+
var NARROW_WORD = /\b(?:org|orgId|orgSlug|organizationId|organisationId|organization|organisation|tenantId|namespace|recipient|recipientId|owner|ownerId|createdBy|assignedTo|oauthCtx|fromAllowList)\b/;
|
|
16171
|
+
var CARRY_WORD = /\b(?:orgId|organizationId|organisationId|tenantId|verifiedOrg|verifiedPerson|verifiedActor|oauthCtx)\b/;
|
|
16172
|
+
var PAGINATION_WORD = /\b(?:nextCursor|truncated|hasMore|total)\b/;
|
|
16173
|
+
function masked2(file) {
|
|
16174
|
+
return file.lines.map((line) => {
|
|
16175
|
+
let row = "";
|
|
16176
|
+
for (let c = 0; c < line.raw.length; c++) {
|
|
16177
|
+
const ch = line.raw[c];
|
|
16178
|
+
row += line.comment ? " " : ch === " " || isCodeAt(file, line.n, c) ? ch : " ";
|
|
16179
|
+
}
|
|
16180
|
+
return row;
|
|
16181
|
+
}).join("\n");
|
|
16182
|
+
}
|
|
16183
|
+
var CLOSE = { "(": ")", "[": "]", "{": "}" };
|
|
16184
|
+
function matchClose2(m, open) {
|
|
16185
|
+
const stack = [];
|
|
16186
|
+
for (let i = open; i < m.length; i++) {
|
|
16187
|
+
const ch = m[i];
|
|
16188
|
+
if (CLOSE[ch] !== void 0) stack.push(CLOSE[ch]);
|
|
16189
|
+
else if (ch === ")" || ch === "]" || ch === "}") {
|
|
16190
|
+
if (stack.pop() !== ch) return -1;
|
|
16191
|
+
if (stack.length === 0) return i;
|
|
16192
|
+
}
|
|
16193
|
+
}
|
|
16194
|
+
return -1;
|
|
16195
|
+
}
|
|
16196
|
+
function splitTop2(m, from, to) {
|
|
16197
|
+
const out = [];
|
|
16198
|
+
let depth = 0;
|
|
16199
|
+
let start = from;
|
|
16200
|
+
for (let i = from; i < to; i++) {
|
|
16201
|
+
const ch = m[i];
|
|
16202
|
+
if (ch === "(" || ch === "[" || ch === "{") depth++;
|
|
16203
|
+
else if (ch === ")" || ch === "]" || ch === "}") depth--;
|
|
16204
|
+
else if (ch === "," && depth === 0) {
|
|
16205
|
+
out.push([start, i]);
|
|
16206
|
+
start = i + 1;
|
|
16207
|
+
}
|
|
16208
|
+
}
|
|
16209
|
+
if (m.slice(start, to).trim() !== "") out.push([start, to]);
|
|
16210
|
+
return out;
|
|
16211
|
+
}
|
|
16212
|
+
function blockEnd(m, at) {
|
|
16213
|
+
let depth = 0;
|
|
16214
|
+
for (let i = at - 1; i >= 0; i--) {
|
|
16215
|
+
const ch = m[i];
|
|
16216
|
+
if (ch === "}") depth++;
|
|
16217
|
+
else if (ch === "{") {
|
|
16218
|
+
if (depth === 0) {
|
|
16219
|
+
const close = matchClose2(m, i);
|
|
16220
|
+
return close < 0 ? m.length : close;
|
|
16221
|
+
}
|
|
16222
|
+
depth--;
|
|
16223
|
+
}
|
|
16224
|
+
}
|
|
16225
|
+
return m.length;
|
|
16226
|
+
}
|
|
16227
|
+
var lineAt2 = (m, offset) => m.slice(0, offset).split("\n").length;
|
|
16228
|
+
var esc2 = (s) => s.replace(/\$/g, "\\$");
|
|
16229
|
+
function boundNames(m, callStart) {
|
|
16230
|
+
const lineStart = m.lastIndexOf("\n", callStart - 1) + 1;
|
|
16231
|
+
const before = m.slice(lineStart, callStart);
|
|
16232
|
+
const simple = before.match(
|
|
16233
|
+
/\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=]+)?=\s*(?:await\s+)?$/
|
|
16234
|
+
);
|
|
16235
|
+
if (simple) return [simple[1]];
|
|
16236
|
+
const destr = before.match(/\b(?:const|let|var)\s+\{([^}]*)\}\s*=\s*(?:await\s+)?$/);
|
|
16237
|
+
if (destr) {
|
|
16238
|
+
return destr[1].split(",").map((p) => p.trim()).filter((p) => p !== "" && !p.startsWith("...")).map((p) => (p.includes(":") ? p.split(":")[1] : p).replace(/=.*$/, "").trim()).filter((p) => /^[A-Za-z_$][\w$]*$/.test(p));
|
|
16239
|
+
}
|
|
16240
|
+
return [];
|
|
16241
|
+
}
|
|
16242
|
+
function inlineNarrowing(m, start, close, end) {
|
|
16243
|
+
let pos = m.slice(0, start).replace(/\bawait\s+$/, "").length;
|
|
16244
|
+
let grouped = 0;
|
|
16245
|
+
for (; ; ) {
|
|
16246
|
+
let depth = 0;
|
|
16247
|
+
let open2 = -1;
|
|
16248
|
+
for (let i = pos - 1; i >= 0; i--) {
|
|
16249
|
+
const ch = m[i];
|
|
16250
|
+
if (ch === ")" || ch === "]" || ch === "}") depth++;
|
|
16251
|
+
else if (ch === "(" || ch === "[" || ch === "{") {
|
|
16252
|
+
if (depth === 0) {
|
|
16253
|
+
open2 = ch === "(" ? i : -2;
|
|
16254
|
+
break;
|
|
16255
|
+
}
|
|
16256
|
+
depth--;
|
|
16257
|
+
} else if (ch === ";" && depth === 0) break;
|
|
16258
|
+
}
|
|
16259
|
+
if (open2 < 0) break;
|
|
16260
|
+
const ident = /([A-Za-z_$][\w$]*)\s*$/.exec(m.slice(0, open2));
|
|
16261
|
+
if (ident) {
|
|
16262
|
+
if (ident[1] === "scopeFilterList" || ident[1] === "scopeFilterGet") {
|
|
16263
|
+
return {
|
|
16264
|
+
narrowing: { offset: open2 - ident[0].trimEnd().length, form: ident[1] }
|
|
16265
|
+
};
|
|
16266
|
+
}
|
|
16267
|
+
break;
|
|
16268
|
+
}
|
|
16269
|
+
if (!/^\s*(?:await\s+)?$/.test(m.slice(open2 + 1, start))) break;
|
|
16270
|
+
grouped++;
|
|
16271
|
+
pos = m.slice(0, open2).replace(/\bawait\s+$/, "").length;
|
|
16272
|
+
}
|
|
16273
|
+
let p = close + 1;
|
|
16274
|
+
for (let g = 0; g < grouped; g++) {
|
|
16275
|
+
const rest = m.slice(p);
|
|
16276
|
+
const skip = /^\s*\)/.exec(rest);
|
|
16277
|
+
if (!skip) return null;
|
|
16278
|
+
p += skip[0].length;
|
|
16279
|
+
}
|
|
16280
|
+
const chain = /^\s*(?:\??\.\s*(?!filter\b|then\b)[A-Za-z_$][\w$]*\s*)*\??\.\s*(filter|then)\s*\(/.exec(
|
|
16281
|
+
m.slice(p, end)
|
|
16282
|
+
);
|
|
16283
|
+
if (!chain) return null;
|
|
16284
|
+
const open = p + chain[0].length - 1;
|
|
16285
|
+
const cbClose = matchClose2(m, open);
|
|
16286
|
+
if (cbClose < 0) return { unreadable: `a .${chain[1]}( whose closing paren is not found` };
|
|
16287
|
+
const argSpans = splitTop2(m, open + 1, cbClose);
|
|
16288
|
+
const cbSpan = argSpans[0];
|
|
16289
|
+
const cb = cbSpan ? m.slice(cbSpan[0], cbSpan[1]) : "";
|
|
16290
|
+
if (chain[1] === "filter") {
|
|
16291
|
+
return NARROW_WORD.test(cb) ? { narrowing: { offset: p + chain[0].indexOf("."), form: ".filter" } } : null;
|
|
16292
|
+
}
|
|
16293
|
+
const head = /^\s*(?:async\s+)?(?:\(\s*([A-Za-z_$][\w$]*)\s*(?::[^)]*)?\)|([A-Za-z_$][\w$]*))\s*=>\s*/.exec(
|
|
16294
|
+
cb
|
|
16295
|
+
) ?? /^\s*(?:async\s+)?function\s*[\w$]*\s*\(\s*([A-Za-z_$][\w$]*)[^)]*\)\s*(?=\{)/.exec(cb);
|
|
16296
|
+
const param = head ? head[1] ?? head[2] : void 0;
|
|
16297
|
+
if (!head || param === void 0) {
|
|
16298
|
+
return {
|
|
16299
|
+
unreadable: `a .then callback (\`${cb.trim().slice(0, 40)}\`) whose body the scan does not read`
|
|
16300
|
+
};
|
|
16301
|
+
}
|
|
16302
|
+
const bodyFrom = (cbSpan?.[0] ?? 0) + head[0].length;
|
|
16303
|
+
const bodyTo = cbSpan?.[1] ?? bodyFrom;
|
|
16304
|
+
const found = narrowingsOver(m, [param], bodyFrom, bodyTo);
|
|
16305
|
+
return found.length > 0 ? { narrowing: found[0] } : null;
|
|
16306
|
+
}
|
|
16307
|
+
function rebound(m, names, from, to) {
|
|
16308
|
+
const out = [];
|
|
16309
|
+
const region = m.slice(from, to);
|
|
16310
|
+
for (const name of names) {
|
|
16311
|
+
for (const hit of region.matchAll(
|
|
16312
|
+
new RegExp(
|
|
16313
|
+
`\\b(?:const|let|var)\\s+([A-Za-z_$][\\w$]*)\\s*(?::[^=]+)?=\\s*${esc2(name)}\\s*(?:;|$)`,
|
|
16314
|
+
"gm"
|
|
16315
|
+
)
|
|
16316
|
+
)) {
|
|
16317
|
+
out.push(hit[1]);
|
|
16318
|
+
}
|
|
16319
|
+
}
|
|
16320
|
+
return out;
|
|
16321
|
+
}
|
|
16322
|
+
function narrowingsOver(m, names, from, to) {
|
|
16323
|
+
const out = [];
|
|
16324
|
+
const region = m.slice(from, to);
|
|
16325
|
+
for (const name of names) {
|
|
16326
|
+
const n = esc2(name);
|
|
16327
|
+
for (const hit of region.matchAll(/\bscopeFilter(List|Get)\s*\(/g)) {
|
|
16328
|
+
const open = from + (hit.index ?? 0) + hit[0].length - 1;
|
|
16329
|
+
const close = matchClose2(m, open);
|
|
16330
|
+
const args = m.slice(open + 1, close < 0 ? to : close);
|
|
16331
|
+
if (new RegExp(`\\b${n}\\b`).test(args)) {
|
|
16332
|
+
out.push({ offset: from + (hit.index ?? 0), form: `scopeFilter${hit[1]}` });
|
|
16333
|
+
}
|
|
16334
|
+
}
|
|
16335
|
+
for (const hit of region.matchAll(
|
|
16336
|
+
new RegExp(`\\b${n}(?:\\s*\\??\\.\\s*[A-Za-z_$][\\w$]*)*\\s*\\??\\.\\s*filter\\s*\\(`, "g")
|
|
16337
|
+
)) {
|
|
16338
|
+
const open = from + (hit.index ?? 0) + hit[0].length - 1;
|
|
16339
|
+
const close = matchClose2(m, open);
|
|
16340
|
+
const cb = m.slice(open + 1, close < 0 ? to : close);
|
|
16341
|
+
if (NARROW_WORD.test(cb)) out.push({ offset: from + (hit.index ?? 0), form: ".filter" });
|
|
16342
|
+
}
|
|
16343
|
+
for (const hit of region.matchAll(
|
|
16344
|
+
new RegExp(
|
|
16345
|
+
`\\bfor\\s*\\(\\s*(?:const|let|var)\\s+[A-Za-z_$][\\w$]*\\s+of\\s+${n}(?:\\s*\\??\\.\\s*[A-Za-z_$][\\w$]*)*\\s*\\)\\s*\\{`,
|
|
16346
|
+
"g"
|
|
16347
|
+
)
|
|
16348
|
+
)) {
|
|
16349
|
+
const open = from + (hit.index ?? 0) + hit[0].length - 1;
|
|
16350
|
+
const close = matchClose2(m, open);
|
|
16351
|
+
const body = m.slice(open + 1, close < 0 ? to : close);
|
|
16352
|
+
for (const cond of body.matchAll(/\bif\s*\(/g)) {
|
|
16353
|
+
const co = open + 1 + (cond.index ?? 0) + cond[0].length - 1;
|
|
16354
|
+
const cc = matchClose2(m, co);
|
|
16355
|
+
if (cc < 0) continue;
|
|
16356
|
+
const after = m.slice(cc + 1, cc + 40);
|
|
16357
|
+
if (NARROW_WORD.test(m.slice(co, cc)) && /^\s*(?:\{\s*)?continue\b/.test(after)) {
|
|
16358
|
+
out.push({ offset: from + (hit.index ?? 0), form: "a skipping branch in a loop" });
|
|
16359
|
+
break;
|
|
16360
|
+
}
|
|
16361
|
+
}
|
|
16362
|
+
}
|
|
16363
|
+
}
|
|
16364
|
+
return out.sort((a, b) => a.offset - b.offset);
|
|
16365
|
+
}
|
|
16366
|
+
function scanS35(tree) {
|
|
16367
|
+
const res = { calls: 0, red: [], cnj: [], ok: [] };
|
|
16368
|
+
for (const file of tree.files) {
|
|
16369
|
+
if (!isProductSource(file.rel)) continue;
|
|
16370
|
+
const m = masked2(file);
|
|
16371
|
+
const raw = file.lines.map((l) => l.raw).join("\n");
|
|
16372
|
+
for (const hit of m.matchAll(CALL_RE2)) {
|
|
16373
|
+
if (!/convex|client/i.test(hit[1])) continue;
|
|
16374
|
+
const start = hit.index ?? 0;
|
|
16375
|
+
const open = start + hit[0].length - 1;
|
|
16376
|
+
const close = matchClose2(m, open);
|
|
16377
|
+
if (close < 0) continue;
|
|
16378
|
+
res.calls++;
|
|
16379
|
+
const anchor = `${file.rel}:${lineAt2(m, start)}`;
|
|
16380
|
+
const parts = splitTop2(m, open + 1, close);
|
|
16381
|
+
const first = parts[0] ? raw.slice(parts[0][0], parts[0][1]) : "";
|
|
16382
|
+
const cast = String.raw`(?:\s+as\s+[\w$.<>[\] ]+)?\s*$`;
|
|
16383
|
+
const lit = new RegExp(String.raw`^\s*["'\x60]([\w/]+:[\w]+)["'\x60]${cast}`).exec(first);
|
|
16384
|
+
const ref = new RegExp(String.raw`^\s*(?:api|internal)\.([\w]+)\.([\w]+)${cast}`).exec(first);
|
|
16385
|
+
const door = lit ? lit[1] : ref ? `${ref[1]}:${ref[2]}` : void 0;
|
|
16386
|
+
if (door === void 0) {
|
|
16387
|
+
res.cnj.push({
|
|
16388
|
+
anchor,
|
|
16389
|
+
note: `${anchor} names its door by an expression (\`${first.trim().slice(0, 40)}\`): which door, and so which arguments it accepts, is unknown`
|
|
16390
|
+
});
|
|
16391
|
+
continue;
|
|
16392
|
+
}
|
|
16393
|
+
const argText = parts.slice(1).map(([a, b]) => m.slice(a, b)).join(",");
|
|
16394
|
+
const carried = CARRY_WORD.test(argText);
|
|
16395
|
+
const names = boundNames(m, start);
|
|
16396
|
+
const end = blockEnd(m, start);
|
|
16397
|
+
const inline = inlineNarrowing(m, start, close, end);
|
|
16398
|
+
const narrowings = [
|
|
16399
|
+
...inline && "narrowing" in inline ? [inline.narrowing] : [],
|
|
16400
|
+
...narrowingsOver(m, [...names, ...rebound(m, names, close + 1, end)], close + 1, end)
|
|
16401
|
+
].sort((a, b) => a.offset - b.offset);
|
|
16402
|
+
if (narrowings.length === 0 && inline && "unreadable" in inline && !carried) {
|
|
16403
|
+
res.cnj.push({
|
|
16404
|
+
anchor,
|
|
16405
|
+
note: `${anchor} ${door} is followed by ${inline.unreadable}: whether it narrows the result is unread`
|
|
16406
|
+
});
|
|
16407
|
+
continue;
|
|
16408
|
+
}
|
|
16409
|
+
if (narrowings.length === 0 || carried) {
|
|
16410
|
+
res.ok.push({
|
|
16411
|
+
anchor,
|
|
16412
|
+
note: names.length === 0 && !inline ? `${anchor} ${door}: result not bound, nothing narrows it` : `${anchor} ${door}`
|
|
16413
|
+
});
|
|
16414
|
+
continue;
|
|
16415
|
+
}
|
|
16416
|
+
const spread = /\.\.\.\s*[A-Za-z_$]/.test(argText);
|
|
16417
|
+
const n = narrowings[0];
|
|
16418
|
+
const at = `${file.rel}:${lineAt2(m, n.offset)}`;
|
|
16419
|
+
if (spread) {
|
|
16420
|
+
res.cnj.push({
|
|
16421
|
+
anchor,
|
|
16422
|
+
note: `${anchor} ${door} spreads its arguments from a value the scan does not read, and ${at} narrows the result (${n.form}): whether the read carried the predicate is unread`
|
|
16423
|
+
});
|
|
16424
|
+
continue;
|
|
16425
|
+
}
|
|
16426
|
+
const paged = PAGINATION_WORD.test(m.slice(n.offset, end));
|
|
16427
|
+
res.red.push({
|
|
16428
|
+
anchor: at,
|
|
16429
|
+
note: `${at} narrows (${n.form}) the rows of ${anchor} ${door}, whose arguments carry no scope predicate${paged ? "; a pagination value (nextCursor/truncated/hasMore/total) is served after the narrowing" : ""}`
|
|
16430
|
+
});
|
|
16431
|
+
}
|
|
16432
|
+
}
|
|
16433
|
+
return res;
|
|
16434
|
+
}
|
|
16435
|
+
function s35Controls() {
|
|
16436
|
+
const red = scanS35Text(
|
|
16437
|
+
'const rows = await client.query("t:list", { limit });\nconst mine = scopeFilterList(rows, oauthCtx);\n'
|
|
16438
|
+
);
|
|
16439
|
+
const green = scanS35Text(
|
|
16440
|
+
'const rows = await client.query("t:list", { limit, orgId: oauthCtx.orgId });\nconst mine = scopeFilterList(rows, oauthCtx);\n'
|
|
16441
|
+
);
|
|
16442
|
+
return { sees: red, spares: !green };
|
|
16443
|
+
}
|
|
16444
|
+
function scanS35Text(text) {
|
|
16445
|
+
const call = /\bclient\.query\(([^)]*)\)/.exec(text);
|
|
16446
|
+
if (!call) return false;
|
|
16447
|
+
const carried = CARRY_WORD.test(call[1].split(",").slice(1).join(","));
|
|
16448
|
+
return !carried && narrowingsOver(text, ["rows"], call.index + call[0].length, text.length).length > 0;
|
|
16449
|
+
}
|
|
16450
|
+
var s35ScopeInRead = {
|
|
16451
|
+
id: "S35 scope-in-read",
|
|
16452
|
+
rule: "S-35",
|
|
16453
|
+
pin: STANDARD_PIN,
|
|
16454
|
+
run(tree) {
|
|
16455
|
+
const ctl = s35Controls();
|
|
16456
|
+
if (!ctl.sees || !ctl.spares) {
|
|
16457
|
+
throw new Error(
|
|
16458
|
+
`S35 positive control FAILED: ${JSON.stringify(ctl)} \u2014 the narrowing matcher cannot tell a filter after an unscoped read from a repeated one, verdict untrustworthy`
|
|
16459
|
+
);
|
|
16460
|
+
}
|
|
16461
|
+
const scan = scanS35(tree);
|
|
16462
|
+
if (scan.calls === 0) {
|
|
16463
|
+
return absentResult(
|
|
16464
|
+
this.id,
|
|
16465
|
+
"-",
|
|
16466
|
+
"no backend call (<client>.query|action) in product source (positive control OK) \u2014 S-35 n/a"
|
|
16467
|
+
);
|
|
16468
|
+
}
|
|
16469
|
+
if (scan.red.length > 0) {
|
|
16470
|
+
return redResult(
|
|
16471
|
+
this.id,
|
|
16472
|
+
scan.red[0].anchor,
|
|
16473
|
+
`${scan.red.map((s) => s.note).join("; ")} -> pass the scope predicate to the door as an argument and forward its pagination unchanged (S-35)`
|
|
16474
|
+
);
|
|
16475
|
+
}
|
|
16476
|
+
if (scan.cnj.length > 0) {
|
|
16477
|
+
return couldNotJudge(
|
|
16478
|
+
this.id,
|
|
16479
|
+
scan.cnj[0].anchor,
|
|
16480
|
+
`${scan.cnj.map((s) => s.note).join("; ")} (S-35)`
|
|
16481
|
+
);
|
|
16482
|
+
}
|
|
16483
|
+
return presentResult(
|
|
16484
|
+
this.id,
|
|
16485
|
+
scan.ok[0].anchor,
|
|
16486
|
+
`${scan.calls} backend call(s) read; no scope narrowing over an unscoped result (positive control OK). Not decided here: a narrowing in a helper the scan cannot find, and the door applying the predicate (R-11, R-55) (S-35)`
|
|
16487
|
+
);
|
|
16488
|
+
}
|
|
16489
|
+
};
|
|
16490
|
+
|
|
15657
16491
|
// src/detectors/index.ts
|
|
15658
16492
|
var registered = [
|
|
15659
16493
|
d2,
|
|
@@ -15752,6 +16586,7 @@ var registered = [
|
|
|
15752
16586
|
s25ResourceDiscipline,
|
|
15753
16587
|
s26ListChangedParity,
|
|
15754
16588
|
s27ElicitationFloor,
|
|
16589
|
+
s35ScopeInRead,
|
|
15755
16590
|
e5ExtensionIsolation,
|
|
15756
16591
|
a3SandboxRespected,
|
|
15757
16592
|
a4WcagGate,
|
|
@@ -15778,6 +16613,7 @@ var READS_CODE = /* @__PURE__ */ new Set([
|
|
|
15778
16613
|
"S-25",
|
|
15779
16614
|
"S-26",
|
|
15780
16615
|
"S-27",
|
|
16616
|
+
"S-35",
|
|
15781
16617
|
"E-5",
|
|
15782
16618
|
"A-3"
|
|
15783
16619
|
]);
|
|
@@ -15856,6 +16692,7 @@ function deriveLaunchPlan(target) {
|
|
|
15856
16692
|
var INTROSPECTOR = String.raw`
|
|
15857
16693
|
import { spawn } from "node:child_process";
|
|
15858
16694
|
const plan = JSON.parse(process.env.MCP_DOCTOR_PLAN);
|
|
16695
|
+
const INVALID_CURSOR = "mcp-doctor:invalid-cursor:%%%";
|
|
15859
16696
|
const done = (o) => { process.stdout.write(JSON.stringify(o)); process.exit(0); };
|
|
15860
16697
|
let child;
|
|
15861
16698
|
try {
|
|
@@ -15865,11 +16702,35 @@ try {
|
|
|
15865
16702
|
}
|
|
15866
16703
|
let settled = false;
|
|
15867
16704
|
let serverInfo;
|
|
16705
|
+
let capabilities = {};
|
|
16706
|
+
let tools;
|
|
16707
|
+
const probes = [];
|
|
16708
|
+
const pending = new Map();
|
|
16709
|
+
let nextId = 10;
|
|
16710
|
+
let probeTimer;
|
|
15868
16711
|
const fail = (msg) => { if (settled) return; settled = true; try { child.kill(); } catch {} done({ launchError: msg }); };
|
|
15869
|
-
const
|
|
16712
|
+
const finish = (why) => {
|
|
16713
|
+
if (settled) return; settled = true; clearTimeout(to); clearTimeout(probeTimer);
|
|
16714
|
+
for (const idx of pending.values()) probes[idx].outcome = { kind: "no-answer", reason: why };
|
|
16715
|
+
try { child.kill(); } catch {}
|
|
16716
|
+
done({ tools, serverInfo, cursorProbes: probes });
|
|
16717
|
+
};
|
|
16718
|
+
const to = setTimeout(() => {
|
|
16719
|
+
if (tools) finish("no answer before the introspection deadline");
|
|
16720
|
+
else fail("timeout: server did not answer initialize+tools/list within the deadline");
|
|
16721
|
+
}, 12000);
|
|
15870
16722
|
child.on("error", (e) => fail("spawn error: " + (e && e.message ? e.message : String(e))));
|
|
15871
|
-
child.on("exit", (code) =>
|
|
16723
|
+
child.on("exit", (code) => {
|
|
16724
|
+
if (tools) finish("the server exited (code " + code + ") before answering");
|
|
16725
|
+
else fail("server exited (code " + code + ") before completing the MCP handshake");
|
|
16726
|
+
});
|
|
15872
16727
|
const send = (o) => { try { child.stdin.write(JSON.stringify(o) + "\n"); } catch (e) { fail("write failed: " + e.message); } };
|
|
16728
|
+
const probe = (target, surface, method, params) => {
|
|
16729
|
+
const id = nextId++;
|
|
16730
|
+
pending.set(id, probes.length);
|
|
16731
|
+
probes.push({ target, surface });
|
|
16732
|
+
send({ jsonrpc: "2.0", id, method, params });
|
|
16733
|
+
};
|
|
15873
16734
|
let buf = "";
|
|
15874
16735
|
child.stdout.on("data", (d) => {
|
|
15875
16736
|
buf += d.toString();
|
|
@@ -15878,6 +16739,8 @@ child.stdout.on("data", (d) => {
|
|
|
15878
16739
|
const line = buf.slice(0, i); buf = buf.slice(i + 1);
|
|
15879
16740
|
if (!line.trim()) continue;
|
|
15880
16741
|
let msg; try { msg = JSON.parse(line); } catch { continue; }
|
|
16742
|
+
// A request or notification FROM the server carries a method: it is not an answer.
|
|
16743
|
+
if (msg.method !== undefined) continue;
|
|
15881
16744
|
if (msg.id === 1) {
|
|
15882
16745
|
// The initialize response's serverInfo is the SAME non-authenticated
|
|
15883
16746
|
// discovery surface a client reads version from (S-7); captured here
|
|
@@ -15885,13 +16748,46 @@ child.stdout.on("data", (d) => {
|
|
|
15885
16748
|
// alongside it, additively -- every existing consumer of the
|
|
15886
16749
|
// tools-only introspector output is unaffected.
|
|
15887
16750
|
serverInfo = msg.result && msg.result.serverInfo;
|
|
16751
|
+
capabilities = (msg.result && msg.result.capabilities) || {};
|
|
15888
16752
|
send({ jsonrpc: "2.0", method: "notifications/initialized" });
|
|
15889
16753
|
send({ jsonrpc: "2.0", id: 2, method: "tools/list", params: {} });
|
|
15890
16754
|
} else if (msg.id === 2) {
|
|
15891
|
-
if (settled) return;
|
|
15892
|
-
const
|
|
15893
|
-
|
|
15894
|
-
|
|
16755
|
+
if (settled) return;
|
|
16756
|
+
const served = msg.result && Array.isArray(msg.result.tools) ? msg.result.tools : [];
|
|
16757
|
+
const schemaOf = (t) => (t && t.inputSchema && typeof t.inputSchema === "object" ? t.inputSchema : {});
|
|
16758
|
+
const takesCursor = (t) => {
|
|
16759
|
+
const props = schemaOf(t).properties;
|
|
16760
|
+
return Boolean(props && typeof props === "object" && Object.prototype.hasOwnProperty.call(props, "cursor"));
|
|
16761
|
+
};
|
|
16762
|
+
tools = served.map((t) => ({ name: String(t && t.name), description: t && typeof t.description === "string" ? t.description : undefined, cursorArgument: takesCursor(t) }));
|
|
16763
|
+
const lists = ["tools/list"];
|
|
16764
|
+
if (capabilities.resources) lists.push("resources/list", "resources/templates/list");
|
|
16765
|
+
if (capabilities.prompts) lists.push("prompts/list");
|
|
16766
|
+
for (const m of lists) probe(m, "list-method", m, { cursor: INVALID_CURSOR });
|
|
16767
|
+
for (const t of served) {
|
|
16768
|
+
if (!takesCursor(t)) continue;
|
|
16769
|
+
const name = String(t && t.name);
|
|
16770
|
+
const required = Array.isArray(schemaOf(t).required) ? schemaOf(t).required.filter((r) => r !== "cursor") : [];
|
|
16771
|
+
if (required.length > 0) {
|
|
16772
|
+
probes.push({ target: name, surface: "tool-argument", outcome: { kind: "not-sent", reason: "the tool requires " + required.join(", ") + " beside the cursor; the probe never invents a value for them" } });
|
|
16773
|
+
continue;
|
|
16774
|
+
}
|
|
16775
|
+
probe(name, "tool-argument", "tools/call", { name, arguments: { cursor: INVALID_CURSOR } });
|
|
16776
|
+
}
|
|
16777
|
+
if (pending.size === 0) { finish("no probe pending"); return; }
|
|
16778
|
+
probeTimer = setTimeout(() => finish("no answer within the 5 s probe deadline"), 5000);
|
|
16779
|
+
} else if (pending.has(msg.id)) {
|
|
16780
|
+
const idx = pending.get(msg.id); pending.delete(msg.id);
|
|
16781
|
+
if (msg.error && typeof msg.error === "object") {
|
|
16782
|
+
probes[idx].outcome = { kind: "error", code: msg.error.code, message: typeof msg.error.message === "string" ? msg.error.message : undefined };
|
|
16783
|
+
} else if (msg.result && typeof msg.result === "object") {
|
|
16784
|
+
const sc = msg.result.structuredContent;
|
|
16785
|
+
const err = sc && typeof sc === "object" ? sc.error : undefined;
|
|
16786
|
+
probes[idx].outcome = { kind: "result", isError: msg.result.isError === true, errorCode: err && typeof err === "object" ? err.code : undefined };
|
|
16787
|
+
} else {
|
|
16788
|
+
probes[idx].outcome = { kind: "no-answer", reason: "an answer with neither result nor error" };
|
|
16789
|
+
}
|
|
16790
|
+
if (pending.size === 0) finish("no probe pending");
|
|
15895
16791
|
}
|
|
15896
16792
|
}
|
|
15897
16793
|
});
|
|
@@ -15942,6 +16838,54 @@ function deriveExpectedCommit(cwd) {
|
|
|
15942
16838
|
}
|
|
15943
16839
|
return void 0;
|
|
15944
16840
|
}
|
|
16841
|
+
function readCursorProbe(raw) {
|
|
16842
|
+
const r = raw ?? {};
|
|
16843
|
+
const target = typeof r.target === "string" ? r.target : "<unnamed>";
|
|
16844
|
+
const surface = r.surface === "tool-argument" ? "tool-argument" : "list-method";
|
|
16845
|
+
const o = r.outcome ?? {};
|
|
16846
|
+
const unreadable = (what) => ({
|
|
16847
|
+
target,
|
|
16848
|
+
surface,
|
|
16849
|
+
outcome: { kind: "no-answer", reason: `unreadable probe record (${what})` }
|
|
16850
|
+
});
|
|
16851
|
+
if (r.surface !== "tool-argument" && r.surface !== "list-method") {
|
|
16852
|
+
return unreadable(`surface ${JSON.stringify(r.surface)}`);
|
|
16853
|
+
}
|
|
16854
|
+
switch (o.kind) {
|
|
16855
|
+
case "error":
|
|
16856
|
+
return {
|
|
16857
|
+
target,
|
|
16858
|
+
surface,
|
|
16859
|
+
outcome: {
|
|
16860
|
+
kind: "error",
|
|
16861
|
+
code: o.code,
|
|
16862
|
+
...typeof o.message === "string" ? { message: o.message } : {}
|
|
16863
|
+
}
|
|
16864
|
+
};
|
|
16865
|
+
case "result":
|
|
16866
|
+
return {
|
|
16867
|
+
target,
|
|
16868
|
+
surface,
|
|
16869
|
+
outcome: {
|
|
16870
|
+
kind: "result",
|
|
16871
|
+
isError: o.isError === true,
|
|
16872
|
+
...o.errorCode !== void 0 ? { errorCode: o.errorCode } : {}
|
|
16873
|
+
}
|
|
16874
|
+
};
|
|
16875
|
+
case "no-answer":
|
|
16876
|
+
case "not-sent":
|
|
16877
|
+
return {
|
|
16878
|
+
target,
|
|
16879
|
+
surface,
|
|
16880
|
+
outcome: {
|
|
16881
|
+
kind: o.kind,
|
|
16882
|
+
reason: typeof o.reason === "string" ? o.reason : "no reason given"
|
|
16883
|
+
}
|
|
16884
|
+
};
|
|
16885
|
+
default:
|
|
16886
|
+
return unreadable(`outcome kind ${JSON.stringify(o.kind)}`);
|
|
16887
|
+
}
|
|
16888
|
+
}
|
|
15945
16889
|
function launchAndIntrospect(target) {
|
|
15946
16890
|
const plan = deriveLaunchPlan(target);
|
|
15947
16891
|
if ("error" in plan) {
|
|
@@ -15987,14 +16931,16 @@ function launchAndIntrospect(target) {
|
|
|
15987
16931
|
const name = t.name;
|
|
15988
16932
|
const count = evalCounts.get(name);
|
|
15989
16933
|
const base = t.description ? { name, description: t.description } : { name };
|
|
15990
|
-
|
|
16934
|
+
const withCursor = t.cursorArgument === true ? { ...base, cursorArgument: true } : base;
|
|
16935
|
+
return count === void 0 ? withCursor : { ...withCursor, evalCaseCount: count };
|
|
15991
16936
|
});
|
|
15992
16937
|
const expectedCommit = deriveExpectedCommit(plan.cwd);
|
|
15993
16938
|
return {
|
|
15994
16939
|
root: target,
|
|
15995
16940
|
tools,
|
|
15996
16941
|
...parsed.serverInfo ? { serverInfo: parsed.serverInfo } : {},
|
|
15997
|
-
...expectedCommit ? { expectedCommit } : {}
|
|
16942
|
+
...expectedCommit ? { expectedCommit } : {},
|
|
16943
|
+
...Array.isArray(parsed.cursorProbes) ? { cursorProbes: parsed.cursorProbes.map(readCursorProbe) } : {}
|
|
15998
16944
|
};
|
|
15999
16945
|
}
|
|
16000
16946
|
|
|
@@ -16246,7 +17192,8 @@ var USAGE = [
|
|
|
16246
17192
|
" --version print the version and exit",
|
|
16247
17193
|
" --help print this and exit",
|
|
16248
17194
|
"",
|
|
16249
|
-
"exit: 0 clean, 1 a violation, 2 an abstention with no violation"
|
|
17195
|
+
"exit: 0 clean, 1 a violation, 2 an abstention with no violation (including a target",
|
|
17196
|
+
" that names nothing readable: missing, empty, or not a source tree)"
|
|
16250
17197
|
].join("\n");
|
|
16251
17198
|
function readVersion() {
|
|
16252
17199
|
try {
|
|
@@ -16297,10 +17244,28 @@ function main() {
|
|
|
16297
17244
|
detectorList = detectors.map((d) => d === localIdentity ? ratchet : d);
|
|
16298
17245
|
}
|
|
16299
17246
|
const backend = backendArg ? backendArg.slice("--backend=".length) : void 0;
|
|
16300
|
-
|
|
16301
|
-
|
|
16302
|
-
|
|
16303
|
-
|
|
17247
|
+
let report;
|
|
17248
|
+
try {
|
|
17249
|
+
report = runDoctorReport(target, detectorList, {
|
|
17250
|
+
mode,
|
|
17251
|
+
...backend !== void 0 ? { backend } : {}
|
|
17252
|
+
});
|
|
17253
|
+
} catch (err) {
|
|
17254
|
+
if (!(err instanceof SubjectUnreadableError)) throw err;
|
|
17255
|
+
if (args.includes("--json")) {
|
|
17256
|
+
console.log(
|
|
17257
|
+
JSON.stringify(
|
|
17258
|
+
{ surface: target, exitCode: 2, subjectUnreadable: err.form, results: [], skipped: [] },
|
|
17259
|
+
null,
|
|
17260
|
+
2
|
|
17261
|
+
)
|
|
17262
|
+
);
|
|
17263
|
+
}
|
|
17264
|
+
console.error(err.message);
|
|
17265
|
+
process.exitCode = 2;
|
|
17266
|
+
return;
|
|
17267
|
+
}
|
|
17268
|
+
const { results, skipped } = report;
|
|
16304
17269
|
console.log(
|
|
16305
17270
|
args.includes("--json") ? renderJson(target, results, skipped) : renderTable(target, results, skipped)
|
|
16306
17271
|
);
|
|
@@ -16323,6 +17288,7 @@ function isDirectInvocation() {
|
|
|
16323
17288
|
}
|
|
16324
17289
|
if (isDirectInvocation()) main();
|
|
16325
17290
|
export {
|
|
17291
|
+
SubjectUnreadableError,
|
|
16326
17292
|
computeExitCode,
|
|
16327
17293
|
countOwedHumanVerdicts,
|
|
16328
17294
|
isLiveDetector,
|