calllint 0.8.0 → 0.9.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.
Files changed (2) hide show
  1. package/dist/index.js +417 -5
  2. package/package.json +2 -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 readFileSync10 } from "node:fs";
4
+ import { readFileSync as readFileSync11 } from "node:fs";
5
5
  import { execFileSync } from "node:child_process";
6
6
 
7
7
  // src/args.ts
@@ -4472,6 +4472,389 @@ function receiptCommand(args, deps) {
4472
4472
  return { stdout, exitCode: EXIT.OK };
4473
4473
  }
4474
4474
 
4475
+ // src/commands/action.ts
4476
+ import { readFileSync as readFileSync9 } from "node:fs";
4477
+ import { resolve as resolve5 } from "node:path";
4478
+
4479
+ // ../../packages/action-analyzer/src/types.ts
4480
+ var KIND_RISK_PROFILES = {
4481
+ "email.reply": ["ACTION", "SECRETS", "PROMPT"],
4482
+ "email.forward": ["ACTION", "SECRETS", "FILES", "PROMPT"],
4483
+ "message.post": ["ACTION", "SECRETS"],
4484
+ "a2a.delegate": ["ACTION", "NETWORK", "PROMPT"],
4485
+ "payment.authorize": ["MONEY", "SECRETS"],
4486
+ "account.register": ["ACTION", "SECRETS", "SUPPLY"],
4487
+ "github.write": ["ACTION", "SUPPLY"],
4488
+ "npm.publish": ["ACTION", "SUPPLY", "MONEY"],
4489
+ "cloud.modify": ["ACTION", "EXEC", "MONEY"]
4490
+ };
4491
+
4492
+ // ../../packages/action-analyzer/src/analyzeAction.ts
4493
+ function analyzeAction(descriptor) {
4494
+ const findings = [];
4495
+ if (descriptor.schema_version !== "calllint.action.v0") {
4496
+ findings.push({
4497
+ id: "action.unknown-schema-version",
4498
+ title: "Unknown action schema version",
4499
+ severity: "medium",
4500
+ blocker: false,
4501
+ symbol: "ACTION",
4502
+ riskClass: "S3",
4503
+ mode: "OBSERVED",
4504
+ confidence: "high",
4505
+ detectionMethod: "config-analysis",
4506
+ evidence: [{
4507
+ type: "config",
4508
+ path: "schema_version",
4509
+ value: descriptor.schema_version
4510
+ }],
4511
+ impact: `Unknown action schema version: ${descriptor.schema_version}`,
4512
+ fix: "Use calllint.action.v0 schema version"
4513
+ });
4514
+ return findings;
4515
+ }
4516
+ const riskProfile = KIND_RISK_PROFILES[descriptor.kind];
4517
+ if (!riskProfile) {
4518
+ findings.push({
4519
+ id: "action.unknown-kind",
4520
+ title: "Unknown action kind",
4521
+ severity: "medium",
4522
+ blocker: false,
4523
+ symbol: "ACTION",
4524
+ riskClass: "S3",
4525
+ mode: "OBSERVED",
4526
+ confidence: "high",
4527
+ detectionMethod: "config-analysis",
4528
+ evidence: [{
4529
+ type: "config",
4530
+ path: "kind",
4531
+ value: descriptor.kind
4532
+ }],
4533
+ impact: `Unknown action kind: ${descriptor.kind}`,
4534
+ fix: "Use one of the 9 defined action kinds (email.reply, email.forward, message.post, a2a.delegate, payment.authorize, account.register, github.write, npm.publish, cloud.modify)"
4535
+ });
4536
+ return findings;
4537
+ }
4538
+ switch (descriptor.kind) {
4539
+ case "email.reply":
4540
+ case "email.forward":
4541
+ findings.push(...analyzeEmail(descriptor));
4542
+ break;
4543
+ case "message.post":
4544
+ findings.push(...analyzeMessaging(descriptor));
4545
+ break;
4546
+ case "a2a.delegate":
4547
+ findings.push(...analyzeDelegate(descriptor));
4548
+ break;
4549
+ case "payment.authorize":
4550
+ findings.push(...analyzePayment(descriptor));
4551
+ break;
4552
+ case "account.register":
4553
+ findings.push(...analyzeRegistration(descriptor));
4554
+ break;
4555
+ case "github.write":
4556
+ findings.push(...analyzeGitHub(descriptor));
4557
+ break;
4558
+ case "npm.publish":
4559
+ findings.push(...analyzeNpmPublish(descriptor));
4560
+ break;
4561
+ case "cloud.modify":
4562
+ findings.push(...analyzeCloud(descriptor));
4563
+ break;
4564
+ }
4565
+ findings.push(...analyzeSecretShapedHeaders(descriptor));
4566
+ return findings;
4567
+ }
4568
+ function analyzeEmail(descriptor) {
4569
+ const findings = [];
4570
+ const metadata = descriptor.metadata || {};
4571
+ const hasAttachments = descriptor.parameters.has_attachments;
4572
+ const attachmentHashes = metadata.attachment_hashes || [];
4573
+ if (hasAttachments && attachmentHashes.length === 0) {
4574
+ findings.push({
4575
+ id: "action.unverified-attachment",
4576
+ title: "Unverified email attachments",
4577
+ severity: "high",
4578
+ blocker: false,
4579
+ symbol: "FILES",
4580
+ riskClass: "S2",
4581
+ mode: "OBSERVED",
4582
+ confidence: "high",
4583
+ detectionMethod: "config-analysis",
4584
+ evidence: [{
4585
+ type: "config",
4586
+ path: "parameters.has_attachments",
4587
+ value: String(hasAttachments)
4588
+ }],
4589
+ impact: "Email action has attachments but no attachment_hashes provided",
4590
+ fix: "Provide SHA-256 hashes for all attachments in metadata.attachment_hashes"
4591
+ });
4592
+ }
4593
+ return findings;
4594
+ }
4595
+ function analyzeMessaging(descriptor) {
4596
+ const findings = [];
4597
+ return findings;
4598
+ }
4599
+ function analyzeDelegate(descriptor) {
4600
+ const findings = [];
4601
+ const metadata = descriptor.metadata || {};
4602
+ const delegateTarget = metadata.delegate_target;
4603
+ if (!delegateTarget) {
4604
+ findings.push({
4605
+ id: "action.missing-delegate-target",
4606
+ title: "Missing delegate target",
4607
+ severity: "high",
4608
+ blocker: false,
4609
+ symbol: "NETWORK",
4610
+ riskClass: "S3",
4611
+ mode: "OBSERVED",
4612
+ confidence: "high",
4613
+ detectionMethod: "config-analysis",
4614
+ evidence: [{
4615
+ type: "config",
4616
+ path: "metadata.delegate_target",
4617
+ value: "null"
4618
+ }],
4619
+ impact: "Agent delegation missing delegate_target in metadata",
4620
+ fix: "Specify the delegate target (agent ID, API endpoint, or service name)"
4621
+ });
4622
+ } else if (typeof delegateTarget === "string" && delegateTarget.startsWith("http://")) {
4623
+ findings.push({
4624
+ id: "action.insecure-delegate-target",
4625
+ title: "Insecure delegate target",
4626
+ severity: "medium",
4627
+ blocker: false,
4628
+ symbol: "NETWORK",
4629
+ riskClass: "S3",
4630
+ mode: "OBSERVED",
4631
+ confidence: "high",
4632
+ detectionMethod: "config-analysis",
4633
+ evidence: [{
4634
+ type: "config",
4635
+ path: "metadata.delegate_target",
4636
+ value: delegateTarget
4637
+ }],
4638
+ impact: "Delegate target uses insecure HTTP",
4639
+ fix: "Use HTTPS for delegate targets"
4640
+ });
4641
+ }
4642
+ return findings;
4643
+ }
4644
+ function analyzePayment(descriptor) {
4645
+ const findings = [];
4646
+ const metadata = descriptor.metadata || {};
4647
+ const amount = metadata.amount;
4648
+ if (typeof amount === "number" && amount > 0) {
4649
+ findings.push({
4650
+ id: "action.financial-observed",
4651
+ title: "Financial transaction observed",
4652
+ severity: "high",
4653
+ blocker: false,
4654
+ symbol: "MONEY",
4655
+ riskClass: "S5",
4656
+ mode: "OBSERVED",
4657
+ confidence: "high",
4658
+ detectionMethod: "config-analysis",
4659
+ evidence: [{
4660
+ type: "config",
4661
+ path: "metadata.amount",
4662
+ value: String(amount)
4663
+ }],
4664
+ impact: `Payment authorization for ${metadata.currency || "unknown currency"} ${amount}`,
4665
+ fix: "Verify payment amount and recipient before authorizing"
4666
+ });
4667
+ }
4668
+ return findings;
4669
+ }
4670
+ function analyzeRegistration(descriptor) {
4671
+ const findings = [];
4672
+ return findings;
4673
+ }
4674
+ function analyzeGitHub(descriptor) {
4675
+ const findings = [];
4676
+ return findings;
4677
+ }
4678
+ function analyzeNpmPublish(descriptor) {
4679
+ const findings = [];
4680
+ return findings;
4681
+ }
4682
+ function analyzeCloud(descriptor) {
4683
+ const findings = [];
4684
+ return findings;
4685
+ }
4686
+ function analyzeSecretShapedHeaders(descriptor) {
4687
+ const findings = [];
4688
+ const metadata = descriptor.metadata || {};
4689
+ const headerKeys = metadata.header_keys || [];
4690
+ const secretPatterns = ["authorization", "api-key", "api_key", "token", "secret", "password", "bearer"];
4691
+ for (const key of headerKeys) {
4692
+ const lowerKey = key.toLowerCase();
4693
+ if (secretPatterns.some((pattern) => lowerKey.includes(pattern))) {
4694
+ findings.push({
4695
+ id: "secrets.env-key",
4696
+ title: "Secret-shaped header key",
4697
+ severity: "medium",
4698
+ blocker: false,
4699
+ symbol: "SECRETS",
4700
+ riskClass: "S2",
4701
+ mode: "OBSERVED",
4702
+ confidence: "medium",
4703
+ detectionMethod: "config-analysis",
4704
+ evidence: [{
4705
+ type: "config",
4706
+ path: "metadata.header_keys",
4707
+ value: key
4708
+ }],
4709
+ impact: `Header key resembles a secret: ${key}`,
4710
+ fix: "Verify that secret headers are properly secured and not logged"
4711
+ });
4712
+ break;
4713
+ }
4714
+ }
4715
+ return findings;
4716
+ }
4717
+
4718
+ // src/commands/action.ts
4719
+ function actionCommand(args, deps) {
4720
+ const subcommand = args.positionals[0];
4721
+ if (!subcommand || subcommand === "help") {
4722
+ return {
4723
+ stdout: actionHelp(),
4724
+ stderr: "",
4725
+ exitCode: 0
4726
+ };
4727
+ }
4728
+ if (subcommand === "inspect") {
4729
+ return actionInspect(args, deps);
4730
+ }
4731
+ return {
4732
+ stdout: "",
4733
+ stderr: `Unknown action subcommand: ${subcommand}
4734
+ Run \`calllint action help\`.`,
4735
+ exitCode: 2
4736
+ };
4737
+ }
4738
+ function actionInspect(args, deps) {
4739
+ const actionFile = args.positionals[1];
4740
+ if (!actionFile) {
4741
+ return {
4742
+ stdout: "",
4743
+ stderr: "Error: Missing action file\nUsage: calllint action inspect <file.json>",
4744
+ exitCode: 2
4745
+ };
4746
+ }
4747
+ try {
4748
+ const absolutePath = resolve5(deps.cwd, actionFile);
4749
+ const content = readFileSync9(absolutePath, "utf-8");
4750
+ const descriptor = JSON.parse(content);
4751
+ if (descriptor.schema_version !== "calllint.action.v0") {
4752
+ return {
4753
+ stdout: "",
4754
+ stderr: `Error: Unsupported schema version: ${descriptor.schema_version}
4755
+ Expected: calllint.action.v0`,
4756
+ exitCode: 2
4757
+ };
4758
+ }
4759
+ const policyPath = args.flags["policy"];
4760
+ const policy = loadPolicyOrDefault(policyPath ? resolve5(deps.cwd, policyPath) : void 0);
4761
+ const findings = analyzeAction(descriptor);
4762
+ const verdict = computeActionVerdict(findings, policy);
4763
+ let stdout = "";
4764
+ if (args.flags["json"]) {
4765
+ const report = {
4766
+ schema_version: "calllint.action-report.v0",
4767
+ verdict,
4768
+ findings,
4769
+ target: {
4770
+ type: "action",
4771
+ kind: descriptor.kind,
4772
+ schema_version: descriptor.schema_version
4773
+ },
4774
+ policy_applied: "default",
4775
+ scan_timestamp: (/* @__PURE__ */ new Date()).toISOString()
4776
+ };
4777
+ stdout = JSON.stringify(report, null, 2);
4778
+ } else {
4779
+ const header = `
4780
+ \u{1F50D} Action: ${descriptor.kind}
4781
+ `;
4782
+ const verdictEmoji = verdict === "SAFE" ? "\u2705" : verdict === "BLOCK" ? "\u26D4" : verdict === "REVIEW" ? "\u26A0\uFE0F" : "\u25C7";
4783
+ const verdictLine = `Verdict: ${verdictEmoji} ${verdict}
4784
+ `;
4785
+ const findingsLine = `Findings: ${findings.length}
4786
+ `;
4787
+ let body = verdictLine + findingsLine;
4788
+ if (findings.length > 0) {
4789
+ body += "\n";
4790
+ for (const f of findings) {
4791
+ body += ` \u2022 ${f.id}: ${f.impact}
4792
+ `;
4793
+ }
4794
+ }
4795
+ stdout = header + body;
4796
+ }
4797
+ return {
4798
+ stdout,
4799
+ stderr: "",
4800
+ exitCode: verdict === "SAFE" ? 0 : 1
4801
+ };
4802
+ } catch (error) {
4803
+ const err = error;
4804
+ let stderr = "";
4805
+ if (err.code === "ENOENT") {
4806
+ stderr = `Error: File not found: ${actionFile}`;
4807
+ } else if (error instanceof SyntaxError) {
4808
+ stderr = `Error: Invalid JSON in ${actionFile}
4809
+ ${error.message}`;
4810
+ } else {
4811
+ stderr = `Error: ${err.message}`;
4812
+ }
4813
+ return {
4814
+ stdout: "",
4815
+ stderr,
4816
+ exitCode: 2
4817
+ };
4818
+ }
4819
+ }
4820
+ function actionHelp() {
4821
+ return `calllint action \u2014 Inspect planned external actions
4822
+
4823
+ USAGE
4824
+ calllint action inspect <file.json>
4825
+ calllint action help
4826
+
4827
+ COMMANDS
4828
+ inspect <file> Analyze a calllint.action.v0 descriptor
4829
+ help Show this help
4830
+
4831
+ OPTIONS
4832
+ --json Output JSON report
4833
+ --policy <file> Use custom policy file
4834
+ --no-emoji Disable emoji in output
4835
+
4836
+ EXAMPLE
4837
+ calllint action inspect payment.json
4838
+ calllint action inspect email-reply.json --json
4839
+
4840
+ See: https://calllint.com/docs/action-inspect (ADR 0029)
4841
+ `;
4842
+ }
4843
+ function computeActionVerdict(findings, policy) {
4844
+ if (findings.length === 0) {
4845
+ return "SAFE";
4846
+ }
4847
+ const hasBlocker = findings.some((f) => f.blocker || f.severity === "critical");
4848
+ if (hasBlocker) {
4849
+ return "BLOCK";
4850
+ }
4851
+ const hasHigh = findings.some((f) => f.severity === "high");
4852
+ if (hasHigh) {
4853
+ return "REVIEW";
4854
+ }
4855
+ return "REVIEW";
4856
+ }
4857
+
4475
4858
  // src/run.ts
