calllint 0.6.0 → 0.8.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.
Files changed (2) hide show
  1. package/dist/index.js +331 -9
  2. package/package.json +1 -1
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 readFileSync8 } from "node:fs";
4
+ import { readFileSync as readFileSync10 } from "node:fs";
5
5
  import { execFileSync } from "node:child_process";
6
6
 
7
7
  // src/args.ts
@@ -67,6 +67,7 @@ COMMANDS
67
67
  diagnostics [target] Emit editor/agent-host diagnostics JSON (calllint.diagnostics.v0)
68
68
  baseline [target] Record the approved risk surface as a baseline
69
69
  approve Record the repo-wide capability surface as approved state (L4)
70
+ receipt verify <f> Structurally validate a local calllint.receipt.v0 file
70
71
  gen-rule --host <h> Emit the CallLint agent-safety rule for a host (CLAUDE.md, etc.)
71
72
  policy init Write a default calllint.policy.json
72
73
  policy explain Show the effective policy
@@ -91,6 +92,9 @@ SCAN OPTIONS
91
92
  --sarif Emit SARIF 2.1.0 (GitHub Code Scanning / CI)
92
93
  --markdown Emit Markdown for PR comments / GitHub Step Summary
93
94
  --html Emit a self-contained HTML report
95
+ --badge Emit a shields.io endpoint badge JSON (SAFE/REVIEW/UNKNOWN/BLOCK)
96
+ --receipt Also write a local calllint.receipt.v0 (offline reporting layer)
97
+ --receipt-out <f> Receipt output path (default: calllint-receipt.json)
94
98
  --policy <file> Use a policy file (default: built-in defaults)
95
99
  --stdin Read config JSON from stdin
96
100
  --ci Exit non-zero per policy (BLOCK=30, UNKNOWN=20, REVIEW=10 if enabled)
@@ -110,6 +114,8 @@ EXAMPLES
110
114
  calllint scan-all --no-emoji
111
115
  calllint check ./mcp.json --json
112
116
  calllint scan .cursor/mcp.json --markdown
117
+ calllint scan .cursor/mcp.json --badge > calllint-badge.json
118
+ calllint scan .cursor/mcp.json --receipt && calllint receipt verify calllint-receipt.json
113
119
  calllint verify ./mcp.json --ci
114
120
  calllint approve && calllint verify --approved --ci
115
121
  calllint explain filesystem
@@ -767,6 +773,25 @@ function looksSecret(key) {
767
773
  const upper = key.toUpperCase();
768
774
  return SECRET_HINTS.some((h) => upper.includes(h));
769
775
  }
776
+ function extractDockerEnvKeys(args) {
777
+ const keys = [];
778
+ for (let i = 0; i < args.length; i += 1) {
779
+ const arg = args[i];
780
+ let spec;
781
+ if (arg === "-e" || arg === "--env") {
782
+ spec = args[i + 1];
783
+ i += 1;
784
+ } else if (arg.startsWith("-e=")) {
785
+ spec = arg.slice("-e=".length);
786
+ } else if (arg.startsWith("--env=")) {
787
+ spec = arg.slice("--env=".length);
788
+ }
789
+ if (spec === void 0) continue;
790
+ const eq = spec.indexOf("=");
791
+ keys.push(eq === -1 ? spec : spec.slice(0, eq));
792
+ }
793
+ return keys;
794
+ }
770
795
  function detectSecretEnvKeys(ctx) {
771
796
  const { server } = ctx;
772
797
  const evidence = [];
@@ -781,6 +806,21 @@ function detectSecretEnvKeys(ctx) {
781
806
  });
782
807
  }
783
808
  }
809
+ if ((server.command ?? "").toLowerCase() === "docker") {
810
+ const already = new Set(server.envKeys);
811
+ for (const key of extractDockerEnvKeys(server.args)) {
812
+ if (already.has(key)) continue;
813
+ already.add(key);
814
+ if (looksSecret(key)) {
815
+ evidence.push({
816
+ type: "config",
817
+ path: server.sourceConfigPath,
818
+ key: "args",
819
+ value: key
820
+ });
821
+ }
822
+ }
823
+ }
784
824
  if (evidence.length === 0) return [];
