calllint 0.7.0 → 0.9.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 +296 -10
  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
@@ -92,6 +93,8 @@ SCAN OPTIONS
92
93
  --markdown Emit Markdown for PR comments / GitHub Step Summary
93
94
  --html Emit a self-contained HTML report
94
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)
95
98
  --policy <file> Use a policy file (default: built-in defaults)
96
99
  --stdin Read config JSON from stdin
97
100
  --ci Exit non-zero per policy (BLOCK=30, UNKNOWN=20, REVIEW=10 if enabled)
@@ -112,6 +115,7 @@ EXAMPLES
112
115
  calllint check ./mcp.json --json
113
116
  calllint scan .cursor/mcp.json --markdown
114
117
  calllint scan .cursor/mcp.json --badge > calllint-badge.json
118
+ calllint scan .cursor/mcp.json --receipt && calllint receipt verify calllint-receipt.json
115
119
  calllint verify ./mcp.json --ci
116
120
  calllint approve && calllint verify --approved --ci
117
121
  calllint explain filesystem
@@ -2978,6 +2982,147 @@ ${rule()}
2978
2982
  }
2979
2983
  }
2980
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
+
2981
3126
  // ../../packages/report-renderer/src/style.ts
2982
3127
  var DEFAULT_STYLE = { emoji: true };
2983
3128
  var NO_EMOJI_STYLE = { emoji: false };
@@ -3738,7 +3883,7 @@ function readDocumentSurfaces(dir) {
3738
3883
  }
3739
3884
 
3740
3885
  // src/commands/scan.ts
3741
- import { readFileSync as readFileSync7 } from "node:fs";
3886
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "node:fs";
3742
3887
  function scanCommand(args, deps) {
3743
3888
  if (flagBool(args.flags, "changed")) {
3744
3889
  return scanChangedCommand(args, deps);
@@ -3761,7 +3906,7 @@ function scanCommand(args, deps) {
3761
3906
  const { text, configPath } = input;
3762
3907
  return scanOneConfig(text, configPath, policy, args, deps);
3763
3908
  }
3764
- function scanOneConfig(text, configPath, policy, args, deps) {
3909
+ function scanOneConfig(text, configPath, policy, args, deps, allowReceipt = true) {
3765
3910
  const surfaceDir = flagStr(args.flags, "surface-dir");
3766
3911
  const surfaces = surfaceDir ? readDocumentSurfaces(resolve2(deps.cwd, surfaceDir)) : void 0;
3767
3912
  let summary;
@@ -3790,9 +3935,38 @@ function scanOneConfig(text, configPath, policy, args, deps) {
3790
3935
  }
3791
3936
  }
3792
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
+ }
3793
3942
  const exitCode = flagBool(args.flags, "ci") ? exitCodeFor(summary, policy) : EXIT.OK;
3794
3943
  return { stdout, exitCode };
3795
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
+ }
3796
3970
  function renderSummary(summary, args) {
3797
3971
  const style = flagBool(args.flags, "no-emoji") ? NO_EMOJI_STYLE : DEFAULT_STYLE;
3798
3972
  if (flagBool(args.flags, "json")) return renderJson(summary);
@@ -3931,7 +4105,7 @@ function worstExit2(decisions) {
3931
4105
  }
3932
4106
 
3933
4107
  // src/commands/genRule.ts
3934
- import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "node:fs";
4108
+ import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync3 } from "node:fs";
3935
4109
  import { dirname as dirname3, join as join7, resolve as resolve3 } from "node:path";
3936
4110
  function isRuleHost(v) {
3937
4111
  return v !== void 0 && RULE_HOSTS.includes(v);
@@ -3978,7 +4152,7 @@ Run \`calllint gen-rule\` to list hosts.`,
3978
4152
  }
3979
4153
  function defaultWrite(path, content) {
3980
4154
  mkdirSync3(dirname3(path), { recursive: true });
3981
- writeFileSync3(path, content, "utf8");
4155
+ writeFileSync4(path, content, "utf8");
3982
4156
  }
3983
4157
 
3984
4158
  // src/commands/diagnostics.ts
@@ -4061,7 +4235,7 @@ function explainCommand(args, deps) {
4061
4235
  }
4062
4236
 
4063
4237
  // src/commands/policy.ts
4064
- import { existsSync as existsSync6, writeFileSync as writeFileSync4 } from "node:fs";
4238
+ import { existsSync as existsSync6, writeFileSync as writeFileSync5 } from "node:fs";
4065
4239
  import { join as join9 } from "node:path";
4066
4240
  function policyCommand(args, deps) {
4067
4241
  const sub = args.positionals[0];
@@ -4074,7 +4248,7 @@ function policyCommand(args, deps) {
4074
4248
  exitCode: EXIT.USAGE
4075
4249
  };
4076
4250
  }
4077
- writeFileSync4(path, defaultPolicyJson(), "utf8");
4251
+ writeFileSync5(path, defaultPolicyJson(), "utf8");
4078
4252
  return { stdout: `Wrote default policy to ${path}`, exitCode: EXIT.OK };
4079
4253
  }