4476
4859
  function run(argv, deps) {
4477
4860
  const args = parseArgs(argv);
@@ -4540,6 +4923,8 @@ function run(argv, deps) {
4540
4923
  return explainCommand(args, { cwd: deps.cwd });
4541
4924
  case "receipt":
4542
4925
  return receiptCommand(args, { cwd: deps.cwd });
4926
+ case "action":
4927
+ return actionCommand(args, { cwd: deps.cwd });
4543
4928
  case "gen-rule":
4544
4929
  return genRuleCommand(args, { cwd: deps.cwd });
4545
4930
  case "policy":
@@ -4594,15 +4979,39 @@ async function fetchNpmFacts(packageSpec, fetchJson) {
4594
4979
  (k) => typeof scripts[k] === "string" && scripts[k].length > 0
4595
4980
  );
4596
4981
  const deprecated = typeof versionDoc.deprecated === "string" ? versionDoc.deprecated : void 0;
4982
+ const description = typeof versionDoc.description === "string" ? versionDoc.description : void 0;
4983
+ const readme = typeof doc.readme === "string" ? doc.readme : void 0;
4597
4984
  return {
4598
4985
  name,
4599
4986
  versionExists: true,
4600
4987
  installScripts,
4601
4988
  deprecated,
4602
4989
  latestVersion,
4603
- resolvedVersion
4990
+ resolvedVersion,
4991
+ description,
4992
+ readme
4604
4993
  };
4605
4994
  }
4995
+ function surfacesFromNpmFacts(facts) {
4996
+ const surfaces = [];
4997
+ if (typeof facts.description === "string" && facts.description.length > 0) {
4998
+ surfaces.push({
4999
+ path: `registry:${facts.name}#description`,
5000
+ kind: "registry-description",
5001
+ text: facts.description,
5002
+ truncated: false
5003
+ });
5004
+ }
5005
+ if (typeof facts.readme === "string" && facts.readme.length > 0) {
5006
+ surfaces.push({
5007
+ path: `registry:${facts.name}#readme`,
5008
+ kind: "registry-readme",
5009
+ text: facts.readme,
5010
+ truncated: false
5011
+ });
5012
+ }
5013
+ return surfaces;
5014
+ }
4606
5015
  function findingsFromNpmFacts(facts, fetchedAt) {
4607
5016
  const findings = [];
4608
5017
  const stamp = (f) => ({ ...f, source: "online", fetchedAt });
@@ -4660,6 +5069,9 @@ function findingsFromNpmFacts(facts, fetchedAt) {
4660
5069
  fix: "Migrate to the maintained successor or a supported version."
4661
5070
  }));