785
825
  return [
786
826
  {
@@ -2942,6 +2982,147 @@ ${rule()}
2942
2982
  }
2943
2983
  }
2944
2984
 
2985
+ // ../../packages/core/src/receipt/createReceipt.ts
2986
+ import { randomBytes } from "node:crypto";
2987
+ function newReceiptId() {
2988
+ const b64 = randomBytes(16).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
2989
+ return `clrec_${b64}`;
2990
+ }
2991
+ function riskCounts(counts) {
2992
+ return {
2993
+ safe: counts.SAFE ?? 0,
2994
+ review: counts.REVIEW ?? 0,
2995
+ block: counts.BLOCK ?? 0,
2996
+ unknown: counts.UNKNOWN ?? 0
2997
+ };
2998
+ }
2999
+ function findingRefs(reports) {
3000
+ const refs = [];
3001
+ for (const report of reports) {
3002
+ for (const f of report.findings ?? []) {
3003
+ const path = f.evidence?.find((e) => typeof e.path === "string")?.path;
3004
+ refs.push({
3005
+ rule_id: f.id,
3006
+ severity: f.severity,
3007
+ ...path ? { evidence_path: path } : {}
3008
+ });
3009
+ }
3010
+ }
3011
+ return refs;
3012
+ }
3013
+ function createReceipt(input, now) {
3014
+ const summary = input.scanReport;
3015
+ const receipt = {
3016
+ schema_version: "calllint.receipt.v0",
3017
+ receipt_id: newReceiptId(),
3018
+ created_at: now,
3019
+ tool: { name: "calllint", version: input.toolVersion },
3020
+ subject: input.subject,
3021
+ verdict: summary.verdict,
3022
+ hashes: {
3023
+ input_hash: hashInput(input.inputForHash),
3024
+ policy_hash: hashJson(input.effectivePolicyForHash),
3025
+ report_hash: hashJson(input.scanReport),
3026
+ ruleset_hash: hashJson(input.rulesetForHash)
3027
+ },
3028
+ risk_counts: riskCounts(summary.counts),
3029
+ finding_refs: findingRefs(summary.reports ?? []),
3030
+ trust_boundaries: {
3031
+ executed_target: false,
3032
+ network_used: input.networkUsed === true,
3033
+ llm_in_verdict_path: false,
3034
+ secret_values_read: false
3035
+ },
3036
+ ...input.corpus ? { corpus: input.corpus } : {}
3037
+ };
3038
+ return receipt;
3039
+ }
3040
+ function hashInput(value) {
3041
+ return typeof value === "string" ? sha256(value) : hashJson(value);
3042
+ }
3043
+
3044
+ // ../../packages/core/src/receipt/verifyReceipt.ts
3045
+ var SHA256_RE = /^sha256:[0-9a-f]{64}$/;
3046
+ var RECEIPT_ID_RE = /^clrec_[A-Za-z0-9_-]+$/;
3047
+ var VERDICTS = /* @__PURE__ */ new Set(["SAFE", "REVIEW", "BLOCK", "UNKNOWN"]);
3048
+ var HASH_KEYS = ["input_hash", "policy_hash", "report_hash", "ruleset_hash"];
3049
+ var COUNT_KEYS = ["safe", "review", "block", "unknown"];
3050
+ function isObject(v) {
3051
+ return typeof v === "object" && v !== null && !Array.isArray(v);
3052
+ }
3053
+ function verifyReceipt(input) {
3054
+ const errors = [];
3055
+ const push = (msg) => errors.push(msg);
3056
+ if (!isObject(input)) {
3057
+ return { valid: false, errors: ["receipt is not a JSON object"], signed: false };
3058
+ }
3059
+ const r = input;
3060
+ if (r.schema_version !== "calllint.receipt.v0") {
3061
+ push(`schema_version must be "calllint.receipt.v0" (got ${JSON.stringify(r.schema_version)})`);
3062
+ }
3063
+ if (typeof r.receipt_id !== "string" || !RECEIPT_ID_RE.test(r.receipt_id)) {
3064
+ push("receipt_id must match /^clrec_[A-Za-z0-9_-]+$/");
3065
+ }
3066
+ if (typeof r.created_at !== "string" || Number.isNaN(Date.parse(r.created_at))) {
3067
+ push("created_at must be an ISO-8601 timestamp");
3068
+ }
3069
+ if (!isObject(r.tool) || r.tool.name !== "calllint" || typeof r.tool.version !== "string") {
3070
+ push('tool must be { name: "calllint", version: <string> }');
3071
+ }
3072
+ if (!isObject(r.subject) || r.subject.type !== "scan") {
3073
+ push('subject.type must be "scan"');
3074
+ }
3075
+ if (typeof r.verdict !== "string" || !VERDICTS.has(r.verdict)) {
3076
+ push("verdict must be one of SAFE | REVIEW | BLOCK | UNKNOWN");
3077
+ }
3078
+ if (!isObject(r.hashes)) {
3079
+ push("hashes must be an object");
3080
+ } else {
3081
+ for (const key of HASH_KEYS) {
3082
+ const val = r.hashes[key];
3083
+ if (typeof val !== "string" || !SHA256_RE.test(val)) {
3084
+ push(`hashes.${key} must match sha256:<64 lowercase hex>`);
3085
+ }
3086
+ }
3087
+ }
3088
+ if (!isObject(r.risk_counts)) {
3089
+ push("risk_counts must be an object");
3090
+ } else {
3091
+ for (const key of COUNT_KEYS) {
3092
+ const val = r.risk_counts[key];
3093
+ if (typeof val !== "number" || !Number.isInteger(val) || val < 0) {
3094
+ push(`risk_counts.${key} must be an integer >= 0`);
3095
+ }
3096
+ }
3097
+ }
3098
+ if (!Array.isArray(r.finding_refs)) {
3099
+ push("finding_refs must be an array");
3100
+ } else {
3101
+ r.finding_refs.forEach((ref, i) => {
3102
+ if (!isObject(ref) || typeof ref.rule_id !== "string" || typeof ref.severity !== "string") {
3103
+ push(`finding_refs[${i}] must have string rule_id and severity`);
3104
+ }
3105
+ });
3106
+ }
3107
+ if (!isObject(r.trust_boundaries)) {
3108
+ push("trust_boundaries must be an object");
3109
+ } else {
3110
+ const tb = r.trust_boundaries;
3111
+ if (tb.executed_target !== false) push("trust_boundaries.executed_target must be false");
3112
+ if (tb.llm_in_verdict_path !== false) push("trust_boundaries.llm_in_verdict_path must be false");
3113
+ if (tb.secret_values_read !== false) push("trust_boundaries.secret_values_read must be false");
3114
+ if (typeof tb.network_used !== "boolean") push("trust_boundaries.network_used must be a boolean");
3115
+ }
3116
+ const signed = isObject(r.signature);
3117
+ if (signed) {
3118
+ const sig = r.signature;
3119
+ if (typeof sig.algorithm !== "string" || typeof sig.key_id !== "string" || typeof sig.value !== "string") {
3120
+ push("signature, when present, must have string algorithm, key_id, and value");
3121
+ }
3122
+ }
3123
+ return { valid: errors.length === 0, errors, signed };
3124
+ }
3125
+
2945
3126
  // ../../packages/report-renderer/src/style.ts
