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.
Files changed (2) hide show
  1. package/dist/index.js +699 -295
  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 readFileSync21 } from "node:fs";
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-commit, github workflow)
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 (pre-commit) | github (workflow)
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
- const provider = detectProvider(parsed, opts.provider);
4822
- let result;
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 finalizeEnvelope(result, rawReportDigest);
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 finalizeEnvelope(r, rawReportDigest) {
4853
- const degradedReasons = [...r.degradedReasons ?? []];
4854
- const rank = { complete: 0, partial: 1, degraded: 2, failed: 3 };
4855
- const rankToCompleteness = ["complete", "partial", "degraded", "failed"];
4856
- let level = rank[r.completenessHint ?? (degradedReasons.length > 0 ? "degraded" : "complete")];
4857
- let providerVersion = r.providerVersion?.trim() || "";
4858
- if (!providerVersion) {
4859
- providerVersion = "unknown";
4860
- degradedReasons.push("provider version not pinned (no release/commit reported)");
4861
- level = Math.max(level, rank.degraded);
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
- const completeness = rankToCompleteness[level];
4864
- return {
4865
- schema_version: EVIDENCE_SCHEMA_VERSION,
4866
- provider: r.provider,
4867
- providerVersion,
4868
- artifactDigest: r.artifactDigest ?? ZERO_DIGEST,
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 = ["git", "github"];
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 GIT_HOOK = `#!/usr/bin/env bash
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 on commit; silent when nothing changed. Generated by \`calllint guard install\`.
10006
+ # surface ${when}; silent when nothing changed. Generated by \`calllint guard install\`.
9995
10007
  # CallLint is static and NEVER executes a scanned server.
9996
- npx -y calllint guard --no-emoji
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
- if (host === "git") {
10000
- return { path: join17(".git", "hooks", "pre-commit"), content: GIT_HOOK, label: "git pre-commit hook" };
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) => ` ${h2.padEnd(8)} \u2192 ${guardArtifact(h2).label}`).join("\n");
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 rel = flagStr(args.flags, "out") ?? art.path;
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 gitHook = existsSync12(join17(deps.cwd, ".git", "hooks", "pre-commit"));
10053
- const ghWorkflow = existsSync12(join17(deps.cwd, ".github", "workflows", "calllint.yml"));
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
- `git pre-commit hook: ${gitHook ? "installed" : "not installed"}`,
10070
- `GitHub workflow: ${ghWorkflow ? "installed" : "not installed"}`
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 readFileSync20 } from "node:fs";
10858
+ import { readFileSync as readFileSync21 } from "node:fs";
10455
10859
  import { fileURLToPath } from "node:url";
10456
- import { dirname as dirname7, join as join18 } from "node:path";
10860
+ import { dirname as dirname7, join as join19 } from "node:path";
10457
10861
  function resolveToolVersion() {
10458
10862
  try {
10459
- const pkgPath = join18(dirname7(fileURLToPath(import.meta.url)), "..", "package.json");
10460
- const pkg = JSON.parse(readFileSync20(pkgPath, "utf8"));
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 readFileSync21(0, "utf8");
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.6.0",
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:*",