4662
5071
  }
5072
+ for (const f of analyzeDocumentSurfaces(surfacesFromNpmFacts(facts))) {
5073
+ findings.push(stamp(f));
5074
+ }
4663
5075
  return findings;
4664
5076
  }
4665
5077
  async function enrichNpmPackage(packageSpec, fetchJson, fetchedAt) {
@@ -4786,13 +5198,13 @@ async function breathe(argv, deps = {}) {
4786
5198
  }
4787
5199
 
4788
5200
  // src/version.ts
4789
- import { readFileSync as readFileSync9 } from "node:fs";
5201
+ import { readFileSync as readFileSync10 } from "node:fs";
4790
5202
  import { fileURLToPath } from "node:url";
4791
5203
  import { dirname as dirname4, join as join11 } from "node:path";
4792
5204
  function resolveToolVersion() {
4793
5205
  try {
4794
5206
  const pkgPath = join11(dirname4(fileURLToPath(import.meta.url)), "..", "package.json");
4795
- const pkg = JSON.parse(readFileSync9(pkgPath, "utf8"));
5207
+ const pkg = JSON.parse(readFileSync10(pkgPath, "utf8"));
4796
5208
  return typeof pkg.version === "string" && pkg.version.length > 0 ? pkg.version : "unknown";
4797
5209
  } catch {
4798
5210
  return "unknown";
@@ -4802,7 +5214,7 @@ function resolveToolVersion() {
4802
5214
  // src/index.ts
4803
5215
  function readStdin() {
4804
5216
  try {
4805
- return readFileSync10(0, "utf8");
5217
+ return readFileSync11(0, "utf8");
4806
5218
  } catch {
4807
5219
  return "";
4808
5220
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "calllint",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
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",
@@ -49,6 +49,7 @@
49
49
  },
50
50
  "dependencies": {},
51
51
  "devDependencies": {
52
+ "@calllint/action-analyzer": "workspace:*",
52
53
  "@calllint/core": "workspace:*",
53
54
  "@calllint/fixtures": "workspace:*",
54
55
  "@calllint/online": "workspace:*",