2946
3127
  var DEFAULT_STYLE = { emoji: true };
2947
3128
  var NO_EMOJI_STYLE = { emoji: false };
@@ -3361,6 +3542,32 @@ function mdCell(s) {
3361
3542
  return s.replace(/\|/g, "\\|").replace(/\n/g, " ");
3362
3543
  }
3363
3544
 
3545
+ // ../../packages/report-renderer/src/renderBadge.ts
3546
+ var BADGE_COLOR = {
3547
+ SAFE: "brightgreen",
3548
+ REVIEW: "yellow",
3549
+ UNKNOWN: "lightgrey",
3550
+ BLOCK: "red"
3551
+ };
3552
+ var BADGE_MESSAGE = {
3553
+ SAFE: "SAFE",
3554
+ REVIEW: "REVIEW",
3555
+ UNKNOWN: "UNKNOWN",
3556
+ BLOCK: "BLOCK"
3557
+ };
3558
+ function renderBadge(summary) {
3559
+ return JSON.stringify(badgeEndpoint(summary.verdict), null, 2);
3560
+ }
3561
+ function badgeEndpoint(verdict) {
3562
+ return {
3563
+ schemaVersion: 1,
3564
+ label: "CallLint",
3565
+ message: BADGE_MESSAGE[verdict],
3566
+ color: BADGE_COLOR[verdict],
3567
+ cacheSeconds: 3600
3568
+ };
3569
+ }
3570
+
3364
3571
  // ../../packages/report-renderer/src/renderDiagnostics.ts