4080
4254
  if (sub === "explain") {
@@ -4231,6 +4405,73 @@ function approveCommand(args, deps) {
4231
4405
  };
4232
4406
  }
4233
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
+
4234
4475
  // src/run.ts
4235
4476
  function run(argv, deps) {
4236
4477
  const args = parseArgs(argv);
@@ -4260,7 +4501,8 @@ function run(argv, deps) {
4260
4501
  generatedAt: deps.generatedAt,
4261
4502
  writeCacheFile: deps.writeCacheFile,
4262
4503
  online: deps.online,
4263
- getChangedFilesDiff: deps.getChangedFilesDiff
4504
+ getChangedFilesDiff: deps.getChangedFilesDiff,
4505
+ toolVersion: deps.toolVersion
4264
4506
  });
4265
4507
  case "diagnostics":
4266
4508
  return diagnosticsCommand(args, {
@@ -4296,6 +4538,8 @@ function run(argv, deps) {
4296
4538
  });
4297
4539
  case "explain":
4298
4540
  return explainCommand(args, { cwd: deps.cwd });
4541
+ case "receipt":
4542
+ return receiptCommand(args, { cwd: deps.cwd });
4299
4543
  case "gen-rule":
4300
4544
  return genRuleCommand(args, { cwd: deps.cwd });
4301
4545
  case "policy":
@@ -4350,15 +4594,39 @@ async function fetchNpmFacts(packageSpec, fetchJson) {
4350
4594
  (k) => typeof scripts[k] === "string" && scripts[k].length > 0
4351
4595
  );
4352
4596
  const deprecated = typeof versionDoc.deprecated === "string" ? versionDoc.deprecated : void 0;
4597
+ const description = typeof versionDoc.description === "string" ? versionDoc.description : void 0;
4598
+ const readme = typeof doc.readme === "string" ? doc.readme : void 0;
4353
4599
  return {
4354
4600
  name,
4355
4601
  versionExists: true,
4356
4602
  installScripts,
4357
4603
  deprecated,
4358
4604
  latestVersion,
4359
- resolvedVersion
4605
+ resolvedVersion,
4606
+ description,
4607
+ readme
4360
4608
  };
4361
4609
  }
4610
+ function surfacesFromNpmFacts(facts) {
4611
+ const surfaces = [];
4612
+ if (typeof facts.description === "string" && facts.description.length > 0) {
4613
+ surfaces.push({
4614
+ path: `registry:${facts.name}#description`,
4615
+ kind: "registry-description",
4616
+ text: facts.description,
4617
+ truncated: false
4618
+ });
4619
+ }
4620
+ if (typeof facts.readme === "string" && facts.readme.length > 0) {
4621
+ surfaces.push({
4622
+ path: `registry:${facts.name}#readme`,
4623
+ kind: "registry-readme",
4624
+ text: facts.readme,
4625
+ truncated: false
4626
+ });
4627
+ }
4628
+ return surfaces;
4629
+ }
4362
4630
  function findingsFromNpmFacts(facts, fetchedAt) {
4363
4631
  const findings = [];
4364
4632
  const stamp = (f) => ({ ...f, source: "online", fetchedAt });
@@ -4416,6 +4684,9 @@ function findingsFromNpmFacts(facts, fetchedAt) {
4416
4684
  fix: "Migrate to the maintained successor or a supported version."
4417
4685
  }));
4418
4686
  }
4687
+ for (const f of analyzeDocumentSurfaces(surfacesFromNpmFacts(facts))) {
4688
+ findings.push(stamp(f));
4689
+ }
4419
4690
  return findings;
4420
4691
  }
4421
4692
  async function enrichNpmPackage(packageSpec, fetchJson, fetchedAt) {
@@ -4541,10 +4812,24 @@ async function breathe(argv, deps = {}) {
4541
4812
  }
4542
4813
  }
4543
4814
 
4815
+ // src/version.ts
4816
+ import { readFileSync as readFileSync9 } from "node:fs";
4817
+ import { fileURLToPath } from "node:url";
4818
+ import { dirname as dirname4, join as join11 } from "node:path";
4819
+ function resolveToolVersion() {
4820
+ try {
4821
+ const pkgPath = join11(dirname4(fileURLToPath(import.meta.url)), "..", "package.json");
4822
+ const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
4823
+ return typeof pkg.version === "string" && pkg.version.length > 0 ? pkg.version : "unknown";
4824
+ } catch {
4825
+ return "unknown";
4826
+ }
4827
+ }
4828
+
4544
4829
  // src/index.ts
4545
4830
  function readStdin() {
4546
4831
  try {
4547
- return readFileSync8(0, "utf8");
4832
+ return readFileSync10(0, "utf8");
4548
4833
  } catch {
4549
4834
  return "";
4550
4835
  }
@@ -4593,6 +4878,7 @@ async function main() {
4593
4878
  now,
4594
4879
  generatedAt,
4595
4880
  online,
4881
+ toolVersion: resolveToolVersion(),
4596
4882
  getChangedFilesDiff: () => gitChangedFiles(process.cwd())
4597
4883
  });
4598
4884
  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.7.0",
3
+ "version": "0.9.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",