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.
- package/dist/index.js +331 -9
- 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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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",
|