calllint 1.6.0 → 1.7.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.
- package/dist/index.js +699 -295
- 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
|
|
4
|
+
import { readFileSync as readFileSync22 } from "node:fs";
|
|
5
5
|
import { execFileSync } from "node:child_process";
|
|
6
6
|
|
|
7
7
|
// src/args.ts
|
|
@@ -71,11 +71,12 @@ COMMANDS
|
|
|
71
71
|
inbox inspect <f> Preflight a normalized agent inbox event
|
|
72
72
|
evidence import <f> Import a third-party scanner report as evidence (no re-scoring)
|
|
73
73
|
trust prepare <target> Read-only Trust Gateway preview: resolve to a digest-pinned identity
|
|
74
|
+
integrate Install the CallLint preflight server into detected hosts (plan-only; --apply writes)
|
|
74
75
|
diagnostics [target] Emit editor/agent-host diagnostics JSON (calllint.diagnostics.v0)
|
|
75
76
|
baseline [target] Record the approved risk surface as a baseline
|
|
76
77
|
approve Record the repo-wide capability surface as approved state (L4)
|
|
77
78
|
guard Continuous Guard: re-decide on authority change; silent when unchanged
|
|
78
|
-
guard install --host <h> Install a guard hook (git pre-
|
|
79
|
+
guard install --host <h> Install a guard hook (git, git-pre-push, github, claude-code, copilot, gemini, vscode)
|
|
79
80
|
guard status Show baseline / disable / installed-hook state
|
|
80
81
|
guard disable Turn Continuous Guard off (.calllint/guard.json)
|
|
81
82
|
receipt verify <f> Validate a calllint.receipt.v0 (structure + signature if present)
|
|
@@ -127,7 +128,8 @@ VERIFY OPTIONS
|
|
|
127
128
|
--json Emit the drift report JSON
|
|
128
129
|
|
|
129
130
|
GUARD OPTIONS
|
|
130
|
-
--host <h> guard install target: git
|
|
131
|
+
--host <h> guard install target: git | git-pre-push | github | claude-code | copilot | gemini | vscode
|
|
132
|
+
(session-start hooks, non-blocking; shared-config hosts print a fragment to merge)
|
|
131
133
|
--approved [file] Approved baseline to diff against (default: .calllint/approved.json)
|
|
132
134
|
--json Emit the guard assessment / status JSON
|
|
133
135
|
(exit) silent/note=0, REVIEW=10, UNKNOWN=20, BLOCK=30; guard self-failure is non-zero
|
|
@@ -895,6 +897,228 @@ function resolveArtifactIdentity(input) {
|
|
|
895
897
|
return identity;
|
|
896
898
|
}
|
|
897
899
|
|
|
900
|
+
// ../../packages/evidence/src/types.ts
|
|
901
|
+
var EVIDENCE_SCHEMA_VERSION = "calllint.evidence-provider.v0";
|
|
902
|
+
|
|
903
|
+
// ../../packages/evidence/src/providers/skillspector.ts
|
|
904
|
+
function pinnedVersion(raw) {
|
|
905
|
+
const tool = raw.tool;
|
|
906
|
+
const commit = typeof raw.commit === "string" && raw.commit || tool && typeof tool.commit === "string" && tool.commit || "";
|
|
907
|
+
if (commit) return `git:${commit}`;
|
|
908
|
+
const version = typeof raw.version === "string" && raw.version || tool && typeof tool.version === "string" && tool.version || "";
|
|
909
|
+
return version || void 0;
|
|
910
|
+
}
|
|
911
|
+
function parseSkillSpectorJson(parsed) {
|
|
912
|
+
const raw = asObject(parsed, "SkillSpector JSON root");
|
|
913
|
+
const degradedReasons = [];
|
|
914
|
+
const usedLlm = raw.llm_used === true || raw.llmUsed === true;
|
|
915
|
+
const scanMode = usedLlm ? "llm" : "static";
|
|
916
|
+
const rawFindings = arrayOf(raw.findings) ?? arrayOf(raw.results) ?? [];
|
|
917
|
+
const findings = rawFindings.map((f) => {
|
|
918
|
+
const o = asObject(f, "SkillSpector finding");
|
|
919
|
+
return {
|
|
920
|
+
providerRuleId: String(o.rule_id ?? o.ruleId ?? o.id ?? "unknown"),
|
|
921
|
+
providerSeverity: String(o.severity ?? o.level ?? "unknown"),
|
|
922
|
+
message: typeof o.message === "string" ? o.message : void 0,
|
|
923
|
+
locations: extractLocations(o)
|
|
924
|
+
};
|
|
925
|
+
});
|
|
926
|
+
let completenessHint;
|
|
927
|
+
const status = String(raw.status ?? raw.scan_status ?? "").toLowerCase();
|
|
928
|
+
if (status && status !== "complete" && status !== "completed" && status !== "success") {
|
|
929
|
+
degradedReasons.push(`SkillSpector reported status "${status}"`);
|
|
930
|
+
completenessHint = status === "partial" ? "partial" : "degraded";
|
|
931
|
+
}
|
|
932
|
+
if (raw.partial === true) {
|
|
933
|
+
degradedReasons.push("SkillSpector reported a partial scan");
|
|
934
|
+
completenessHint = completenessHint ?? "partial";
|
|
935
|
+
}
|
|
936
|
+
if (raw.degraded === true) {
|
|
937
|
+
degradedReasons.push("SkillSpector reported a degraded scan");
|
|
938
|
+
completenessHint = "degraded";
|
|
939
|
+
}
|
|
940
|
+
const coverage = (arrayOf(raw.categories) ?? arrayOf(raw.coverage) ?? []).map((c) => String(c));
|
|
941
|
+
return {
|
|
942
|
+
provider: "skillspector",
|
|
943
|
+
providerVersion: pinnedVersion(raw),
|
|
944
|
+
scanMode,
|
|
945
|
+
coverage,
|
|
946
|
+
findings,
|
|
947
|
+
degradedReasons,
|
|
948
|
+
completenessHint
|
|
949
|
+
};
|
|
950
|
+
}
|
|
951
|
+
function parseSkillSpectorSarif(parsed) {
|
|
952
|
+
const raw = asObject(parsed, "SARIF root");
|
|
953
|
+
const runs = arrayOf(raw.runs) ?? [];
|
|
954
|
+
if (runs.length === 0) {
|
|
955
|
+
return {
|
|
956
|
+
provider: "skillspector",
|
|
957
|
+
findings: [],
|
|
958
|
+
degradedReasons: ["SARIF had no runs"]
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
const degradedReasons = [];
|
|
962
|
+
const findings = [];
|
|
963
|
+
let providerVersion;
|
|
964
|
+
const coverage = [];
|
|
965
|
+
for (const runUnknown of runs) {
|
|
966
|
+
const run2 = asObject(runUnknown, "SARIF run");
|
|
967
|
+
const tool = asObject(run2.tool ?? {}, "SARIF tool");
|
|
968
|
+
const driver = asObject(tool.driver ?? {}, "SARIF tool.driver");
|
|
969
|
+
if (!providerVersion) {
|
|
970
|
+
const v = typeof driver.semanticVersion === "string" && driver.semanticVersion || typeof driver.version === "string" && driver.version || "";
|
|
971
|
+
if (v) providerVersion = v;
|
|
972
|
+
}
|
|
973
|
+
const results = arrayOf(run2.results) ?? [];
|
|
974
|
+
for (const resUnknown of results) {
|
|
975
|
+
const res = asObject(resUnknown, "SARIF result");
|
|
976
|
+
findings.push({
|
|
977
|
+
providerRuleId: String(res.ruleId ?? "unknown"),
|
|
978
|
+
providerSeverity: String(res.level ?? "warning"),
|
|
979
|
+
message: extractSarifMessage(res),
|
|
980
|
+
locations: extractSarifLocations(res)
|
|
981
|
+
});
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
degradedReasons.push("SARIF import: provider-specific fields may be lost vs JSON form");
|
|
985
|
+
return {
|
|
986
|
+
provider: "skillspector",
|
|
987
|
+
providerVersion,
|
|
988
|
+
scanMode: "static",
|
|
989
|
+
coverage,
|
|
990
|
+
findings,
|
|
991
|
+
degradedReasons
|
|
992
|
+
};
|
|
993
|
+
}
|
|
994
|
+
function asObject(v, what) {
|
|
995
|
+
if (v && typeof v === "object" && !Array.isArray(v)) return v;
|
|
996
|
+
throw new Error(`expected object for ${what}`);
|
|
997
|
+
}
|
|
998
|
+
function arrayOf(v) {
|
|
999
|
+
return Array.isArray(v) ? v : void 0;
|
|
1000
|
+
}
|
|
1001
|
+
function extractLocations(o) {
|
|
1002
|
+
const loc = o.location ?? o.locations ?? o.path;
|
|
1003
|
+
if (typeof loc === "string") return [loc];
|
|
1004
|
+
if (Array.isArray(loc)) return loc.map((x) => String(x));
|
|
1005
|
+
return void 0;
|
|
1006
|
+
}
|
|
1007
|
+
function extractSarifMessage(res) {
|
|
1008
|
+
const msg = res.message;
|
|
1009
|
+
if (msg && typeof msg.text === "string") return msg.text;
|
|
1010
|
+
return void 0;
|
|
1011
|
+
}
|
|
1012
|
+
function extractSarifLocations(res) {
|
|
1013
|
+
const locs = arrayOf(res.locations);
|
|
1014
|
+
if (!locs) return void 0;
|
|
1015
|
+
const out = [];
|
|
1016
|
+
for (const l of locs) {
|
|
1017
|
+
const lo = l;
|
|
1018
|
+
const phys = lo?.physicalLocation;
|
|
1019
|
+
const art = phys?.artifactLocation;
|
|
1020
|
+
const uri = art && typeof art.uri === "string" ? art.uri : void 0;
|
|
1021
|
+
const region = phys?.region;
|
|
1022
|
+
const line = region && typeof region.startLine === "number" ? region.startLine : void 0;
|
|
1023
|
+
if (uri) out.push(line ? `${uri}:${line}` : uri);
|
|
1024
|
+
}
|
|
1025
|
+
return out.length > 0 ? out : void 0;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
// ../../packages/evidence/src/importEvidence.ts
|
|
1029
|
+
var ZERO_DIGEST = `sha256:${"0".repeat(64)}`;
|
|
1030
|
+
function detectProvider(raw, explicit) {
|
|
1031
|
+
if (explicit) return explicit;
|
|
1032
|
+
const r = raw;
|
|
1033
|
+
if (!r) return "unknown";
|
|
1034
|
+
const topTool = r.tool;
|
|
1035
|
+
const jsonName = typeof r.scanner === "string" && r.scanner || topTool && typeof topTool.name === "string" && topTool.name || "";
|
|
1036
|
+
let sarifName = "";
|
|
1037
|
+
const runs = r.runs;
|
|
1038
|
+
if (Array.isArray(runs) && runs.length > 0) {
|
|
1039
|
+
const run2 = runs[0];
|
|
1040
|
+
const tool = run2?.tool;
|
|
1041
|
+
const driver = tool?.driver;
|
|
1042
|
+
if (driver && typeof driver.name === "string") sarifName = driver.name;
|
|
1043
|
+
}
|
|
1044
|
+
if (/skillspector/i.test(String(jsonName)) || /skillspector/i.test(sarifName)) {
|
|
1045
|
+
return "skillspector";
|
|
1046
|
+
}
|
|
1047
|
+
return "unknown";
|
|
1048
|
+
}
|
|
1049
|
+
function importEvidence(rawText, opts = {}) {
|
|
1050
|
+
const format = opts.format ?? (looksLikeSarif(rawText) ? "sarif" : "json");
|
|
1051
|
+
const rawReportDigest = sha256(rawText);
|
|
1052
|
+
let parsed;
|
|
1053
|
+
try {
|
|
1054
|
+
parsed = JSON.parse(rawText);
|
|
1055
|
+
} catch {
|
|
1056
|
+
return failClosed(opts.provider ?? "unknown", rawReportDigest, [
|
|
1057
|
+
`report is not valid JSON (format=${format})`
|
|
1058
|
+
]);
|
|
1059
|
+
}
|
|
1060
|
+
const provider = detectProvider(parsed, opts.provider);
|
|
1061
|
+
let result;
|
|
1062
|
+
try {
|
|
1063
|
+
if (provider === "skillspector") {
|
|
1064
|
+
result = format === "sarif" ? parseSkillSpectorSarif(parsed) : parseSkillSpectorJson(parsed);
|
|
1065
|
+
} else {
|
|
1066
|
+
return failClosed(provider, rawReportDigest, [
|
|
1067
|
+
`no adapter for provider "${provider}"; evidence not interpreted`
|
|
1068
|
+
]);
|
|
1069
|
+
}
|
|
1070
|
+
} catch (err2) {
|
|
1071
|
+
return failClosed(provider, rawReportDigest, [
|
|
1072
|
+
`adapter error: ${err2.message}`
|
|
1073
|
+
]);
|
|
1074
|
+
}
|
|
1075
|
+
return finalizeEnvelope(result, rawReportDigest);
|
|
1076
|
+
}
|
|
1077
|
+
function failClosed(provider, rawReportDigest, reasons) {
|
|
1078
|
+
return {
|
|
1079
|
+
schema_version: EVIDENCE_SCHEMA_VERSION,
|
|
1080
|
+
provider: provider || "unknown",
|
|
1081
|
+
providerVersion: "unknown",
|
|
1082
|
+
artifactDigest: ZERO_DIGEST,
|
|
1083
|
+
scanMode: "static",
|
|
1084
|
+
coverage: [],
|
|
1085
|
+
completeness: "failed",
|
|
1086
|
+
findings: [],
|
|
1087
|
+
rawReportDigest,
|
|
1088
|
+
degradedReasons: reasons
|
|
1089
|
+
};
|
|
1090
|
+
}
|
|
1091
|
+
function finalizeEnvelope(r, rawReportDigest) {
|
|
1092
|
+
const degradedReasons = [...r.degradedReasons ?? []];
|
|
1093
|
+
const rank = { complete: 0, partial: 1, degraded: 2, failed: 3 };
|
|
1094
|
+
const rankToCompleteness = ["complete", "partial", "degraded", "failed"];
|
|
1095
|
+
let level = rank[r.completenessHint ?? (degradedReasons.length > 0 ? "degraded" : "complete")];
|
|
1096
|
+
let providerVersion = r.providerVersion?.trim() || "";
|
|
1097
|
+
if (!providerVersion) {
|
|
1098
|
+
providerVersion = "unknown";
|
|
1099
|
+
degradedReasons.push("provider version not pinned (no release/commit reported)");
|
|
1100
|
+
level = Math.max(level, rank.degraded);
|
|
1101
|
+
}
|
|
1102
|
+
const completeness = rankToCompleteness[level];
|
|
1103
|
+
return {
|
|
1104
|
+
schema_version: EVIDENCE_SCHEMA_VERSION,
|
|
1105
|
+
provider: r.provider,
|
|
1106
|
+
providerVersion,
|
|
1107
|
+
artifactDigest: r.artifactDigest ?? ZERO_DIGEST,
|
|
1108
|
+
scanMode: r.scanMode ?? "static",
|
|
1109
|
+
coverage: r.coverage ?? [],
|
|
1110
|
+
completeness,
|
|
1111
|
+
findings: r.findings,
|
|
1112
|
+
rawReportDigest,
|
|
1113
|
+
startedAt: r.startedAt,
|
|
1114
|
+
finishedAt: r.finishedAt,
|
|
1115
|
+
degradedReasons
|
|
1116
|
+
};
|
|
1117
|
+
}
|
|
1118
|
+
function looksLikeSarif(text) {
|
|
1119
|
+
return /"runs"\s*:/.test(text) && /sarif/i.test(text);
|
|
1120
|
+
}
|
|
1121
|
+
|
|
898
1122
|
// ../../packages/static-analyzer/src/detectors/unpinnedPackage.ts
|
|
899
1123
|
function detectUnpinnedPackage(ctx) {
|
|
900
1124
|
const { binding } = ctx;
|
|
@@ -4610,274 +4834,52 @@ function renderHtml(summary) {
|
|
|
4610
4834
|
</body>
|
|
4611
4835
|
</html>`;
|
|
4612
4836
|
}
|
|
4613
|
-
|
|
4614
|
-
// ../../packages/report-renderer/src/renderTrustPacket.ts
|
|
4615
|
-
var COMPLETENESS_HINT = {
|
|
4616
|
-
complete: "complete",
|
|
4617
|
-
partial: "partial (incomplete \u2014 treat as inconclusive)",
|
|
4618
|
-
degraded: "degraded (not a pass)",
|
|
4619
|
-
failed: "failed (not a pass)"
|
|
4620
|
-
};
|
|
4621
|
-
function topProviderSeverity(ev) {
|
|
4622
|
-
const sevs = ev.findings.map(
|
|
4623
|
-
(f) => f && typeof f === "object" && "providerSeverity" in f ? String(f.providerSeverity) : void 0
|
|
4624
|
-
).filter((s) => Boolean(s));
|
|
4625
|
-
return sevs[0];
|
|
4626
|
-
}
|
|
4627
|
-
function whyTheyDiffer(ev, authorityVerdict) {
|
|
4628
|
-
if (ev.completeness === "failed" || ev.completeness === "degraded") {
|
|
4629
|
-
return "Why they differ: the content scan is " + COMPLETENESS_HINT[ev.completeness] + ", so it carries no weight here; CallLint's authority verdict stands on its own evidence.";
|
|
4630
|
-
}
|
|
4631
|
-
if (ev.findings.length === 0 && authorityVerdict !== "SAFE") {
|
|
4632
|
-
return "Why they differ: the content scan found nothing malicious, but CallLint judges the granted authority itself too broad \u2014 clean code can still request unsafe capabilities.";
|
|
4633
|
-
}
|
|
4634
|
-
return "Why they differ: the two tools answer different questions \u2014 content risk (the scanner) vs. whether the requested authority is acceptable (CallLint). Neither overrides the other.";
|
|
4635
|
-
}
|
|
4636
|
-
function renderTrustPacket(summary, toolVersion, style = DEFAULT_STYLE) {
|
|
4637
|
-
const evidence = summary.evidence;
|
|
4638
|
-
if (!evidence || evidence.length === 0) return "";
|
|
4639
|
-
const lines = ["", "Joint Trust Packet", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"];
|
|
4640
|
-
lines.push("Content scan");
|
|
4641
|
-
for (const ev of evidence) {
|
|
4642
|
-
const sev = topProviderSeverity(ev);
|
|
4643
|
-
const findingsLabel = ev.findings.length === 0 ? "no findings" : `${ev.findings.length} finding${ev.findings.length === 1 ? "" : "s"}` + (sev ? ` (top severity: ${sev})` : "");
|
|
4644
|
-
lines.push(
|
|
4645
|
-
` ${ev.provider} ${ev.providerVersion} scanMode: ${ev.scanMode} completeness: ${COMPLETENESS_HINT[ev.completeness]}`
|
|
4646
|
-
);
|
|
4647
|
-
lines.push(` ${findingsLabel}`);
|
|
4648
|
-
lines.push(` raw report digest: ${ev.rawReportDigest}`);
|
|
4649
|
-
for (const reason of ev.degradedReasons) {
|
|
4650
|
-
lines.push(` degraded: ${reason}`);
|
|
4651
|
-
}
|
|
4652
|
-
}
|
|
4653
|
-
lines.push("Authority scan");
|
|
4654
|
-
lines.push(
|
|
4655
|
-
` CallLint ${toolVersion} ${verdictTag(summary.verdict, style)} (${summary.publicVerdictLabel})`
|
|
4656
|
-
);
|
|
4657
|
-
lines.push(whyTheyDiffer(evidence[0], summary.verdict));
|
|
4658
|
-
return lines.join("\n");
|
|
4659
|
-
}
|
|
4660
|
-
|
|
4661
|
-
// ../../packages/evidence/src/types.ts
|
|
4662
|
-
var EVIDENCE_SCHEMA_VERSION = "calllint.evidence-provider.v0";
|
|
4663
|
-
|
|
4664
|
-
// ../../packages/evidence/src/providers/skillspector.ts
|
|
4665
|
-
function pinnedVersion(raw) {
|
|
4666
|
-
const tool = raw.tool;
|
|
4667
|
-
const commit = typeof raw.commit === "string" && raw.commit || tool && typeof tool.commit === "string" && tool.commit || "";
|
|
4668
|
-
if (commit) return `git:${commit}`;
|
|
4669
|
-
const version = typeof raw.version === "string" && raw.version || tool && typeof tool.version === "string" && tool.version || "";
|
|
4670
|
-
return version || void 0;
|
|
4671
|
-
}
|
|
4672
|
-
function parseSkillSpectorJson(parsed) {
|
|
4673
|
-
const raw = asObject(parsed, "SkillSpector JSON root");
|
|
4674
|
-
const degradedReasons = [];
|
|
4675
|
-
const usedLlm = raw.llm_used === true || raw.llmUsed === true;
|
|
4676
|
-
const scanMode = usedLlm ? "llm" : "static";
|
|
4677
|
-
const rawFindings = arrayOf(raw.findings) ?? arrayOf(raw.results) ?? [];
|
|
4678
|
-
const findings = rawFindings.map((f) => {
|
|
4679
|
-
const o = asObject(f, "SkillSpector finding");
|
|
4680
|
-
return {
|
|
4681
|
-
providerRuleId: String(o.rule_id ?? o.ruleId ?? o.id ?? "unknown"),
|
|
4682
|
-
providerSeverity: String(o.severity ?? o.level ?? "unknown"),
|
|
4683
|
-
message: typeof o.message === "string" ? o.message : void 0,
|
|
4684
|
-
locations: extractLocations(o)
|
|
4685
|
-
};
|
|
4686
|
-
});
|
|
4687
|
-
let completenessHint;
|
|
4688
|
-
const status = String(raw.status ?? raw.scan_status ?? "").toLowerCase();
|
|
4689
|
-
if (status && status !== "complete" && status !== "completed" && status !== "success") {
|
|
4690
|
-
degradedReasons.push(`SkillSpector reported status "${status}"`);
|
|
4691
|
-
completenessHint = status === "partial" ? "partial" : "degraded";
|
|
4692
|
-
}
|
|
4693
|
-
if (raw.partial === true) {
|
|
4694
|
-
degradedReasons.push("SkillSpector reported a partial scan");
|
|
4695
|
-
completenessHint = completenessHint ?? "partial";
|
|
4696
|
-
}
|
|
4697
|
-
if (raw.degraded === true) {
|
|
4698
|
-
degradedReasons.push("SkillSpector reported a degraded scan");
|
|
4699
|
-
completenessHint = "degraded";
|
|
4700
|
-
}
|
|
4701
|
-
const coverage = (arrayOf(raw.categories) ?? arrayOf(raw.coverage) ?? []).map((c) => String(c));
|
|
4702
|
-
return {
|
|
4703
|
-
provider: "skillspector",
|
|
4704
|
-
providerVersion: pinnedVersion(raw),
|
|
4705
|
-
scanMode,
|
|
4706
|
-
coverage,
|
|
4707
|
-
findings,
|
|
4708
|
-
degradedReasons,
|
|
4709
|
-
completenessHint
|
|
4710
|
-
};
|
|
4711
|
-
}
|
|
4712
|
-
function parseSkillSpectorSarif(parsed) {
|
|
4713
|
-
const raw = asObject(parsed, "SARIF root");
|
|
4714
|
-
const runs = arrayOf(raw.runs) ?? [];
|
|
4715
|
-
if (runs.length === 0) {
|
|
4716
|
-
return {
|
|
4717
|
-
provider: "skillspector",
|
|
4718
|
-
findings: [],
|
|
4719
|
-
degradedReasons: ["SARIF had no runs"]
|
|
4720
|
-
};
|
|
4721
|
-
}
|
|
4722
|
-
const degradedReasons = [];
|
|
4723
|
-
const findings = [];
|
|
4724
|
-
let providerVersion;
|
|
4725
|
-
const coverage = [];
|
|
4726
|
-
for (const runUnknown of runs) {
|
|
4727
|
-
const run2 = asObject(runUnknown, "SARIF run");
|
|
4728
|
-
const tool = asObject(run2.tool ?? {}, "SARIF tool");
|
|
4729
|
-
const driver = asObject(tool.driver ?? {}, "SARIF tool.driver");
|
|
4730
|
-
if (!providerVersion) {
|
|
4731
|
-
const v = typeof driver.semanticVersion === "string" && driver.semanticVersion || typeof driver.version === "string" && driver.version || "";
|
|
4732
|
-
if (v) providerVersion = v;
|
|
4733
|
-
}
|
|
4734
|
-
const results = arrayOf(run2.results) ?? [];
|
|
4735
|
-
for (const resUnknown of results) {
|
|
4736
|
-
const res = asObject(resUnknown, "SARIF result");
|
|
4737
|
-
findings.push({
|
|
4738
|
-
providerRuleId: String(res.ruleId ?? "unknown"),
|
|
4739
|
-
providerSeverity: String(res.level ?? "warning"),
|
|
4740
|
-
message: extractSarifMessage(res),
|
|
4741
|
-
locations: extractSarifLocations(res)
|
|
4742
|
-
});
|
|
4743
|
-
}
|
|
4744
|
-
}
|
|
4745
|
-
degradedReasons.push("SARIF import: provider-specific fields may be lost vs JSON form");
|
|
4746
|
-
return {
|
|
4747
|
-
provider: "skillspector",
|
|
4748
|
-
providerVersion,
|
|
4749
|
-
scanMode: "static",
|
|
4750
|
-
coverage,
|
|
4751
|
-
findings,
|
|
4752
|
-
degradedReasons
|
|
4753
|
-
};
|
|
4754
|
-
}
|
|
4755
|
-
function asObject(v, what) {
|
|
4756
|
-
if (v && typeof v === "object" && !Array.isArray(v)) return v;
|
|
4757
|
-
throw new Error(`expected object for ${what}`);
|
|
4758
|
-
}
|
|
4759
|
-
function arrayOf(v) {
|
|
4760
|
-
return Array.isArray(v) ? v : void 0;
|
|
4761
|
-
}
|
|
4762
|
-
function extractLocations(o) {
|
|
4763
|
-
const loc = o.location ?? o.locations ?? o.path;
|
|
4764
|
-
if (typeof loc === "string") return [loc];
|
|
4765
|
-
if (Array.isArray(loc)) return loc.map((x) => String(x));
|
|
4766
|
-
return void 0;
|
|
4767
|
-
}
|
|
4768
|
-
function extractSarifMessage(res) {
|
|
4769
|
-
const msg = res.message;
|
|
4770
|
-
if (msg && typeof msg.text === "string") return msg.text;
|
|
4771
|
-
return void 0;
|
|
4772
|
-
}
|
|
4773
|
-
function extractSarifLocations(res) {
|
|
4774
|
-
const locs = arrayOf(res.locations);
|
|
4775
|
-
if (!locs) return void 0;
|
|
4776
|
-
const out = [];
|
|
4777
|
-
for (const l of locs) {
|
|
4778
|
-
const lo = l;
|
|
4779
|
-
const phys = lo?.physicalLocation;
|
|
4780
|
-
const art = phys?.artifactLocation;
|
|
4781
|
-
const uri = art && typeof art.uri === "string" ? art.uri : void 0;
|
|
4782
|
-
const region = phys?.region;
|
|
4783
|
-
const line = region && typeof region.startLine === "number" ? region.startLine : void 0;
|
|
4784
|
-
if (uri) out.push(line ? `${uri}:${line}` : uri);
|
|
4785
|
-
}
|
|
4786
|
-
return out.length > 0 ? out : void 0;
|
|
4787
|
-
}
|
|
4788
|
-
|
|
4789
|
-
// ../../packages/evidence/src/importEvidence.ts
|
|
4790
|
-
var ZERO_DIGEST = `sha256:${"0".repeat(64)}`;
|
|
4791
|
-
function detectProvider(raw, explicit) {
|
|
4792
|
-
if (explicit) return explicit;
|
|
4793
|
-
const r = raw;
|
|
4794
|
-
if (!r) return "unknown";
|
|
4795
|
-
const topTool = r.tool;
|
|
4796
|
-
const jsonName = typeof r.scanner === "string" && r.scanner || topTool && typeof topTool.name === "string" && topTool.name || "";
|
|
4797
|
-
let sarifName = "";
|
|
4798
|
-
const runs = r.runs;
|
|
4799
|
-
if (Array.isArray(runs) && runs.length > 0) {
|
|
4800
|
-
const run2 = runs[0];
|
|
4801
|
-
const tool = run2?.tool;
|
|
4802
|
-
const driver = tool?.driver;
|
|
4803
|
-
if (driver && typeof driver.name === "string") sarifName = driver.name;
|
|
4804
|
-
}
|
|
4805
|
-
if (/skillspector/i.test(String(jsonName)) || /skillspector/i.test(sarifName)) {
|
|
4806
|
-
return "skillspector";
|
|
4807
|
-
}
|
|
4808
|
-
return "unknown";
|
|
4809
|
-
}
|
|
4810
|
-
function importEvidence(rawText, opts = {}) {
|
|
4811
|
-
const format = opts.format ?? (looksLikeSarif(rawText) ? "sarif" : "json");
|
|
4812
|
-
const rawReportDigest = sha256(rawText);
|
|
4813
|
-
let parsed;
|
|
4814
|
-
try {
|
|
4815
|
-
parsed = JSON.parse(rawText);
|
|
4816
|
-
} catch {
|
|
4817
|
-
return failClosed(opts.provider ?? "unknown", rawReportDigest, [
|
|
4818
|
-
`report is not valid JSON (format=${format})`
|
|
4819
|
-
]);
|
|
4837
|
+
|
|
4838
|
+
// ../../packages/report-renderer/src/renderTrustPacket.ts
|
|
4839
|
+
var COMPLETENESS_HINT = {
|
|
4840
|
+
complete: "complete",
|
|
4841
|
+
partial: "partial (incomplete \u2014 treat as inconclusive)",
|
|
4842
|
+
degraded: "degraded (not a pass)",
|
|
4843
|
+
failed: "failed (not a pass)"
|
|
4844
|
+
};
|
|
4845
|
+
function topProviderSeverity(ev) {
|
|
4846
|
+
const sevs = ev.findings.map(
|
|
4847
|
+
(f) => f && typeof f === "object" && "providerSeverity" in f ? String(f.providerSeverity) : void 0
|
|
4848
|
+
).filter((s) => Boolean(s));
|
|
4849
|
+
return sevs[0];
|
|
4850
|
+
}
|
|
4851
|
+
function whyTheyDiffer(ev, authorityVerdict) {
|
|
4852
|
+
if (ev.completeness === "failed" || ev.completeness === "degraded") {
|
|
4853
|
+
return "Why they differ: the content scan is " + COMPLETENESS_HINT[ev.completeness] + ", so it carries no weight here; CallLint's authority verdict stands on its own evidence.";
|
|
4820
4854
|
}
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
try {
|
|
4824
|
-
if (provider === "skillspector") {
|
|
4825
|
-
result = format === "sarif" ? parseSkillSpectorSarif(parsed) : parseSkillSpectorJson(parsed);
|
|
4826
|
-
} else {
|
|
4827
|
-
return failClosed(provider, rawReportDigest, [
|
|
4828
|
-
`no adapter for provider "${provider}"; evidence not interpreted`
|
|
4829
|
-
]);
|
|
4830
|
-
}
|
|
4831
|
-
} catch (err2) {
|
|
4832
|
-
return failClosed(provider, rawReportDigest, [
|
|
4833
|
-
`adapter error: ${err2.message}`
|
|
4834
|
-
]);
|
|
4855
|
+
if (ev.findings.length === 0 && authorityVerdict !== "SAFE") {
|
|
4856
|
+
return "Why they differ: the content scan found nothing malicious, but CallLint judges the granted authority itself too broad \u2014 clean code can still request unsafe capabilities.";
|
|
4835
4857
|
}
|
|
4836
|
-
return
|
|
4837
|
-
}
|
|
4838
|
-
function failClosed(provider, rawReportDigest, reasons) {
|
|
4839
|
-
return {
|
|
4840
|
-
schema_version: EVIDENCE_SCHEMA_VERSION,
|
|
4841
|
-
provider: provider || "unknown",
|
|
4842
|
-
providerVersion: "unknown",
|
|
4843
|
-
artifactDigest: ZERO_DIGEST,
|
|
4844
|
-
scanMode: "static",
|
|
4845
|
-
coverage: [],
|
|
4846
|
-
completeness: "failed",
|
|
4847
|
-
findings: [],
|
|
4848
|
-
rawReportDigest,
|
|
4849
|
-
degradedReasons: reasons
|
|
4850
|
-
};
|
|
4858
|
+
return "Why they differ: the two tools answer different questions \u2014 content risk (the scanner) vs. whether the requested authority is acceptable (CallLint). Neither overrides the other.";
|
|
4851
4859
|
}
|
|
4852
|
-
function
|
|
4853
|
-
const
|
|
4854
|
-
|
|
4855
|
-
const
|
|
4856
|
-
|
|
4857
|
-
|
|
4858
|
-
|
|
4859
|
-
|
|
4860
|
-
|
|
4861
|
-
|
|
4860
|
+
function renderTrustPacket(summary, toolVersion, style = DEFAULT_STYLE) {
|
|
4861
|
+
const evidence = summary.evidence;
|
|
4862
|
+
if (!evidence || evidence.length === 0) return "";
|
|
4863
|
+
const lines = ["", "Joint Trust Packet", "\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"];
|
|
4864
|
+
lines.push("Content scan");
|
|
4865
|
+
for (const ev of evidence) {
|
|
4866
|
+
const sev = topProviderSeverity(ev);
|
|
4867
|
+
const findingsLabel = ev.findings.length === 0 ? "no findings" : `${ev.findings.length} finding${ev.findings.length === 1 ? "" : "s"}` + (sev ? ` (top severity: ${sev})` : "");
|
|
4868
|
+
lines.push(
|
|
4869
|
+
` ${ev.provider} ${ev.providerVersion} scanMode: ${ev.scanMode} completeness: ${COMPLETENESS_HINT[ev.completeness]}`
|
|
4870
|
+
);
|
|
4871
|
+
lines.push(` ${findingsLabel}`);
|
|
4872
|
+
lines.push(` raw report digest: ${ev.rawReportDigest}`);
|
|
4873
|
+
for (const reason of ev.degradedReasons) {
|
|
4874
|
+
lines.push(` degraded: ${reason}`);
|
|
4875
|
+
}
|
|
4862
4876
|
}
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
scanMode: r.scanMode ?? "static",
|
|
4870
|
-
coverage: r.coverage ?? [],
|
|
4871
|
-
completeness,
|
|
4872
|
-
findings: r.findings,
|
|
4873
|
-
rawReportDigest,
|
|
4874
|
-
startedAt: r.startedAt,
|
|
4875
|
-
finishedAt: r.finishedAt,
|
|
4876
|
-
degradedReasons
|
|
4877
|
-
};
|
|
4878
|
-
}
|
|
4879
|
-
function looksLikeSarif(text) {
|
|
4880
|
-
return /"runs"\s*:/.test(text) && /sarif/i.test(text);
|
|
4877
|
+
lines.push("Authority scan");
|
|
4878
|
+
lines.push(
|
|
4879
|
+
` CallLint ${toolVersion} ${verdictTag(summary.verdict, style)} (${summary.publicVerdictLabel})`
|
|
4880
|
+
);
|
|
4881
|
+
lines.push(whyTheyDiffer(evidence[0], summary.verdict));
|
|
4882
|
+
return lines.join("\n");
|
|
4881
4883
|
}
|
|
4882
4884
|
|
|
4883
4885
|
// src/exitCode.ts
|
|
@@ -9893,7 +9895,15 @@ SEE ALSO
|
|
|
9893
9895
|
// src/commands/guard.ts
|
|
9894
9896
|
import { existsSync as existsSync12, readFileSync as readFileSync19, writeFileSync as writeFileSync11, mkdirSync as mkdirSync6 } from "node:fs";
|
|
9895
9897
|
import { dirname as dirname6, join as join17, resolve as resolve10 } from "node:path";
|
|
9896
|
-
var GUARD_HOSTS = [
|
|
9898
|
+
var GUARD_HOSTS = [
|
|
9899
|
+
"git",
|
|
9900
|
+
"git-pre-push",
|
|
9901
|
+
"github",
|
|
9902
|
+
"claude-code",
|
|
9903
|
+
"copilot",
|
|
9904
|
+
"gemini",
|
|
9905
|
+
"vscode"
|
|
9906
|
+
];
|
|
9897
9907
|
function guardConfigPath(cwd) {
|
|
9898
9908
|
return join17(cwd, ".calllint", "guard.json");
|
|
9899
9909
|
}
|
|
@@ -9989,31 +9999,86 @@ function guardRun(args, deps, env) {
|
|
|
9989
9999
|
function isGuardHost(v) {
|
|
9990
10000
|
return v !== void 0 && GUARD_HOSTS.includes(v);
|
|
9991
10001
|
}
|
|
9992
|
-
var
|
|
10002
|
+
var GUARD_CMD = "npx -y calllint guard --no-emoji";
|
|
10003
|
+
function gitHookBody(when) {
|
|
10004
|
+
return `#!/usr/bin/env bash
|
|
9993
10005
|
# CallLint Continuous Guard (ADR 0045). Re-decides the agent-tool authority
|
|
9994
|
-
# surface
|
|
10006
|
+
# surface ${when}; silent when nothing changed. Generated by \`calllint guard install\`.
|
|
9995
10007
|
# CallLint is static and NEVER executes a scanned server.
|
|
9996
|
-
|
|
10008
|
+
${GUARD_CMD}
|
|
9997
10009
|
`;
|
|
10010
|
+
}
|
|
10011
|
+
var CLAUDE_CODE_HOOK = JSON.stringify(
|
|
10012
|
+
{ hooks: { SessionStart: [{ hooks: [{ type: "command", command: GUARD_CMD }] }] } },
|
|
10013
|
+
null,
|
|
10014
|
+
2
|
|
10015
|
+
) + "\n";
|
|
10016
|
+
var GEMINI_HOOK = JSON.stringify(
|
|
10017
|
+
{
|
|
10018
|
+
hooks: {
|
|
10019
|
+
SessionStart: [
|
|
10020
|
+
{ hooks: [{ type: "command", command: GUARD_CMD, name: "calllint-guard" }] }
|
|
10021
|
+
]
|
|
10022
|
+
}
|
|
10023
|
+
},
|
|
10024
|
+
null,
|
|
10025
|
+
2
|
|
10026
|
+
) + "\n";
|
|
10027
|
+
var COPILOT_HOOK = JSON.stringify(
|
|
10028
|
+
{ version: 1, hooks: { sessionStart: [{ type: "command", command: GUARD_CMD }] } },
|
|
10029
|
+
null,
|
|
10030
|
+
2
|
|
10031
|
+
) + "\n";
|
|
10032
|
+
var VSCODE_TASK = JSON.stringify(
|
|
10033
|
+
{
|
|
10034
|
+
version: "2.0.0",
|
|
10035
|
+
tasks: [
|
|
10036
|
+
{
|
|
10037
|
+
label: "CallLint Continuous Guard",
|
|
10038
|
+
type: "shell",
|
|
10039
|
+
command: GUARD_CMD,
|
|
10040
|
+
runOptions: { runOn: "folderOpen" },
|
|
10041
|
+
presentation: { reveal: "silent", panel: "shared" },
|
|
10042
|
+
problemMatcher: []
|
|
10043
|
+
}
|
|
10044
|
+
]
|
|
10045
|
+
},
|
|
10046
|
+
null,
|
|
10047
|
+
2
|
|
10048
|
+
) + "\n";
|
|
9998
10049
|
function guardArtifact(host) {
|
|
9999
|
-
|
|
10000
|
-
|
|
10050
|
+
switch (host) {
|
|
10051
|
+
case "git":
|
|
10052
|
+
return { path: join17(".git", "hooks", "pre-commit"), content: gitHookBody("on commit"), label: "git pre-commit hook", posture: "dedicated" };
|
|
10053
|
+
case "git-pre-push":
|
|
10054
|
+
return { path: join17(".git", "hooks", "pre-push"), content: gitHookBody("on push"), label: "git pre-push hook", posture: "dedicated" };
|
|
10055
|
+
case "github":
|
|
10056
|
+
return { path: join17(".github", "workflows", "calllint.yml"), content: renderCiGate({ mode: "drift" }), label: "GitHub Actions drift-gate workflow", posture: "dedicated" };
|
|
10057
|
+
case "copilot":
|
|
10058
|
+
return { path: join17(".github", "hooks", "calllint.json"), content: COPILOT_HOOK, label: "Copilot CLI sessionStart hook", posture: "dedicated" };
|
|
10059
|
+
case "claude-code":
|
|
10060
|
+
return { path: join17(".claude", "settings.json"), content: CLAUDE_CODE_HOOK, label: "Claude Code SessionStart hook", posture: "shared" };
|
|
10061
|
+
case "gemini":
|
|
10062
|
+
return { path: join17(".gemini", "settings.json"), content: GEMINI_HOOK, label: "Gemini CLI SessionStart hook", posture: "shared" };
|
|
10063
|
+
case "vscode":
|
|
10064
|
+
return { path: join17(".vscode", "tasks.json"), content: VSCODE_TASK, label: "VS Code folderOpen guard task", posture: "shared" };
|
|
10001
10065
|
}
|
|
10002
|
-
return {
|
|
10003
|
-
path: join17(".github", "workflows", "calllint.yml"),
|
|
10004
|
-
content: renderCiGate({ mode: "drift" }),
|
|
10005
|
-
label: "GitHub Actions drift-gate workflow"
|
|
10006
|
-
};
|
|
10007
10066
|
}
|
|
10008
10067
|
function guardInstall(args, deps) {
|
|
10009
10068
|
const host = flagStr(args.flags, "host");
|
|
10010
10069
|
if (!host) {
|
|
10011
|
-
const list = GUARD_HOSTS.map((h2) =>
|
|
10070
|
+
const list = GUARD_HOSTS.map((h2) => {
|
|
10071
|
+
const a = guardArtifact(h2);
|
|
10072
|
+
const tag = a.posture === "shared" ? " (fragment)" : "";
|
|
10073
|
+
return ` ${h2.padEnd(13)} \u2192 ${a.label}${tag}`;
|
|
10074
|
+
}).join("\n");
|
|
10012
10075
|
return {
|
|
10013
10076
|
stdout: `Usage: calllint guard install --host <host>
|
|
10014
10077
|
|
|
10015
10078
|
Hosts:
|
|
10016
|
-
${list}
|
|
10079
|
+
${list}
|
|
10080
|
+
|
|
10081
|
+
(fragment) hosts live inside a shared config file; install prints a snippet to merge and never overwrites it.`,
|
|
10017
10082
|
exitCode: EXIT.OK
|
|
10018
10083
|
};
|
|
10019
10084
|
}
|
|
@@ -10026,7 +10091,39 @@ Run \`calllint guard install\` to list hosts.`,
|
|
|
10026
10091
|
};
|
|
10027
10092
|
}
|
|
10028
10093
|
const art = guardArtifact(host);
|
|
10029
|
-
const
|
|
10094
|
+
const outFlag = flagStr(args.flags, "out");
|
|
10095
|
+
const rel = outFlag ?? art.path;
|
|
10096
|
+
if (art.posture === "shared") {
|
|
10097
|
+
if (outFlag) {
|
|
10098
|
+
const abs2 = resolve10(deps.cwd, rel);
|
|
10099
|
+
if (existsSync12(abs2)) {
|
|
10100
|
+
return {
|
|
10101
|
+
stdout: "",
|
|
10102
|
+
stderr: `Refusing to overwrite ${rel} \u2014 CallLint will not clobber a shared config file.
|
|
10103
|
+
Merge this fragment into it instead:
|
|
10104
|
+
|
|
10105
|
+
${art.content}`,
|
|
10106
|
+
exitCode: EXIT.USAGE
|
|
10107
|
+
};
|
|
10108
|
+
}
|
|
10109
|
+
if (deps.writeFile === false) {
|
|
10110
|
+
return { stdout: `Would write ${rel} (${art.label})`, exitCode: EXIT.OK };
|
|
10111
|
+
}
|
|
10112
|
+
try {
|
|
10113
|
+
mkdirSync6(dirname6(abs2), { recursive: true });
|
|
10114
|
+
writeFileSync11(abs2, art.content, "utf8");
|
|
10115
|
+
} catch (e) {
|
|
10116
|
+
return { stdout: "", stderr: `Failed to write ${rel}: ${e instanceof Error ? e.message : String(e)}`, exitCode: EXIT.ERROR };
|
|
10117
|
+
}
|
|
10118
|
+
return { stdout: `Wrote ${rel} (${art.label})`, exitCode: EXIT.OK };
|
|
10119
|
+
}
|
|
10120
|
+
return {
|
|
10121
|
+
stdout: `${art.label} \u2014 merge this into ${rel} (a shared config file CallLint will not overwrite):
|
|
10122
|
+
|
|
10123
|
+
${art.content}`,
|
|
10124
|
+
exitCode: EXIT.OK
|
|
10125
|
+
};
|
|
10126
|
+
}
|
|
10030
10127
|
if (deps.writeFile === false) {
|
|
10031
10128
|
return { stdout: `Would write ${rel} (${art.label})`, exitCode: EXIT.OK };
|
|
10032
10129
|
}
|
|
@@ -10041,7 +10138,7 @@ Run \`calllint guard install\` to list hosts.`,
|
|
|
10041
10138
|
exitCode: EXIT.ERROR
|
|
10042
10139
|
};
|
|
10043
10140
|
}
|
|
10044
|
-
const note = host === "git" ? `
|
|
10141
|
+
const note = host === "git" || host === "git-pre-push" ? `
|
|
10045
10142
|
Make it executable: chmod +x ${rel}` : "";
|
|
10046
10143
|
return { stdout: `Wrote ${rel} (${art.label})${note}`, exitCode: EXIT.OK };
|
|
10047
10144
|
}
|
|
@@ -10049,16 +10146,26 @@ function guardStatus(args, deps, env) {
|
|
|
10049
10146
|
const approvedPath = flagStr(args.flags, "approved") ?? defaultApprovedPath(deps.cwd);
|
|
10050
10147
|
const hasBaseline = existsSync12(approvedPath);
|
|
10051
10148
|
const disabled = isDisabled(deps.cwd, env);
|
|
10052
|
-
const
|
|
10053
|
-
|
|
10149
|
+
const HOST_STATUS_KEY = {
|
|
10150
|
+
git: "git:pre-commit",
|
|
10151
|
+
"git-pre-push": "git:pre-push",
|
|
10152
|
+
github: "github:workflow",
|
|
10153
|
+
copilot: "copilot:sessionStart",
|
|
10154
|
+
"claude-code": "claude-code:sessionStart",
|
|
10155
|
+
gemini: "gemini:sessionStart",
|
|
10156
|
+
vscode: "vscode:folderOpen"
|
|
10157
|
+
};
|
|
10158
|
+
const installedHooks = {};
|
|
10159
|
+
for (const host of GUARD_HOSTS) {
|
|
10160
|
+
const art = guardArtifact(host);
|
|
10161
|
+
const abs = join17(deps.cwd, art.path);
|
|
10162
|
+
installedHooks[HOST_STATUS_KEY[host]] = hookInstalled(abs, art.posture);
|
|
10163
|
+
}
|
|
10054
10164
|
const status = {
|
|
10055
10165
|
enabled: !disabled,
|
|
10056
10166
|
disabledBy: disabled ? env["CALLLINT_GUARD"] === "0" ? "env:CALLLINT_GUARD=0" : "flag:.calllint/guard.json" : null,
|
|
10057
10167
|
approvedBaseline: hasBaseline ? approvedPath : null,
|
|
10058
|
-
installedHooks
|
|
10059
|
-
"git:pre-commit": gitHook,
|
|
10060
|
-
"github:workflow": ghWorkflow
|
|
10061
|
-
}
|
|
10168
|
+
installedHooks
|
|
10062
10169
|
};
|
|
10063
10170
|
if (flagBool(args.flags, "json")) {
|
|
10064
10171
|
return { stdout: JSON.stringify(status), exitCode: EXIT.OK };
|
|
@@ -10066,11 +10173,21 @@ function guardStatus(args, deps, env) {
|
|
|
10066
10173
|
const lines = [
|
|
10067
10174
|
`Continuous Guard: ${status.enabled ? "enabled" : `disabled (${status.disabledBy})`}`,
|
|
10068
10175
|
`Approved baseline: ${hasBaseline ? approvedPath : "none \u2014 run `calllint approve`"}`,
|
|
10069
|
-
|
|
10070
|
-
|
|
10176
|
+
...GUARD_HOSTS.map(
|
|
10177
|
+
(h2) => `${HOST_STATUS_KEY[h2]}: ${installedHooks[HOST_STATUS_KEY[h2]] ? "installed" : "not installed"}`
|
|
10178
|
+
)
|
|
10071
10179
|
];
|
|
10072
10180
|
return { stdout: lines.join("\n"), exitCode: EXIT.OK };
|
|
10073
10181
|
}
|
|
10182
|
+
function hookInstalled(abs, posture) {
|
|
10183
|
+
if (!existsSync12(abs)) return false;
|
|
10184
|
+
if (posture === "dedicated") return true;
|
|
10185
|
+
try {
|
|
10186
|
+
return readFileSync19(abs, "utf8").includes("calllint guard");
|
|
10187
|
+
} catch {
|
|
10188
|
+
return false;
|
|
10189
|
+
}
|
|
10190
|
+
}
|
|
10074
10191
|
function guardSetEnabled(deps, enabled) {
|
|
10075
10192
|
const cfg = { schemaVersion: "calllint.guard-config.v0", enabled };
|
|
10076
10193
|
if (deps.writeFile === false) {
|
|
@@ -10093,6 +10210,291 @@ function guardSetEnabled(deps, enabled) {
|
|
|
10093
10210
|
};
|
|
10094
10211
|
}
|
|
10095
10212
|
|
|
10213
|
+
// src/commands/integrate.ts
|
|
10214
|
+
import { existsSync as existsSync13, readFileSync as readFileSync20, writeFileSync as writeFileSync12, mkdirSync as mkdirSync7 } from "node:fs";
|
|
10215
|
+
import { homedir as homedir2 } from "node:os";
|
|
10216
|
+
import { join as join18, resolve as resolve11 } from "node:path";
|
|
10217
|
+
|
|
10218
|
+
// ../../packages/agent-triggers/src/overlays.ts
|
|
10219
|
+
var PLATFORM_OVERLAYS = {
|
|
10220
|
+
"claude-code": {
|
|
10221
|
+
host: "claude-code",
|
|
10222
|
+
displayName: "Claude Code",
|
|
10223
|
+
channels: ["plugin-hook", "cli-recommend"],
|
|
10224
|
+
supportsRuntimeHook: true
|
|
10225
|
+
},
|
|
10226
|
+
cursor: {
|
|
10227
|
+
host: "cursor",
|
|
10228
|
+
displayName: "Cursor",
|
|
10229
|
+
channels: ["cli-recommend", "ide-diagnostic"],
|
|
10230
|
+
supportsRuntimeHook: false
|
|
10231
|
+
},
|
|
10232
|
+
windsurf: {
|
|
10233
|
+
host: "windsurf",
|
|
10234
|
+
displayName: "Windsurf",
|
|
10235
|
+
channels: ["cli-recommend", "ide-diagnostic"],
|
|
10236
|
+
supportsRuntimeHook: false
|
|
10237
|
+
},
|
|
10238
|
+
"claude-desktop": {
|
|
10239
|
+
host: "claude-desktop",
|
|
10240
|
+
displayName: "Claude Desktop",
|
|
10241
|
+
channels: ["cli-recommend"],
|
|
10242
|
+
supportsRuntimeHook: false
|
|
10243
|
+
},
|
|
10244
|
+
vscode: {
|
|
10245
|
+
host: "vscode",
|
|
10246
|
+
displayName: "VS Code",
|
|
10247
|
+
channels: ["cli-recommend", "ide-diagnostic"],
|
|
10248
|
+
supportsRuntimeHook: false
|
|
10249
|
+
}
|
|
10250
|
+
};
|
|
10251
|
+
function overlayForHost(host) {
|
|
10252
|
+
return PLATFORM_OVERLAYS[host] ?? null;
|
|
10253
|
+
}
|
|
10254
|
+
|
|
10255
|
+
// src/commands/integrate.ts
|
|
10256
|
+
var CALLLINT_MCP_SERVER_NAME = "calllint";
|
|
10257
|
+
function calllintMcpEntry() {
|
|
10258
|
+
return { command: "npx", args: ["-y", "calllint-mcp"] };
|
|
10259
|
+
}
|
|
10260
|
+
function integrableHosts() {
|
|
10261
|
+
return Object.values(HOST_ADAPTERS).filter((a) => typeof a.applyPlan === "function").map((a) => a.id).sort();
|
|
10262
|
+
}
|
|
10263
|
+
function decideForIntegrate(authority) {
|
|
10264
|
+
const policy = loadPolicyOrDefault();
|
|
10265
|
+
const flows = buildFlows([authority]);
|
|
10266
|
+
const flowReasons = foldFlowsIntoReasons(flows);
|
|
10267
|
+
return decideOverAuthority({ authority, policy, flowReasons });
|
|
10268
|
+
}
|
|
10269
|
+
function integrateCommand(args, deps) {
|
|
10270
|
+
const sub = args.positionals[0];
|
|
10271
|
+
if (sub === "help") return integrateHelp();
|
|
10272
|
+
if (sub !== void 0) {
|
|
10273
|
+
return usage(`Unexpected argument "${sub}". Run \`calllint integrate help\`.`);
|
|
10274
|
+
}
|
|
10275
|
+
const apply = flagBool(args.flags, "apply");
|
|
10276
|
+
return apply ? integrateApply(args, deps) : integratePlan(args, deps);
|
|
10277
|
+
}
|
|
10278
|
+
function integratePlan(args, deps) {
|
|
10279
|
+
const only = flagStr(args.flags, "host");
|
|
10280
|
+
const hosts = only ? [only] : integrableHosts();
|
|
10281
|
+
const detected = detectHosts(deps.cwd, hosts);
|
|
10282
|
+
const hostPlans = hosts.map((h2) => planForHost(h2, detected, deps));
|
|
10283
|
+
const written = /* @__PURE__ */ new Map();
|
|
10284
|
+
if (flagBool(args.flags, "write-plan") && deps.write !== false) {
|
|
10285
|
+
for (const hp of hostPlans) {
|
|
10286
|
+
if (hp.plan) written.set(hp.host, writePlanFile2(hp.plan, deps.cwd));
|
|
10287
|
+
}
|
|
10288
|
+
}
|
|
10289
|
+
if (flagBool(args.flags, "json")) {
|
|
10290
|
+
const payload = {
|
|
10291
|
+
schema: "calllint.integrate-plan.v0",
|
|
10292
|
+
generatedAt: deps.generatedAt,
|
|
10293
|
+
hosts: hostPlans.map((hp) => ({
|
|
10294
|
+
host: hp.host,
|
|
10295
|
+
configPath: hp.configPath,
|
|
10296
|
+
planDigest: hp.plan?.planDigest ?? null,
|
|
10297
|
+
operations: hp.plan?.operations.length ?? 0,
|
|
10298
|
+
planFile: written.get(hp.host) ?? null,
|
|
10299
|
+
note: hp.note
|
|
10300
|
+
}))
|
|
10301
|
+
};
|
|
10302
|
+
return { stdout: JSON.stringify(payload, null, 2), stderr: "", exitCode: EXIT.OK };
|
|
10303
|
+
}
|
|
10304
|
+
return { stdout: renderPlans(hostPlans, written), stderr: "", exitCode: EXIT.OK };
|
|
10305
|
+
}
|
|
10306
|
+
function writePlanFile2(plan, cwd) {
|
|
10307
|
+
const planDir = join18(cwd, ".calllint", "plans");
|
|
10308
|
+
mkdirSync7(planDir, { recursive: true });
|
|
10309
|
+
const file = join18(planDir, `${plan.planId}.json`);
|
|
10310
|
+
writeFileSync12(file, JSON.stringify(plan, null, 2) + "\n", "utf8");
|
|
10311
|
+
return file;
|
|
10312
|
+
}
|
|
10313
|
+
function detectHosts(cwd, hosts) {
|
|
10314
|
+
const result = discoverConfigs({ cwd, agentTypes: hosts, includeMissing: true });
|
|
10315
|
+
const cwdPrefix = resolve11(cwd);
|
|
10316
|
+
const byHost = /* @__PURE__ */ new Map();
|
|
10317
|
+
for (const d of result.discovered) {
|
|
10318
|
+
if (!resolve11(d.configPath).startsWith(cwdPrefix)) continue;
|
|
10319
|
+
const prior = byHost.get(d.agentType);
|
|
10320
|
+
if (!prior || !prior.exists && d.exists) byHost.set(d.agentType, d);
|
|
10321
|
+
}
|
|
10322
|
+
return byHost;
|
|
10323
|
+
}
|
|
10324
|
+
function planForHost(host, detected, deps) {
|
|
10325
|
+
const adapter = getHostAdapter(host);
|
|
10326
|
+
const overlay = overlayForHost(host);
|
|
10327
|
+
const discovered = detected.get(host);
|
|
10328
|
+
const configFromDiscovery = discovered?.configPath;
|
|
10329
|
+
const configPath = configFromDiscovery ?? "(no default config path)";
|
|
10330
|
+
if (!adapter || !adapter.applyPlan) {
|
|
10331
|
+
return { host, configPath, plan: null, note: `host "${host}" has no audited writer (plan-only elsewhere)` };
|
|
10332
|
+
}
|
|
10333
|
+
if (!configFromDiscovery || !discovered?.exists) {
|
|
10334
|
+
return { host, configPath, plan: null, note: `host "${host}" not detected on this machine` };
|
|
10335
|
+
}
|
|
10336
|
+
let currentConfig = null;
|
|
10337
|
+
let configDigest = "absent";
|
|
10338
|
+
if (existsSync13(configFromDiscovery)) {
|
|
10339
|
+
try {
|
|
10340
|
+
const bytes = readFileSync20(configFromDiscovery, "utf8");
|
|
10341
|
+
configDigest = hashJson(bytes);
|
|
10342
|
+
currentConfig = JSON.parse(bytes);
|
|
10343
|
+
} catch {
|
|
10344
|
+
return { host, configPath, plan: null, note: `host config exists but is not valid JSON: ${configFromDiscovery}` };
|
|
10345
|
+
}
|
|
10346
|
+
}
|
|
10347
|
+
if (serverAlreadyPresent(currentConfig)) {
|
|
10348
|
+
return { host, configPath, plan: null, note: "already integrated (calllint server present) \u2014 no change" };
|
|
10349
|
+
}
|
|
10350
|
+
const calllintServer = {
|
|
10351
|
+
name: CALLLINT_MCP_SERVER_NAME,
|
|
10352
|
+
sourceConfigPath: configFromDiscovery,
|
|
10353
|
+
transport: "stdio",
|
|
10354
|
+
command: "npx",
|
|
10355
|
+
args: ["-y", "calllint-mcp"],
|
|
10356
|
+
envKeys: [],
|
|
10357
|
+
env: {},
|
|
10358
|
+
providedTools: [],
|
|
10359
|
+
raw: calllintMcpEntry()
|
|
10360
|
+
};
|
|
10361
|
+
const authority = buildAuthorityManifest({ artifactDigest: null, servers: [calllintServer], surfaces: [] });
|
|
10362
|
+
const decision = decideForIntegrate(authority);
|
|
10363
|
+
const backupPath = `${configFromDiscovery}.calllint-backup`;
|
|
10364
|
+
const expiresAt = planExpiry2(deps.generatedAt);
|
|
10365
|
+
const ctx = {
|
|
10366
|
+
host,
|
|
10367
|
+
tier: adapter.tier,
|
|
10368
|
+
configPath: configFromDiscovery,
|
|
10369
|
+
configDigest,
|
|
10370
|
+
currentConfig,
|
|
10371
|
+
servers: [{ name: CALLLINT_MCP_SERVER_NAME, entry: calllintMcpEntry() }],
|
|
10372
|
+
backupPath,
|
|
10373
|
+
expiresAt
|
|
10374
|
+
};
|
|
10375
|
+
const plan = adapter.createPlan(ctx, { artifactDigest: null, authority, decision });
|
|
10376
|
+
return { host, configPath, plan, note: null };
|
|
10377
|
+
}
|
|
10378
|
+
function serverAlreadyPresent(config) {
|
|
10379
|
+
if (!config || typeof config !== "object") return false;
|
|
10380
|
+
const servers = config.mcpServers;
|
|
10381
|
+
if (!servers || typeof servers !== "object") return false;
|
|
10382
|
+
return Object.prototype.hasOwnProperty.call(servers, CALLLINT_MCP_SERVER_NAME);
|
|
10383
|
+
}
|
|
10384
|
+
function integrateApply(args, deps) {
|
|
10385
|
+
const planFile = flagStr(args.flags, "plan");
|
|
10386
|
+
const approve = flagStr(args.flags, "approve");
|
|
10387
|
+
if (!planFile) return usage("Missing --plan <file>\nUsage: calllint integrate --apply --plan <plan.json> --approve <plan-digest>");
|
|
10388
|
+
if (!approve) return usage("Missing --approve <plan-digest>\nApproval must name the exact plan digest you reviewed.");
|
|
10389
|
+
let plan;
|
|
10390
|
+
try {
|
|
10391
|
+
plan = JSON.parse(readFileSync20(resolve11(deps.cwd, planFile), "utf8"));
|
|
10392
|
+
} catch (err2) {
|
|
10393
|
+
const e = err2;
|
|
10394
|
+
return { stdout: "", stderr: e.code === "ENOENT" ? `Plan file not found: ${planFile}` : `Plan file is not valid JSON: ${e.message}`, exitCode: EXIT.ERROR };
|
|
10395
|
+
}
|
|
10396
|
+
if (plan?.schema !== "calllint.install-plan.v1") {
|
|
10397
|
+
return { stdout: "", stderr: `Not a calllint.install-plan.v1 document: ${planFile}`, exitCode: EXIT.ERROR };
|
|
10398
|
+
}
|
|
10399
|
+
if (!verifyPlanDigest(plan)) {
|
|
10400
|
+
return { stdout: "", stderr: "Plan digest does not match its contents (tampered or hand-edited). Re-run `calllint integrate` to regenerate.", exitCode: EXIT.ERROR };
|
|
10401
|
+
}
|
|
10402
|
+
const adapter = getHostAdapter(plan.host);
|
|
10403
|
+
if (!adapter) return usage(`Unknown host "${plan.host}".`);
|
|
10404
|
+
if (!adapter.applyPlan) return usage(`Host "${plan.host}" is tier ${adapter.tier} \u2014 plan-only; cannot apply.`);
|
|
10405
|
+
const rawTarget = plan.operations[0]?.target;
|
|
10406
|
+
if (!rawTarget) return usage("Plan has no operations to apply.");
|
|
10407
|
+
let configPath;
|
|
10408
|
+
try {
|
|
10409
|
+
configPath = safeConfigPath(rawTarget, { cwd: deps.cwd, home: homedir2() });
|
|
10410
|
+
} catch (err2) {
|
|
10411
|
+
if (err2 instanceof PathSafetyError) return { stdout: "", stderr: `Unsafe target path in plan: ${err2.message}`, exitCode: EXIT.ERROR };
|
|
10412
|
+
throw err2;
|
|
10413
|
+
}
|
|
10414
|
+
const rid = shortReceiptId(plan.planDigest, deps.generatedAt);
|
|
10415
|
+
const backupPath = `${configPath}.calllint-backup-${rid}`;
|
|
10416
|
+
const lockPath = join18(deps.cwd, ".calllint", "locks", `${hashJson(configPath).slice("sha256:".length, "sha256:".length + 16)}.lock`);
|
|
10417
|
+
const result = adapter.applyPlan(plan, {
|
|
10418
|
+
approvalDigest: approve,
|
|
10419
|
+
configPath,
|
|
10420
|
+
backupPath,
|
|
10421
|
+
lockPath,
|
|
10422
|
+
fs: nodeFsPort(),
|
|
10423
|
+
now: deps.generatedAt
|
|
10424
|
+
});
|
|
10425
|
+
if (flagBool(args.flags, "json")) {
|
|
10426
|
+
return { stdout: JSON.stringify(result, null, 2), stderr: "", exitCode: applyExitCode2(result) };
|
|
10427
|
+
}
|
|
10428
|
+
return { stdout: renderApply(result), stderr: "", exitCode: applyExitCode2(result) };
|
|
10429
|
+
}
|
|
10430
|
+
function planExpiry2(generatedAt) {
|
|
10431
|
+
const ms = Date.parse(generatedAt);
|
|
10432
|
+
if (Number.isNaN(ms)) return generatedAt;
|
|
10433
|
+
return new Date(ms + 30 * 24 * 60 * 60 * 1e3).toISOString();
|
|
10434
|
+
}
|
|
10435
|
+
function shortReceiptId(planDigest, generatedAt) {
|
|
10436
|
+
return hashJson({ planDigest, generatedAt }).slice("sha256:".length, "sha256:".length + 12);
|
|
10437
|
+
}
|
|
10438
|
+
function applyExitCode2(result) {
|
|
10439
|
+
switch (result.outcome) {
|
|
10440
|
+
case "applied":
|
|
10441
|
+
case "already_applied":
|
|
10442
|
+
return EXIT.OK;
|
|
10443
|
+
case "stale":
|
|
10444
|
+
case "conflict":
|
|
10445
|
+
return EXIT.ERROR;
|
|
10446
|
+
case "rolled_back":
|
|
10447
|
+
case "rollback_failed":
|
|
10448
|
+
return EXIT.ERROR;
|
|
10449
|
+
}
|
|
10450
|
+
}
|
|
10451
|
+
function renderPlans(hostPlans, written) {
|
|
10452
|
+
const lines = ["CallLint integrate \u2014 preflight install plan (read-only; nothing written)\n"];
|
|
10453
|
+
for (const hp of hostPlans) {
|
|
10454
|
+
if (hp.plan) {
|
|
10455
|
+
lines.push(` ${hp.host}: ${hp.plan.operations.length} op(s) \u2192 ${hp.configPath}`);
|
|
10456
|
+
lines.push(` plan digest: ${hp.plan.planDigest}`);
|
|
10457
|
+
const planRef = written.get(hp.host) ?? "<saved-plan.json>";
|
|
10458
|
+
lines.push(` apply with: calllint integrate --apply --plan ${planRef} --approve ${hp.plan.planDigest}`);
|
|
10459
|
+
} else {
|
|
10460
|
+
lines.push(` ${hp.host}: no change \u2014 ${hp.note}`);
|
|
10461
|
+
}
|
|
10462
|
+
}
|
|
10463
|
+
lines.push("\nInstalling the CallLint preflight server does not block your agent (ADR 0051); it recommends. CallLint never executes the servers it judges.");
|
|
10464
|
+
return lines.join("\n") + "\n";
|
|
10465
|
+
}
|
|
10466
|
+
function renderApply(result) {
|
|
10467
|
+
const head = `integrate apply: ${result.outcome} (${result.state}) \u2014 host ${result.host}`;
|
|
10468
|
+
const trail = result.notes.map((n) => ` ${n}`).join("\n");
|
|
10469
|
+
const rb = result.rolledBack ? "\n rolled back to the original config (verify failed)." : "";
|
|
10470
|
+
return `${head}
|
|
10471
|
+
${trail}${rb}
|
|
10472
|
+
`;
|
|
10473
|
+
}
|
|
10474
|
+
function integrateHelp() {
|
|
10475
|
+
return {
|
|
10476
|
+
stdout: [
|
|
10477
|
+
"calllint integrate \u2014 install the CallLint preflight server into your agent hosts",
|
|
10478
|
+
"",
|
|
10479
|
+
" calllint integrate detect hosts + print an install plan (read-only)",
|
|
10480
|
+
" calllint integrate --host cursor plan for one host only",
|
|
10481
|
+
" calllint integrate --write-plan persist each plan to .calllint/plans/<id>.json",
|
|
10482
|
+
" calllint integrate --json machine-readable plan",
|
|
10483
|
+
" calllint integrate --apply --plan <p.json> --approve <digest>",
|
|
10484
|
+
" apply an approved plan (atomic, verified, auto-rollback)",
|
|
10485
|
+
"",
|
|
10486
|
+
"Idempotent: a host that already has the calllint server yields no change.",
|
|
10487
|
+
"Plan-only by default; --apply is the only writer and reuses the audited",
|
|
10488
|
+
"trust-apply engine. Installing the preflight does not block your agent (ADR 0051)."
|
|
10489
|
+
].join("\n") + "\n",
|
|
10490
|
+
stderr: "",
|
|
10491
|
+
exitCode: EXIT.OK
|
|
10492
|
+
};
|
|
10493
|
+
}
|
|
10494
|
+
function usage(msg) {
|
|
10495
|
+
return { stdout: "", stderr: msg, exitCode: EXIT.USAGE };
|
|
10496
|
+
}
|
|
10497
|
+
|
|
10096
10498
|
// src/run.ts
|
|
10097
10499
|
function run(argv, deps) {
|
|
10098
10500
|
const args = parseArgs(argv);
|
|
@@ -10171,6 +10573,8 @@ function run(argv, deps) {
|
|
|
10171
10573
|
return evidenceCommand(args, { cwd: deps.cwd });
|
|
10172
10574
|
case "trust":
|
|
10173
10575
|
return trustCommand(args, { cwd: deps.cwd, generatedAt: deps.generatedAt, toolVersion: deps.toolVersion });
|
|
10576
|
+
case "integrate":
|
|
10577
|
+
return integrateCommand(args, { cwd: deps.cwd, generatedAt: deps.generatedAt, toolVersion: deps.toolVersion });
|
|
10174
10578
|
case "guard":
|
|
10175
10579
|
return guardCommand(args, {
|
|
10176
10580
|
cwd: deps.cwd,
|
|
@@ -10451,13 +10855,13 @@ async function breathe(argv, deps = {}) {
|
|
|
10451
10855
|
}
|
|
10452
10856
|
|
|
10453
10857
|
// src/version.ts
|
|
10454
|
-
import { readFileSync as
|
|
10858
|
+
import { readFileSync as readFileSync21 } from "node:fs";
|
|
10455
10859
|
import { fileURLToPath } from "node:url";
|
|
10456
|
-
import { dirname as dirname7, join as
|
|
10860
|
+
import { dirname as dirname7, join as join19 } from "node:path";
|
|
10457
10861
|
function resolveToolVersion() {
|
|
10458
10862
|
try {
|
|
10459
|
-
const pkgPath =
|
|
10460
|
-
const pkg = JSON.parse(
|
|
10863
|
+
const pkgPath = join19(dirname7(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
10864
|
+
const pkg = JSON.parse(readFileSync21(pkgPath, "utf8"));
|
|
10461
10865
|
return typeof pkg.version === "string" && pkg.version.length > 0 ? pkg.version : "unknown";
|
|
10462
10866
|
} catch {
|
|
10463
10867
|
return "unknown";
|
|
@@ -10467,7 +10871,7 @@ function resolveToolVersion() {
|
|
|
10467
10871
|
// src/index.ts
|
|
10468
10872
|
function readStdin() {
|
|
10469
10873
|
try {
|
|
10470
|
-
return
|
|
10874
|
+
return readFileSync22(0, "utf8");
|
|
10471
10875
|
} catch {
|
|
10472
10876
|
return "";
|
|
10473
10877
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "calllint",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.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",
|
|
@@ -50,6 +50,7 @@
|
|
|
50
50
|
"dependencies": {},
|
|
51
51
|
"devDependencies": {
|
|
52
52
|
"@calllint/action-analyzer": "workspace:*",
|
|
53
|
+
"@calllint/agent-triggers": "workspace:*",
|
|
53
54
|
"@calllint/core": "workspace:*",
|
|
54
55
|
"@calllint/discovery": "workspace:*",
|
|
55
56
|
"@calllint/evidence": "workspace:*",
|