3365
3572
  function verdictContributionFor(f) {
3366
3573
  if (f.blocker) return "blocker";
@@ -3676,7 +3883,7 @@ function readDocumentSurfaces(dir) {
3676
3883
  }
3677
3884
 
3678
3885
  // src/commands/scan.ts
3679
- import { readFileSync as readFileSync7 } from "node:fs";
3886
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "node:fs";
3680
3887
  function scanCommand(args, deps) {
3681
3888
  if (flagBool(args.flags, "changed")) {
3682
3889
  return scanChangedCommand(args, deps);
@@ -3699,7 +3906,7 @@ function scanCommand(args, deps) {
3699
3906
  const { text, configPath } = input;
3700
3907
  return scanOneConfig(text, configPath, policy, args, deps);
3701
3908
  }
3702
- function scanOneConfig(text, configPath, policy, args, deps) {
3909
+ function scanOneConfig(text, configPath, policy, args, deps, allowReceipt = true) {
3703
3910
  const surfaceDir = flagStr(args.flags, "surface-dir");
3704
3911
  const surfaces = surfaceDir ? readDocumentSurfaces(resolve2(deps.cwd, surfaceDir)) : void 0;
3705
3912
  let summary;
@@ -3728,14 +3935,44 @@ function scanOneConfig(text, configPath, policy, args, deps) {
3728
3935
  }
3729
3936
  }
3730
3937
  const stdout = renderSummary(summary, args);
3938
+ if (allowReceipt && flagBool(args.flags, "receipt")) {
3939
+ const err = writeReceiptFile(summary, text, configPath, policy, args, deps);
3940
+ if (err) return { stdout: "", stderr: err, exitCode: EXIT.ERROR };
3941
+ }
3731
3942
  const exitCode = flagBool(args.flags, "ci") ? exitCodeFor(summary, policy) : EXIT.OK;
3732
3943
  return { stdout, exitCode };
3733
3944
  }
3945
+ function writeReceiptFile(summary, text, configPath, policy, args, deps) {
3946
+ const toolVersion = deps.toolVersion ?? "0.0.0-dev";
3947
+ const receipt = createReceipt(
3948
+ {
3949
+ toolVersion,
3950
+ subject: { type: "scan", target: configPath },
3951
+ inputForHash: text,
3952
+ effectivePolicyForHash: policy ?? { policy: "default" },
3953
+ scanReport: summary,
3954
+ rulesetForHash: { tool: "calllint", version: toolVersion },
3955
+ networkUsed: flagBool(args.flags, "online")
3956
+ },
3957
+ deps.generatedAt
3958
+ );
3959
+ const outPath = resolve2(
3960
+ deps.cwd,
3961
+ flagStr(args.flags, "receipt-out") ?? "calllint-receipt.json"
3962
+ );
3963
+ try {
3964
+ writeFileSync3(outPath, JSON.stringify(receipt, null, 2) + "\n", "utf8");
3965
+ } catch (e) {
3966
+ return `Could not write receipt to ${outPath}: ${e instanceof Error ? e.message : String(e)}`;
3967
+ }
3968
+ return void 0;
3969
+ }
3734
3970
  function renderSummary(summary, args) {
3735
3971
  const style = flagBool(args.flags, "no-emoji") ? NO_EMOJI_STYLE : DEFAULT_STYLE;
3736
3972
  if (flagBool(args.flags, "json")) return renderJson(summary);
3737
3973
  if (flagBool(args.flags, "sarif")) return renderSarif(summary);
3738
3974
  if (flagBool(args.flags, "markdown")) return renderMarkdown(summary);
3975
+ if (flagBool(args.flags, "badge")) return renderBadge(summary);
3739
3976
  if (flagBool(args.flags, "html")) return renderHtml(summary);
3740
3977
  if (flagBool(args.flags, "compact")) return renderCompact(summary, style);
3741
3978
  return renderTerminal(summary, style);
@@ -3868,7 +4105,7 @@ function worstExit2(decisions) {
3868
4105
  }
3869
4106
 
3870
4107
  // src/commands/genRule.ts
3871
- import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "node:fs";
4108
+ import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync3 } from "node:fs";
3872
4109
  import { dirname as dirname3, join as join7, resolve as resolve3 } from "node:path";
3873
4110
  function isRuleHost(v) {
3874
4111
  return v !== void 0 && RULE_HOSTS.includes(v);
@@ -3915,7 +4152,7 @@ Run \`calllint gen-rule\` to list hosts.`,
3915
4152
  }
3916
4153
  function defaultWrite(path, content) {
3917
4154
  mkdirSync3(dirname3(path), { recursive: true });
3918
- writeFileSync3(path, content, "utf8");
4155
+ writeFileSync4(path, content, "utf8");
3919
4156
  }
3920
4157
 
3921
4158
  // src/commands/diagnostics.ts
@@ -3998,7 +4235,7 @@ function explainCommand(args, deps) {
3998
4235
  }
3999
4236
 
4000
4237
  // src/commands/policy.ts
4001
- import { existsSync as existsSync6, writeFileSync as writeFileSync4 } from "node:fs";
4238
+ import { existsSync as existsSync6, writeFileSync as writeFileSync5 } from "node:fs";
4002
4239
  import { join as join9 } from "node:path";
4003
4240
  function policyCommand(args, deps) {
4004
4241
  const sub = args.positionals[0];
@@ -4011,7 +4248,7 @@ function policyCommand(args, deps) {
4011
4248
  exitCode: EXIT.USAGE
4012
4249
  };
4013
4250
  }
4014
- writeFileSync4(path, defaultPolicyJson(), "utf8");
4251
+ writeFileSync5(path, defaultPolicyJson(), "utf8");
4015
4252
  return { stdout: `Wrote default policy to ${path}`, exitCode: EXIT.OK };
4016
4253
  }
4017
4254
  if (sub === "explain") {
@@ -4168,6 +4405,73 @@ function approveCommand(args, deps) {
4168
4405
  };
4169
4406
  }
4170
4407
 
4408
+ // src/commands/receipt.ts
4409
+ import { readFileSync as readFileSync8 } from "node:fs";
4410
+ import { resolve as resolve4 } from "node:path";
4411
+ function receiptCommand(args, deps) {
4412
+ const sub = args.positionals[0];
4413
+ if (sub !== "verify") {
4414
+ return {
4415
+ stdout: "",
4416
+ stderr: "Usage: calllint receipt verify <receipt.json>",
4417
+ exitCode: EXIT.USAGE
4418
+ };
4419
+ }
4420
+ const file = args.positionals[1];
4421
+ if (!file) {
4422
+ return {
4423
+ stdout: "",
4424
+ stderr: "Usage: calllint receipt verify <receipt.json>",
4425
+ exitCode: EXIT.USAGE
4426
+ };
4427
+ }
4428
+ let raw;
4429
+ try {
4430
+ raw = readFileSync8(resolve4(deps.cwd, file), "utf8");
4431
+ } catch (e) {
4432
+ return {
4433
+ stdout: "",
4434
+ stderr: `Could not read receipt ${file}: ${e instanceof Error ? e.message : String(e)}`,
4435
+ exitCode: EXIT.ERROR
4436
+ };
4437
+ }
4438
+ let parsed;
4439
+ try {
4440
+ parsed = JSON.parse(raw);
4441
+ } catch (e) {
4442
+ const msg = `receipt is not valid JSON: ${e instanceof Error ? e.message : String(e)}`;
4443
+ if (flagBool(args.flags, "json")) {
4444
+ return {
4445
+ stdout: JSON.stringify({ valid: false, errors: [msg], signed: false }, null, 2),
4446
+ exitCode: 1
4447
+ };
4448
+ }
4449
+ return { stdout: "", stderr: `CallLint receipt: invalid
4450
+ - ${msg}`, exitCode: 1 };
4451
+ }
4452
+ const result = verifyReceipt(parsed);
4453
+ if (flagBool(args.flags, "json")) {
4454
+ return {
4455
+ stdout: JSON.stringify(result, null, 2),
4456
+ exitCode: result.valid ? EXIT.OK : 1
4457
+ };
4458
+ }
4459
+ if (!result.valid) {
4460
+ const lines = ["CallLint receipt: invalid", ...result.errors.map((e) => ` - ${e}`)];
4461
+ return { stdout: "", stderr: lines.join("\n"), exitCode: 1 };
4462
+ }
4463
+ const r = parsed;
4464
+ const signature = r.signature ? "present (shape-only, not cryptographically verified)" : "unsigned local receipt";
4465
+ const stdout = [
4466
+ "CallLint receipt: valid",
4467
+ `receipt_id: ${r.receipt_id}`,
4468
+ `schema: ${r.schema_version}`,
4469
+ `verdict: ${r.verdict}`,
4470
+ `signature: ${signature}`
4471
+ ].join("\n");
4472
+ return { stdout, exitCode: EXIT.OK };
4473
+ }
4474
+
4171
4475
  // src/run.ts
4172
4476
  function run(argv, deps) {
4173
4477
  const args = parseArgs(argv);
@@ -4197,7 +4501,8 @@ function run(argv, deps) {
4197
4501
  generatedAt: deps.generatedAt,
4198
4502
  writeCacheFile: deps.writeCacheFile,
4199
4503
  online: deps.online,
4200
- getChangedFilesDiff: deps.getChangedFilesDiff
4504
+ getChangedFilesDiff: deps.getChangedFilesDiff,
4505
+ toolVersion: deps.toolVersion
4201
4506
  });
4202
4507
  case "diagnostics":
4203
4508
  return diagnosticsCommand(args, {
@@ -4233,6 +4538,8 @@ function run(argv, deps) {
4233
4538
  });
4234
4539
  case "explain":
4235
4540
  return explainCommand(args, { cwd: deps.cwd });
4541
+ case "receipt":
4542
+ return receiptCommand(args, { cwd: deps.cwd });
4236
4543
  case "gen-rule":
4237
4544
  return genRuleCommand(args, { cwd: deps.cwd });
4238
4545
  case "policy":
@@ -4478,10 +4785,24 @@ async function breathe(argv, deps = {}) {
4478
4785
  }
4479
4786
  }
4480
4787
 
4788
+ // src/version.ts
4789
+ import { readFileSync as readFileSync9 } from "node:fs";
4790
+ import { fileURLToPath } from "node:url";
4791
+ import { dirname as dirname4, join as join11 } from "node:path";
4792
+ function resolveToolVersion() {
4793
+ try {
4794
+ const pkgPath = join11(dirname4(fileURLToPath(import.meta.url)), "..", "package.json");
4795
+ const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
4796
+ return typeof pkg.version === "string" && pkg.version.length > 0 ? pkg.version : "unknown";
4797
+ } catch {
4798
+ return "unknown";
4799
+ }
4800
+ }
4801
+
4481
4802
  // src/index.ts
4482
4803
  function readStdin() {
4483
4804
  try {
4484
- return readFileSync8(0, "utf8");
4805
+ return readFileSync10(0, "utf8");
4485
4806
  } catch {
4486
4807
  return "";
4487
4808
  }
@@ -4530,6 +4851,7 @@ async function main() {
4530
4851
  now,
4531
4852
  generatedAt,
4532
4853
  online,
4854
+ toolVersion: resolveToolVersion(),
4533
4855
  getChangedFilesDiff: () => gitChangedFiles(process.cwd())
4534
4856
  });
4535
4857
  if (result.stdout) process.stdout.write(result.stdout + "\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "calllint",
3
- "version": "0.6.0",
3
+ "version": "0.8.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",