calllint 0.3.0-rc.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/index.js +622 -29
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -60,6 +60,7 @@ will do, it says so and never silently upgrades to `SAFE`.
|
|
|
60
60
|
## More
|
|
61
61
|
|
|
62
62
|
- SARIF 2.1.0 for GitHub Code Scanning: `calllint scan <config> --sarif`
|
|
63
|
+
- Editor / agent-host diagnostics JSON (`calllint.diagnostics.v0`): `calllint diagnostics <config> --json`
|
|
63
64
|
- Self-contained HTML report: `calllint scan <config> --html > report.html`
|
|
64
65
|
- Drift / rug-pull detection: `calllint baseline <config>` then `calllint verify <config> --ci`
|
|
65
66
|
- Policy-as-code: `calllint policy init`
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync5 } from "node:fs";
|
|
5
5
|
|
|
6
6
|
// src/args.ts
|
|
7
7
|
var EXIT = {
|
|
@@ -57,6 +57,7 @@ USAGE
|
|
|
57
57
|
|
|
58
58
|
COMMANDS
|
|
59
59
|
scan [target] Scan an MCP config file, or npm:<pkg> / github:<owner/repo>
|
|
60
|
+
diagnostics [target] Emit editor/agent-host diagnostics JSON (calllint.diagnostics.v0)
|
|
60
61
|
baseline [target] Record the approved risk surface as a baseline
|
|
61
62
|
verify [target] Compare a fresh scan against the baseline (drift / rug-pull)
|
|
62
63
|
explain <server> Explain the verdict for one server from the last scan
|
|
@@ -89,6 +90,7 @@ EXAMPLES
|
|
|
89
90
|
calllint scan .cursor/mcp.json
|
|
90
91
|
cat .cursor/mcp.json | calllint scan --stdin --json
|
|
91
92
|
calllint scan ./mcp.json --ci --no-emoji
|
|
93
|
+
calllint diagnostics ./mcp.json --json
|
|
92
94
|
calllint scan npm:mcp-weather@1.0.0
|
|
93
95
|
calllint scan github:owner/repo --online
|
|
94
96
|
calllint baseline ./mcp.json
|
|
@@ -100,7 +102,7 @@ function helpCommand() {
|
|
|
100
102
|
}
|
|
101
103
|
|
|
102
104
|
// src/commands/scan.ts
|
|
103
|
-
import { join as
|
|
105
|
+
import { join as join4, resolve } from "node:path";
|
|
104
106
|
|
|
105
107
|
// ../../packages/policy/src/defaultPolicy.ts
|
|
106
108
|
function defaultPolicy() {
|
|
@@ -267,7 +269,8 @@ function resolveScanOptions(opts) {
|
|
|
267
269
|
policy: opts?.policy ?? defaultPolicy(),
|
|
268
270
|
now: opts?.now ?? 0,
|
|
269
271
|
generatedAt: opts?.generatedAt ?? "1970-01-01T00:00:00.000Z",
|
|
270
|
-
extraFindings: opts?.extraFindings ?? {}
|
|
272
|
+
extraFindings: opts?.extraFindings ?? {},
|
|
273
|
+
surfaces: opts?.surfaces ?? []
|
|
271
274
|
};
|
|
272
275
|
}
|
|
273
276
|
|
|
@@ -534,6 +537,56 @@ function looksLikeBroadPath(arg) {
|
|
|
534
537
|
if (/^[A-Za-z]:\\Users\\[^\\]+\\?$/.test(arg)) return true;
|
|
535
538
|
return false;
|
|
536
539
|
}
|
|
540
|
+
function looksLikeHostPath(s) {
|
|
541
|
+
if (!s) return false;
|
|
542
|
+
return s.startsWith("/") || // POSIX absolute (incl. /Users, /home, /etc, ...)
|
|
543
|
+
s.startsWith("~") || // home
|
|
544
|
+
s.startsWith(".") || // workspace-relative (handled as not-broad by isWorkspaceScoped/below)
|
|
545
|
+
/^[A-Za-z]:[\\/]/.test(s) || // Windows drive, C:\ or C:/
|
|
546
|
+
s.startsWith("$") || // $HOME / ${HOME}
|
|
547
|
+
s.startsWith("%");
|
|
548
|
+
}
|
|
549
|
+
function extractDockerHostPaths(args) {
|
|
550
|
+
const hosts = [];
|
|
551
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
552
|
+
const arg = args[i];
|
|
553
|
+
if (arg === "--mount" || arg.startsWith("--mount=")) {
|
|
554
|
+
const csv = arg === "--mount" ? args[i + 1] ?? "" : arg.slice("--mount=".length);
|
|
555
|
+
if (arg === "--mount") i += 1;
|
|
556
|
+
const fields = /* @__PURE__ */ new Map();
|
|
557
|
+
for (const part of csv.split(",")) {
|
|
558
|
+
const eq = part.indexOf("=");
|
|
559
|
+
if (eq === -1) continue;
|
|
560
|
+
fields.set(part.slice(0, eq).trim(), part.slice(eq + 1).trim());
|
|
561
|
+
}
|
|
562
|
+
const type = fields.get("type") ?? "volume";
|
|
563
|
+
if (type !== "bind") continue;
|
|
564
|
+
const src = fields.get("source") ?? fields.get("src");
|
|
565
|
+
if (src && looksLikeHostPath(src)) hosts.push(src);
|
|
566
|
+
continue;
|
|
567
|
+
}
|
|
568
|
+
let spec;
|
|
569
|
+
if (arg === "-v" || arg === "--volume") {
|
|
570
|
+
spec = args[i + 1];
|
|
571
|
+
i += 1;
|
|
572
|
+
} else if (arg.startsWith("-v=")) {
|
|
573
|
+
spec = arg.slice("-v=".length);
|
|
574
|
+
} else if (arg.startsWith("--volume=")) {
|
|
575
|
+
spec = arg.slice("--volume=".length);
|
|
576
|
+
}
|
|
577
|
+
if (spec === void 0) continue;
|
|
578
|
+
const host = dockerVolumeHostSide(spec);
|
|
579
|
+
if (host && looksLikeHostPath(host)) hosts.push(host);
|
|
580
|
+
}
|
|
581
|
+
return hosts;
|
|
582
|
+
}
|
|
583
|
+
function dockerVolumeHostSide(spec) {
|
|
584
|
+
const winDrive = /^[A-Za-z]:[\\/]/.test(spec);
|
|
585
|
+
const searchFrom = winDrive ? 2 : 0;
|
|
586
|
+
const sep = spec.indexOf(":", searchFrom);
|
|
587
|
+
if (sep === -1) return void 0;
|
|
588
|
+
return spec.slice(0, sep);
|
|
589
|
+
}
|
|
537
590
|
function detectBroadFilesystemPath(ctx) {
|
|
538
591
|
const { server } = ctx;
|
|
539
592
|
const evidence = [];
|
|
@@ -547,6 +600,18 @@ function detectBroadFilesystemPath(ctx) {
|
|
|
547
600
|
});
|
|
548
601
|
}
|
|
549
602
|
}
|
|
603
|
+
if ((server.command ?? "").toLowerCase() === "docker") {
|
|
604
|
+
for (const host of extractDockerHostPaths(server.args)) {
|
|
605
|
+
if (looksLikeBroadPath(host)) {
|
|
606
|
+
evidence.push({
|
|
607
|
+
type: "config",
|
|
608
|
+
path: server.sourceConfigPath,
|
|
609
|
+
key: "args",
|
|
610
|
+
value: host
|
|
611
|
+
});
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
}
|
|
550
615
|
if (evidence.length === 0) return [];
|
|
551
616
|
return [
|
|
552
617
|
{
|
|
@@ -727,7 +792,7 @@ function detectUnknownRemote(ctx) {
|
|
|
727
792
|
];
|
|
728
793
|
}
|
|
729
794
|
|
|
730
|
-
// ../../packages/static-analyzer/src/
|
|
795
|
+
// ../../packages/static-analyzer/src/promptScan.ts
|
|
731
796
|
var POISON_PATTERNS = [
|
|
732
797
|
"ignore previous instructions",
|
|
733
798
|
"ignore all previous instructions",
|
|
@@ -749,23 +814,69 @@ var POISON_PATTERNS = [
|
|
|
749
814
|
"override the system prompt",
|
|
750
815
|
"you are now"
|
|
751
816
|
];
|
|
752
|
-
function
|
|
817
|
+
function findPoisonPhrases(text) {
|
|
753
818
|
if (!text) return [];
|
|
754
819
|
const lower = text.toLowerCase();
|
|
755
820
|
const hits = [];
|
|
756
821
|
for (const pattern of POISON_PATTERNS) {
|
|
757
|
-
if (lower.includes(pattern))
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
822
|
+
if (lower.includes(pattern)) hits.push(pattern);
|
|
823
|
+
}
|
|
824
|
+
return hits;
|
|
825
|
+
}
|
|
826
|
+
var INVISIBLE_CODE_POINTS = [8203, 8204, 8205, 8288, 65279];
|
|
827
|
+
var BIDI_RANGES = [
|
|
828
|
+
[8234, 8238],
|
|
829
|
+
[8294, 8297]
|
|
830
|
+
];
|
|
831
|
+
var TAG_RANGE = [917504, 917631];
|
|
832
|
+
function hasInvisible(text) {
|
|
833
|
+
for (const ch of text) {
|
|
834
|
+
if (INVISIBLE_CODE_POINTS.includes(ch.codePointAt(0))) return true;
|
|
835
|
+
}
|
|
836
|
+
return false;
|
|
837
|
+
}
|
|
838
|
+
function inAnyRange(text, ranges) {
|
|
839
|
+
for (const ch of text) {
|
|
840
|
+
const cp = ch.codePointAt(0);
|
|
841
|
+
for (const [lo, hi] of ranges) {
|
|
842
|
+
if (cp >= lo && cp <= hi) return true;
|
|
767
843
|
}
|
|
768
844
|
}
|
|
845
|
+
return false;
|
|
846
|
+
}
|
|
847
|
+
function findHiddenContent(text) {
|
|
848
|
+
if (!text) return [];
|
|
849
|
+
const categories = [];
|
|
850
|
+
if (hasInvisible(text)) categories.push("zero-width or invisible characters");
|
|
851
|
+
if (inAnyRange(text, BIDI_RANGES)) {
|
|
852
|
+
categories.push("Unicode bidirectional override controls");
|
|
853
|
+
}
|
|
854
|
+
if (inAnyRange(text, [TAG_RANGE])) {
|
|
855
|
+
categories.push("invisible tag-character ASCII smuggling");
|
|
856
|
+
}
|
|
857
|
+
if (/<!--[\s\S]*?-->/.test(text)) categories.push("embedded HTML/XML comment");
|
|
858
|
+
return categories;
|
|
859
|
+
}
|
|
860
|
+
function poisonEvidence(pattern, source) {
|
|
861
|
+
return { type: source.type, path: source.path, key: source.key, snippet: pattern };
|
|
862
|
+
}
|
|
863
|
+
function hiddenEvidence(category, source) {
|
|
864
|
+
return { type: source.type, path: source.path, key: source.key, snippet: category };
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
// ../../packages/static-analyzer/src/detectors/promptPoisoning.ts
|
|
868
|
+
function scanText(text, source) {
|
|
869
|
+
const hits = [];
|
|
870
|
+
for (const pattern of findPoisonPhrases(text)) {
|
|
871
|
+
hits.push({
|
|
872
|
+
pattern,
|
|
873
|
+
evidence: poisonEvidence(pattern, {
|
|
874
|
+
type: "tool-metadata",
|
|
875
|
+
path: source.path,
|
|
876
|
+
key: source.key
|
|
877
|
+
})
|
|
878
|
+
});
|
|
879
|
+
}
|
|
769
880
|
return hits;
|
|
770
881
|
}
|
|
771
882
|
function detectPromptPoisoning(ctx) {
|
|
@@ -1009,6 +1120,93 @@ function detectFinancialAction(ctx) {
|
|
|
1009
1120
|
return findings;
|
|
1010
1121
|
}
|
|
1011
1122
|
|
|
1123
|
+
// ../../packages/static-analyzer/src/detectors/unverifiedLocalSource.ts
|
|
1124
|
+
function localSubject(args) {
|
|
1125
|
+
for (const a of args) {
|
|
1126
|
+
if (a === "-y" || a === "--yes" || a.startsWith("-")) continue;
|
|
1127
|
+
return a;
|
|
1128
|
+
}
|
|
1129
|
+
return void 0;
|
|
1130
|
+
}
|
|
1131
|
+
function detectUnverifiedLocalSource(ctx) {
|
|
1132
|
+
const { binding, server } = ctx;
|
|
1133
|
+
if (!binding.sourceKnown) return [];
|
|
1134
|
+
if (!binding.runtimeExecutable) return [];
|
|
1135
|
+
if (binding.runtimeKind === "docker") return [];
|
|
1136
|
+
if (binding.packageName) return [];
|
|
1137
|
+
const command = binding.declaredCommand ?? server.command;
|
|
1138
|
+
if (!command) return [];
|
|
1139
|
+
const subject = localSubject(binding.declaredArgs);
|
|
1140
|
+
const display = subject ? `${command} ${subject}` : command;
|
|
1141
|
+
const evidence = [
|
|
1142
|
+
{
|
|
1143
|
+
type: "runtime-binding",
|
|
1144
|
+
path: server.sourceConfigPath,
|
|
1145
|
+
key: "command",
|
|
1146
|
+
value: display
|
|
1147
|
+
}
|
|
1148
|
+
];
|
|
1149
|
+
return [
|
|
1150
|
+
{
|
|
1151
|
+
id: "exec.unverified-local-source",
|
|
1152
|
+
title: "Runs an unverified local executable",
|
|
1153
|
+
severity: "medium",
|
|
1154
|
+
blocker: false,
|
|
1155
|
+
symbol: "EXEC",
|
|
1156
|
+
riskClass: "S2",
|
|
1157
|
+
mode: "OBSERVED",
|
|
1158
|
+
confidence: "medium",
|
|
1159
|
+
detectionMethod: "runtime-binding",
|
|
1160
|
+
evidence,
|
|
1161
|
+
impact: "The runtime executes a local script or binary whose contents CallLint never inspects. It is not a recognized package or pinned image, so what actually runs cannot be verified from the config alone.",
|
|
1162
|
+
fix: "Confirm you trust the local source, or run it from a pinned, recognized package (e.g. npx pkg@1.2.3 / a pinned docker image) so the runtime is independently verifiable.",
|
|
1163
|
+
falsePositiveNote: "A developer running their own local server (e.g. node ./dist/server.js) is normal; this flags that the source is not independently verifiable, not that it is malicious."
|
|
1164
|
+
}
|
|
1165
|
+
];
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// ../../packages/static-analyzer/src/detectors/hiddenInstructions.ts
|
|
1169
|
+
function scanHidden(text, key, path) {
|
|
1170
|
+
const out = [];
|
|
1171
|
+
for (const category of findHiddenContent(text)) {
|
|
1172
|
+
out.push(hiddenEvidence(category, { type: "tool-metadata", path, key }));
|
|
1173
|
+
}
|
|
1174
|
+
return out;
|
|
1175
|
+
}
|
|
1176
|
+
function detectHiddenInstructions(ctx) {
|
|
1177
|
+
const { server } = ctx;
|
|
1178
|
+
const evidence = [];
|
|
1179
|
+
evidence.push(...scanHidden(server.instructions, "instructions", server.sourceConfigPath));
|
|
1180
|
+
for (const tool of server.providedTools) {
|
|
1181
|
+
const base = tool.name ? `tools.${tool.name}` : "tools";
|
|
1182
|
+
evidence.push(...scanHidden(tool.name, `${base}.name`, server.sourceConfigPath));
|
|
1183
|
+
evidence.push(
|
|
1184
|
+
...scanHidden(tool.description, `${base}.description`, server.sourceConfigPath)
|
|
1185
|
+
);
|
|
1186
|
+
evidence.push(
|
|
1187
|
+
...scanHidden(tool.inputSchemaText, `${base}.inputSchema`, server.sourceConfigPath)
|
|
1188
|
+
);
|
|
1189
|
+
}
|
|
1190
|
+
if (evidence.length === 0) return [];
|
|
1191
|
+
return [
|
|
1192
|
+
{
|
|
1193
|
+
id: "prompt.hidden-instructions",
|
|
1194
|
+
title: "Hidden or obfuscated content in model-visible metadata",
|
|
1195
|
+
severity: "medium",
|
|
1196
|
+
blocker: false,
|
|
1197
|
+
symbol: "PROMPT",
|
|
1198
|
+
riskClass: "S2",
|
|
1199
|
+
mode: "OBSERVED",
|
|
1200
|
+
confidence: "medium",
|
|
1201
|
+
detectionMethod: "tool-metadata",
|
|
1202
|
+
evidence,
|
|
1203
|
+
impact: "Invisible or obfuscated characters in tool metadata reach the model but not a human reader, so a model-directed instruction can hide from review while still steering autonomous tool use.",
|
|
1204
|
+
fix: "Remove zero-width/bidirectional/tag characters and HTML comments from tool names, descriptions, schemas, and server instructions; keep model-visible text identical to what a human reviewer sees.",
|
|
1205
|
+
falsePositiveNote: "Some hidden characters are benign (e.g. a BOM, or a bidi control in a legitimate right-to-left language sample, or an HTML comment in documentation). This flags that the model-visible text differs from the rendered text; review the surface in context."
|
|
1206
|
+
}
|
|
1207
|
+
];
|
|
1208
|
+
}
|
|
1209
|
+
|
|
1012
1210
|
// ../../packages/static-analyzer/src/analyzeServerConfig.ts
|
|
1013
1211
|
var DETECTORS = [
|
|
1014
1212
|
detectBroadFilesystemPath,
|
|
@@ -1018,7 +1216,9 @@ var DETECTORS = [
|
|
|
1018
1216
|
detectUnpinnedPackage,
|
|
1019
1217
|
detectUnknownRemote,
|
|
1020
1218
|
detectExternalMutation,
|
|
1021
|
-
detectFinancialAction
|
|
1219
|
+
detectFinancialAction,
|
|
1220
|
+
detectUnverifiedLocalSource,
|
|
1221
|
+
detectHiddenInstructions
|
|
1022
1222
|
];
|
|
1023
1223
|
function analyzeServerConfig(server) {
|
|
1024
1224
|
const binding = resolveRuntimeBinding(server);
|
|
@@ -1030,6 +1230,41 @@ function analyzeServerConfig(server) {
|
|
|
1030
1230
|
return findings;
|
|
1031
1231
|
}
|
|
1032
1232
|
|
|
1233
|
+
// ../../packages/static-analyzer/src/documentSurface.ts
|
|
1234
|
+
function analyzeDocumentSurfaces(surfaces) {
|
|
1235
|
+
const evidence = [];
|
|
1236
|
+
for (const surface of surfaces) {
|
|
1237
|
+
for (const pattern of findPoisonPhrases(surface.text)) {
|
|
1238
|
+
evidence.push(
|
|
1239
|
+
poisonEvidence(pattern, { type: "source", path: surface.path, key: surface.kind })
|
|
1240
|
+
);
|
|
1241
|
+
}
|
|
1242
|
+
for (const category of findHiddenContent(surface.text)) {
|
|
1243
|
+
evidence.push(
|
|
1244
|
+
hiddenEvidence(category, { type: "source", path: surface.path, key: surface.kind })
|
|
1245
|
+
);
|
|
1246
|
+
}
|
|
1247
|
+
}
|
|
1248
|
+
if (evidence.length === 0) return [];
|
|
1249
|
+
return [
|
|
1250
|
+
{
|
|
1251
|
+
id: "prompt.surface-instructions",
|
|
1252
|
+
title: "Model-directed or hidden content in a project document",
|
|
1253
|
+
severity: "medium",
|
|
1254
|
+
blocker: false,
|
|
1255
|
+
symbol: "PROMPT",
|
|
1256
|
+
riskClass: "S2",
|
|
1257
|
+
mode: "OBSERVED",
|
|
1258
|
+
confidence: "medium",
|
|
1259
|
+
detectionMethod: "source-text",
|
|
1260
|
+
evidence,
|
|
1261
|
+
impact: "A project document (README / SKILL.md / AGENTS.md / package description) contains model-directed instructions or hidden/obfuscated content. An agent that reads project docs alongside the tool could be steered by text a human reviewer skims past.",
|
|
1262
|
+
fix: "Remove model-directed instructions and hidden/obfuscated characters from project documents; keep their visible text equal to their intent.",
|
|
1263
|
+
falsePositiveNote: "Documentation legitimately discusses prompts, tool ordering, or includes HTML comments and non-Latin scripts. This flags prompt-surface shape in project docs, not a proven injection; review the cited file in context."
|
|
1264
|
+
}
|
|
1265
|
+
];
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1033
1268
|
// ../../packages/risk-engine/src/computeRiskClass.ts
|
|
1034
1269
|
function computeRiskClass(findings, binding) {
|
|
1035
1270
|
const classes = findings.map((f) => f.riskClass);
|
|
@@ -1387,6 +1622,126 @@ function normalizeMcpServers(root, sourceConfigPath) {
|
|
|
1387
1622
|
|
|
1388
1623
|
// ../../packages/config-parser/src/parseConfig.ts
|
|
1389
1624
|
import { basename } from "node:path";
|
|
1625
|
+
|
|
1626
|
+
// ../../packages/config-parser/src/positionIndex.ts
|
|
1627
|
+
function peek(c) {
|
|
1628
|
+
return c.text[c.i] ?? "";
|
|
1629
|
+
}
|
|
1630
|
+
function advance(c) {
|
|
1631
|
+
const ch = c.text[c.i] ?? "";
|
|
1632
|
+
c.i++;
|
|
1633
|
+
if (ch === "\n") {
|
|
1634
|
+
c.line++;
|
|
1635
|
+
c.col = 1;
|
|
1636
|
+
} else {
|
|
1637
|
+
c.col++;
|
|
1638
|
+
}
|
|
1639
|
+
return ch;
|
|
1640
|
+
}
|
|
1641
|
+
function skipWhitespace(c) {
|
|
1642
|
+
while (c.i < c.text.length) {
|
|
1643
|
+
const ch = peek(c);
|
|
1644
|
+
if (ch === " " || ch === " " || ch === "\n" || ch === "\r") advance(c);
|
|
1645
|
+
else break;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
function readString(c) {
|
|
1649
|
+
advance(c);
|
|
1650
|
+
let out = "";
|
|
1651
|
+
while (c.i < c.text.length) {
|
|
1652
|
+
const ch = advance(c);
|
|
1653
|
+
if (ch === "\\") {
|
|
1654
|
+
const esc2 = advance(c);
|
|
1655
|
+
if (esc2 === "n") out += "\n";
|
|
1656
|
+
else if (esc2 === "t") out += " ";
|
|
1657
|
+
else if (esc2 === "r") out += "\r";
|
|
1658
|
+
else if (esc2 === "u") {
|
|
1659
|
+
let hex = "";
|
|
1660
|
+
for (let k = 0; k < 4; k++) hex += advance(c);
|
|
1661
|
+
const code = Number.parseInt(hex, 16);
|
|
1662
|
+
out += Number.isNaN(code) ? "" : String.fromCharCode(code);
|
|
1663
|
+
} else out += esc2;
|
|
1664
|
+
} else if (ch === '"') {
|
|
1665
|
+
break;
|
|
1666
|
+
} else {
|
|
1667
|
+
out += ch;
|
|
1668
|
+
}
|
|
1669
|
+
}
|
|
1670
|
+
return out;
|
|
1671
|
+
}
|
|
1672
|
+
function skipValue(c, index, pathPrefix) {
|
|
1673
|
+
skipWhitespace(c);
|
|
1674
|
+
const ch = peek(c);
|
|
1675
|
+
if (ch === "{") {
|
|
1676
|
+
scanObject(c, index, pathPrefix);
|
|
1677
|
+
} else if (ch === "[") {
|
|
1678
|
+
scanArray(c, index, pathPrefix);
|
|
1679
|
+
} else if (ch === '"') {
|
|
1680
|
+
readString(c);
|
|
1681
|
+
} else {
|
|
1682
|
+
while (c.i < c.text.length) {
|
|
1683
|
+
const n = peek(c);
|
|
1684
|
+
if (n === "," || n === "}" || n === "]" || n === " " || n === " " || n === "\n" || n === "\r") break;
|
|
1685
|
+
advance(c);
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
function scanArray(c, index, pathPrefix) {
|
|
1690
|
+
advance(c);
|
|
1691
|
+
for (; ; ) {
|
|
1692
|
+
skipWhitespace(c);
|
|
1693
|
+
const ch = peek(c);
|
|
1694
|
+
if (ch === "]" || ch === "") {
|
|
1695
|
+
if (ch === "]") advance(c);
|
|
1696
|
+
return;
|
|
1697
|
+
}
|
|
1698
|
+
if (ch === ",") {
|
|
1699
|
+
advance(c);
|
|
1700
|
+
continue;
|
|
1701
|
+
}
|
|
1702
|
+
skipValue(c, index, pathPrefix);
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
function scanObject(c, index, pathPrefix) {
|
|
1706
|
+
advance(c);
|
|
1707
|
+
for (; ; ) {
|
|
1708
|
+
skipWhitespace(c);
|
|
1709
|
+
const ch = peek(c);
|
|
1710
|
+
if (ch === "}" || ch === "") {
|
|
1711
|
+
if (ch === "}") advance(c);
|
|
1712
|
+
return;
|
|
1713
|
+
}
|
|
1714
|
+
if (ch === ",") {
|
|
1715
|
+
advance(c);
|
|
1716
|
+
continue;
|
|
1717
|
+
}
|
|
1718
|
+
if (ch !== '"') {
|
|
1719
|
+
advance(c);
|
|
1720
|
+
continue;
|
|
1721
|
+
}
|
|
1722
|
+
const keyLine = c.line;
|
|
1723
|
+
const keyCol = c.col;
|
|
1724
|
+
const key = readString(c);
|
|
1725
|
+
const path = pathPrefix ? `${pathPrefix}.${key}` : key;
|
|
1726
|
+
if (!(path in index)) index[path] = { line: keyLine, column: keyCol };
|
|
1727
|
+
skipWhitespace(c);
|
|
1728
|
+
if (peek(c) === ":") advance(c);
|
|
1729
|
+
skipValue(c, index, path);
|
|
1730
|
+
}
|
|
1731
|
+
}
|
|
1732
|
+
function buildPositionIndex(text) {
|
|
1733
|
+
const index = {};
|
|
1734
|
+
const c = { text, i: 0, line: 1, col: 1 };
|
|
1735
|
+
try {
|
|
1736
|
+
skipWhitespace(c);
|
|
1737
|
+
if (peek(c) === "{") scanObject(c, index, "");
|
|
1738
|
+
else if (peek(c) === "[") scanArray(c, index, "");
|
|
1739
|
+
} catch {
|
|
1740
|
+
}
|
|
1741
|
+
return index;
|
|
1742
|
+
}
|
|
1743
|
+
|
|
1744
|
+
// ../../packages/config-parser/src/parseConfig.ts
|
|
1390
1745
|
function kindForPath(path) {
|
|
1391
1746
|
const base = basename(path).toLowerCase();
|
|
1392
1747
|
if (base.includes("settings")) return "claude-settings";
|
|
@@ -1399,10 +1754,84 @@ function parseConfigText(text, configPath = "<inline>") {
|
|
|
1399
1754
|
configPath,
|
|
1400
1755
|
kind: configPath === "<inline>" ? "inline" : kindForPath(configPath),
|
|
1401
1756
|
servers: normalizeMcpServers(root, configPath),
|
|
1402
|
-
root
|
|
1757
|
+
root,
|
|
1758
|
+
positions: buildPositionIndex(text)
|
|
1759
|
+
};
|
|
1760
|
+
}
|
|
1761
|
+
|
|
1762
|
+
// ../../packages/core/src/scanSurfaces.ts
|
|
1763
|
+
function scanDocumentSurfaces(surfaces, configPath, generatedAt) {
|
|
1764
|
+
if (surfaces.length === 0) return void 0;
|
|
1765
|
+
const findings = analyzeDocumentSurfaces(surfaces);
|
|
1766
|
+
if (findings.length === 0) return void 0;
|
|
1767
|
+
const truncated = surfaces.some((s) => s.truncated);
|
|
1768
|
+
return {
|
|
1769
|
+
schemaVersion: "calllint.report.v0",
|
|
1770
|
+
reportKind: "single-target",
|
|
1771
|
+
target: {
|
|
1772
|
+
name: "project-docs",
|
|
1773
|
+
kind: "project-docs",
|
|
1774
|
+
configPath
|
|
1775
|
+
},
|
|
1776
|
+
verdict: "REVIEW",
|
|
1777
|
+
publicVerdictLabel: VERDICT_PUBLIC_LABEL.REVIEW,
|
|
1778
|
+
riskClass: "S2",
|
|
1779
|
+
symbols: ["PROMPT"],
|
|
1780
|
+
confidence: "medium",
|
|
1781
|
+
reproducibility: {
|
|
1782
|
+
level: "HIGH",
|
|
1783
|
+
reasons: ["Static scan of local documents; deterministic given the same files."]
|
|
1784
|
+
},
|
|
1785
|
+
summary: "Project documents contain model-directed or hidden prompt-surface content; review the cited files.",
|
|
1786
|
+
observed: findings,
|
|
1787
|
+
inferred: [],
|
|
1788
|
+
findings,
|
|
1789
|
+
topFindings: findings,
|
|
1790
|
+
policy: {
|
|
1791
|
+
autonomousUse: "warn",
|
|
1792
|
+
manualApproval: "recommended",
|
|
1793
|
+
sandbox: "recommended"
|
|
1794
|
+
},
|
|
1795
|
+
fingerprints: {
|
|
1796
|
+
configHash: "",
|
|
1797
|
+
targetSpecHash: "",
|
|
1798
|
+
riskSurfaceHash: ""
|
|
1799
|
+
},
|
|
1800
|
+
diagnostics: truncated ? [
|
|
1801
|
+
{
|
|
1802
|
+
level: "info",
|
|
1803
|
+
code: "surface.truncated",
|
|
1804
|
+
message: "One or more project documents exceeded the scan size cap and were truncated."
|
|
1805
|
+
}
|
|
1806
|
+
] : [],
|
|
1807
|
+
generatedAt
|
|
1403
1808
|
};
|
|
1404
1809
|
}
|
|
1405
1810
|
|
|
1811
|
+
// ../../packages/core/src/enrichPositions.ts
|
|
1812
|
+
function enrichEvidencePositions(reports, positions) {
|
|
1813
|
+
if (Object.keys(positions).length === 0) return;
|
|
1814
|
+
const prefixes = ["mcpServers", "servers", ""];
|
|
1815
|
+
for (const report of reports) {
|
|
1816
|
+
const server = report.target.name;
|
|
1817
|
+
for (const finding of report.findings) {
|
|
1818
|
+
for (const ev of finding.evidence) {
|
|
1819
|
+
if (ev.line !== void 0) continue;
|
|
1820
|
+
if (!ev.key) continue;
|
|
1821
|
+
for (const prefix of prefixes) {
|
|
1822
|
+
const path = prefix ? `${prefix}.${server}.${ev.key}` : `${server}.${ev.key}`;
|
|
1823
|
+
const pos = positions[path];
|
|
1824
|
+
if (pos) {
|
|
1825
|
+
ev.line = pos.line;
|
|
1826
|
+
ev.column = pos.column;
|
|
1827
|
+
break;
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
|
|
1406
1835
|
// ../../packages/core/src/scanConfig.ts
|
|
1407
1836
|
function aggregate(configPath, reports, generatedAt) {
|
|
1408
1837
|
const counts = { SAFE: 0, REVIEW: 0, BLOCK: 0, UNKNOWN: 0 };
|
|
@@ -1421,10 +1850,13 @@ function aggregate(configPath, reports, generatedAt) {
|
|
|
1421
1850
|
};
|
|
1422
1851
|
}
|
|
1423
1852
|
function scanParsed(parsed, opts) {
|
|
1424
|
-
const { generatedAt } = resolveScanOptions(opts);
|
|
1853
|
+
const { generatedAt, surfaces } = resolveScanOptions(opts);
|
|
1425
1854
|
const reports = parsed.servers.map(
|
|
1426
1855
|
(server) => scanServer({ server, targetKind: parsed.kind }, opts)
|
|
1427
1856
|
);
|
|
1857
|
+
enrichEvidencePositions(reports, parsed.positions);
|
|
1858
|
+
const surfaceReport = scanDocumentSurfaces(surfaces, parsed.configPath, generatedAt);
|
|
1859
|
+
if (surfaceReport) reports.push(surfaceReport);
|
|
1428
1860
|
return aggregate(parsed.configPath, reports, generatedAt);
|
|
1429
1861
|
}
|
|
1430
1862
|
function scanConfigText(text, configPath, opts) {
|
|
@@ -1872,6 +2304,60 @@ function renderSarif(summary) {
|
|
|
1872
2304
|
return JSON.stringify(sarif, null, 2);
|
|
1873
2305
|
}
|
|
1874
2306
|
|
|
2307
|
+
// ../../packages/report-renderer/src/renderDiagnostics.ts
|
|
2308
|
+
function verdictContributionFor(f) {
|
|
2309
|
+
if (f.blocker) return "blocker";
|
|
2310
|
+
return f.mode === "INFERRED" ? "inferred" : "review";
|
|
2311
|
+
}
|
|
2312
|
+
function keyPathFor(f) {
|
|
2313
|
+
const ev = f.evidence[0];
|
|
2314
|
+
if (!ev) return null;
|
|
2315
|
+
return ev.key ?? null;
|
|
2316
|
+
}
|
|
2317
|
+
function observedFor(f) {
|
|
2318
|
+
const ev = f.evidence[0];
|
|
2319
|
+
if (!ev) return null;
|
|
2320
|
+
return ev.value ?? ev.snippet ?? null;
|
|
2321
|
+
}
|
|
2322
|
+
function entryFromFinding(f, report, configFile) {
|
|
2323
|
+
const ev = f.evidence[0];
|
|
2324
|
+
return {
|
|
2325
|
+
ruleId: f.id,
|
|
2326
|
+
title: f.title,
|
|
2327
|
+
severity: f.severity,
|
|
2328
|
+
server: report.target.name,
|
|
2329
|
+
file: ev?.path ?? configFile,
|
|
2330
|
+
keyPath: keyPathFor(f),
|
|
2331
|
+
// Populated by the post-hoc position enrichment (config-parser position
|
|
2332
|
+
// index → core), when the evidence's config key maps to a source location;
|
|
2333
|
+
// null for evidence with no locatable source key (e.g. binding-derived).
|
|
2334
|
+
line: ev?.line ?? null,
|
|
2335
|
+
column: ev?.column ?? null,
|
|
2336
|
+
observed: observedFor(f),
|
|
2337
|
+
remediation: f.fix,
|
|
2338
|
+
mode: f.mode,
|
|
2339
|
+
confidence: f.confidence,
|
|
2340
|
+
verdictContribution: verdictContributionFor(f)
|
|
2341
|
+
};
|
|
2342
|
+
}
|
|
2343
|
+
function renderDiagnostics(summary) {
|
|
2344
|
+
const diagnostics = [];
|
|
2345
|
+
for (const report of summary.reports) {
|
|
2346
|
+
for (const f of report.findings) {
|
|
2347
|
+
diagnostics.push(entryFromFinding(f, report, summary.configPath));
|
|
2348
|
+
}
|
|
2349
|
+
}
|
|
2350
|
+
const out = {
|
|
2351
|
+
schemaVersion: "calllint.diagnostics.v0",
|
|
2352
|
+
verdict: summary.verdict,
|
|
2353
|
+
publicVerdictLabel: summary.publicVerdictLabel,
|
|
2354
|
+
file: summary.configPath,
|
|
2355
|
+
diagnostics,
|
|
2356
|
+
generatedAt: summary.generatedAt
|
|
2357
|
+
};
|
|
2358
|
+
return JSON.stringify(out, null, 2);
|
|
2359
|
+
}
|
|
2360
|
+
|
|
1875
2361
|
// ../../packages/report-renderer/src/logoBase64.ts
|
|
1876
2362
|
var LOGO_REPORT_BASE64 = "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";
|
|
1877
2363
|
|
|
@@ -2066,6 +2552,55 @@ function resolveConfigInput(args, deps) {
|
|
|
2066
2552
|
return { text: readFileSync3(resolved, "utf8"), configPath: resolved };
|
|
2067
2553
|
}
|
|
2068
2554
|
|
|
2555
|
+
// src/commands/surfaces.ts
|
|
2556
|
+
import { existsSync as existsSync3, readFileSync as readFileSync4, statSync } from "node:fs";
|
|
2557
|
+
import { join as join3 } from "node:path";
|
|
2558
|
+
var SURFACE_SIZE_CAP = 256 * 1024;
|
|
2559
|
+
var SURFACE_FILES = [
|
|
2560
|
+
{ file: "README.md", kind: "readme" },
|
|
2561
|
+
{ file: "SKILL.md", kind: "skill" },
|
|
2562
|
+
{ file: "AGENTS.md", kind: "agents" }
|
|
2563
|
+
];
|
|
2564
|
+
function readCapped(path) {
|
|
2565
|
+
try {
|
|
2566
|
+
const st = statSync(path);
|
|
2567
|
+
if (!st.isFile()) return void 0;
|
|
2568
|
+
const raw = readFileSync4(path, "utf8");
|
|
2569
|
+
if (raw.length > SURFACE_SIZE_CAP) {
|
|
2570
|
+
return { text: raw.slice(0, SURFACE_SIZE_CAP), truncated: true };
|
|
2571
|
+
}
|
|
2572
|
+
return { text: raw, truncated: false };
|
|
2573
|
+
} catch {
|
|
2574
|
+
return void 0;
|
|
2575
|
+
}
|
|
2576
|
+
}
|
|
2577
|
+
function readDocumentSurfaces(dir) {
|
|
2578
|
+
const surfaces = [];
|
|
2579
|
+
if (!existsSync3(dir)) return surfaces;
|
|
2580
|
+
for (const { file, kind } of SURFACE_FILES) {
|
|
2581
|
+
const got = readCapped(join3(dir, file));
|
|
2582
|
+
if (got) surfaces.push({ path: file, kind, text: got.text, truncated: got.truncated });
|
|
2583
|
+
}
|
|
2584
|
+
const pkgPath = join3(dir, "package.json");
|
|
2585
|
+
const pkgRaw = readCapped(pkgPath);
|
|
2586
|
+
if (pkgRaw) {
|
|
2587
|
+
try {
|
|
2588
|
+
const pkg = JSON.parse(pkgRaw.text);
|
|
2589
|
+
const description = pkg && typeof pkg === "object" && !Array.isArray(pkg) ? pkg.description : void 0;
|
|
2590
|
+
if (typeof description === "string") {
|
|
2591
|
+
surfaces.push({
|
|
2592
|
+
path: "package.json",
|
|
2593
|
+
kind: "package-description",
|
|
2594
|
+
text: description,
|
|
2595
|
+
truncated: pkgRaw.truncated
|
|
2596
|
+
});
|
|
2597
|
+
}
|
|
2598
|
+
} catch {
|
|
2599
|
+
}
|
|
2600
|
+
}
|
|
2601
|
+
return surfaces;
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2069
2604
|
// src/commands/scan.ts
|
|
2070
2605
|
function scanCommand(args, deps) {
|
|
2071
2606
|
const policyPath = flagStr(args.flags, "policy");
|
|
@@ -2084,13 +2619,16 @@ function scanCommand(args, deps) {
|
|
|
2084
2619
|
return { stdout: "", stderr: input.error, exitCode: input.exitCode };
|
|
2085
2620
|
}
|
|
2086
2621
|
const { text, configPath } = input;
|
|
2622
|
+
const surfaceDir = flagStr(args.flags, "surface-dir");
|
|
2623
|
+
const surfaces = surfaceDir ? readDocumentSurfaces(resolve(deps.cwd, surfaceDir)) : void 0;
|
|
2087
2624
|
let summary;
|
|
2088
2625
|
try {
|
|
2089
2626
|
summary = scanConfigText(text, configPath, {
|
|
2090
2627
|
policy,
|
|
2091
2628
|
now: deps.now,
|
|
2092
2629
|
generatedAt: deps.generatedAt,
|
|
2093
|
-
extraFindings: deps.online?.extraFindings
|
|
2630
|
+
extraFindings: deps.online?.extraFindings,
|
|
2631
|
+
surfaces
|
|
2094
2632
|
});
|
|
2095
2633
|
} catch (err) {
|
|
2096
2634
|
if (err instanceof ConfigParseError) {
|
|
@@ -2104,7 +2642,7 @@ function scanCommand(args, deps) {
|
|
|
2104
2642
|
}
|
|
2105
2643
|
if (deps.writeCacheFile !== false) {
|
|
2106
2644
|
try {
|
|
2107
|
-
writeCache(summary,
|
|
2645
|
+
writeCache(summary, join4(deps.cwd, ".calllint", "last-scan.json"));
|
|
2108
2646
|
} catch {
|
|
2109
2647
|
}
|
|
2110
2648
|
}
|
|
@@ -2119,8 +2657,55 @@ function scanCommand(args, deps) {
|
|
|
2119
2657
|
return { stdout, exitCode };
|
|
2120
2658
|
}
|
|
2121
2659
|
|
|
2660
|
+
// src/commands/diagnostics.ts
|
|
2661
|
+
function diagnosticsCommand(args, deps) {
|
|
2662
|
+
if (!flagBool(args.flags, "json")) {
|
|
2663
|
+
return {
|
|
2664
|
+
stdout: "",
|
|
2665
|
+
stderr: "diagnostics v0 emits JSON only \u2014 pass --json (calllint.diagnostics.v0).",
|
|
2666
|
+
exitCode: EXIT.USAGE
|
|
2667
|
+
};
|
|
2668
|
+
}
|
|
2669
|
+
let policy;
|
|
2670
|
+
try {
|
|
2671
|
+
policy = loadPolicyOrDefault(flagStr(args.flags, "policy"));
|
|
2672
|
+
} catch (err) {
|
|
2673
|
+
return {
|
|
2674
|
+
stdout: "",
|
|
2675
|
+
stderr: err instanceof Error ? err.message : String(err),
|
|
2676
|
+
exitCode: EXIT.ERROR
|
|
2677
|
+
};
|
|
2678
|
+
}
|
|
2679
|
+
const input = deps.online?.inputOverride ?? resolveConfigInput(args, deps);
|
|
2680
|
+
if (isInputError(input)) {
|
|
2681
|
+
return { stdout: "", stderr: input.error, exitCode: input.exitCode };
|
|
2682
|
+
}
|
|
2683
|
+
const { text, configPath } = input;
|
|
2684
|
+
let summary;
|
|
2685
|
+
try {
|
|
2686
|
+
summary = scanConfigText(text, configPath, {
|
|
2687
|
+
policy,
|
|
2688
|
+
now: deps.now,
|
|
2689
|
+
generatedAt: deps.generatedAt,
|
|
2690
|
+
extraFindings: deps.online?.extraFindings
|
|
2691
|
+
});
|
|
2692
|
+
} catch (err) {
|
|
2693
|
+
if (err instanceof ConfigParseError) {
|
|
2694
|
+
return {
|
|
2695
|
+
stdout: "",
|
|
2696
|
+
stderr: `Parse error in ${configPath}: ${err.message}`,
|
|
2697
|
+
exitCode: EXIT.ERROR
|
|
2698
|
+
};
|
|
2699
|
+
}
|
|
2700
|
+
throw err;
|
|
2701
|
+
}
|
|
2702
|
+
const stdout = renderDiagnostics(summary);
|
|
2703
|
+
const exitCode = flagBool(args.flags, "ci") ? exitCodeFor(summary, policy) : EXIT.OK;
|
|
2704
|
+
return { stdout, exitCode };
|
|
2705
|
+
}
|
|
2706
|
+
|
|
2122
2707
|
// src/commands/explain.ts
|
|
2123
|
-
import { join as
|
|
2708
|
+
import { join as join5 } from "node:path";
|
|
2124
2709
|
function explainCommand(args, deps) {
|
|
2125
2710
|
const serverName = args.positionals[0];
|
|
2126
2711
|
if (!serverName) {
|
|
@@ -2130,7 +2715,7 @@ function explainCommand(args, deps) {
|
|
|
2130
2715
|
exitCode: EXIT.USAGE
|
|
2131
2716
|
};
|
|
2132
2717
|
}
|
|
2133
|
-
const summary = readCache(
|
|
2718
|
+
const summary = readCache(join5(deps.cwd, ".calllint", "last-scan.json"));
|
|
2134
2719
|
if (!summary) {
|
|
2135
2720
|
return {
|
|
2136
2721
|
stdout: "",
|
|
@@ -2152,13 +2737,13 @@ function explainCommand(args, deps) {
|
|
|
2152
2737
|
}
|
|
2153
2738
|
|
|
2154
2739
|
// src/commands/policy.ts
|
|
2155
|
-
import { existsSync as
|
|
2156
|
-
import { join as
|
|
2740
|
+
import { existsSync as existsSync4, writeFileSync as writeFileSync2 } from "node:fs";
|
|
2741
|
+
import { join as join6 } from "node:path";
|
|
2157
2742
|
function policyCommand(args, deps) {
|
|
2158
2743
|
const sub = args.positionals[0];
|
|
2159
2744
|
if (sub === "init") {
|
|
2160
|
-
const path =
|
|
2161
|
-
if (
|
|
2745
|
+
const path = join6(deps.cwd, "calllint.policy.json");
|
|
2746
|
+
if (existsSync4(path) && !flagBool(args.flags, "force")) {
|
|
2162
2747
|
return {
|
|
2163
2748
|
stdout: "",
|
|
2164
2749
|
stderr: `${path} already exists. Use --force to overwrite.`,
|
|
@@ -2188,7 +2773,7 @@ function policyCommand(args, deps) {
|
|
|
2188
2773
|
}
|
|
2189
2774
|
|
|
2190
2775
|
// src/commands/verify.ts
|
|
2191
|
-
import { join as
|
|
2776
|
+
import { join as join7 } from "node:path";
|
|
2192
2777
|
function loadPolicy(args) {
|
|
2193
2778
|
try {
|
|
2194
2779
|
return loadPolicyOrDefault(flagStr(args.flags, "policy"));
|
|
@@ -2197,7 +2782,7 @@ function loadPolicy(args) {
|
|
|
2197
2782
|
}
|
|
2198
2783
|
}
|
|
2199
2784
|
function baselinePathFor(args, cwd) {
|
|
2200
|
-
return flagStr(args.flags, "baseline") ??
|
|
2785
|
+
return flagStr(args.flags, "baseline") ?? join7(cwd, ".calllint", "baseline.json");
|
|
2201
2786
|
}
|
|
2202
2787
|
function baselineCommand(args, deps) {
|
|
2203
2788
|
const policy = loadPolicy(args);
|
|
@@ -2285,6 +2870,14 @@ function run(argv, deps) {
|
|
|
2285
2870
|
writeCacheFile: deps.writeCacheFile,
|
|
2286
2871
|
online: deps.online
|
|
2287
2872
|
});
|
|
2873
|
+
case "diagnostics":
|
|
2874
|
+
return diagnosticsCommand(args, {
|
|
2875
|
+
cwd: deps.cwd,
|
|
2876
|
+
readStdin: deps.readStdin,
|
|
2877
|
+
now: deps.now,
|
|
2878
|
+
generatedAt: deps.generatedAt,
|
|
2879
|
+
online: deps.online
|
|
2880
|
+
});
|
|
2288
2881
|
case "baseline":
|
|
2289
2882
|
return baselineCommand(args, {
|
|
2290
2883
|
cwd: deps.cwd,
|
|
@@ -2548,7 +3141,7 @@ async function breathe(argv, deps = {}) {
|
|
|
2548
3141
|
// src/index.ts
|
|
2549
3142
|
function readStdin() {
|
|
2550
3143
|
try {
|
|
2551
|
-
return
|
|
3144
|
+
return readFileSync5(0, "utf8");
|
|
2552
3145
|
} catch {
|
|
2553
3146
|
return "";
|
|
2554
3147
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "calllint",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Evidence-backed security verdicts for MCP servers and agent tools. Lint agent tool-call risk before tools run — SAFE / REVIEW / BLOCK / UNKNOWN, with evidence. Never executes the server it judges.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|