calllint 1.7.1 → 1.7.3

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 (3) hide show
  1. package/README.md +3 -0
  2. package/dist/index.js +241 -3
  3. package/package.json +3 -1
package/README.md CHANGED
@@ -64,6 +64,9 @@ will do, it says so and never silently upgrades to `SAFE`.
64
64
  - Self-contained HTML report: `calllint scan <config> --html > report.html`
65
65
  - Drift / rug-pull detection: `calllint baseline <config>` then `calllint verify <config> --ci`
66
66
  - Policy-as-code: `calllint policy init`
67
+ - Continuous Guard: `calllint guard` re-decides the approved authority surface (silent when unchanged); `calllint guard install --host <git|git-pre-push|github|claude-code|copilot|gemini|vscode>` adds a session-start / commit / CI hook that only shells out to `calllint guard` — never a per-call blocker
68
+ - Install the preflight into your hosts: `calllint integrate` prints a reversible install plan (writes nothing); `--apply --approve <digest>` is the only writer and reuses the audited atomic-write-and-rollback engine
69
+ - Claude Code plugin: a `PreToolUse` hook that *recommends* scanning before an agent-tool config edit — advisory, non-blocking, always exits 0, runs no scan itself
67
70
 
68
71
  CallLint is a heuristic, evidence-backed pre-flight check, **not a proof of
69
72
  safety**. `No blockers observed` ≠ guaranteed safe. Full docs, security model,
package/dist/index.js CHANGED
@@ -5510,7 +5510,7 @@ function scanOneConfig(text, configPath, policy, args, deps, allowReceipt = true
5510
5510
  if (err2) return { stdout: "", stderr: err2, exitCode: EXIT.ERROR };
5511
5511
  }
5512
5512
  const exitCode = flagBool(args.flags, "ci") ? exitCodeFor(summary, policy) : EXIT.OK;
5513
- return { stdout, exitCode };
5513
+ return { stdout, exitCode, telemetry: { verdict: summary.verdict } };
5514
5514
  }
5515
5515
  function writeReceiptFile(summary, text, configPath, policy, args, deps) {
5516
5516
  const toolVersion = deps.toolVersion ?? "0.0.0-dev";
@@ -8985,11 +8985,63 @@ function windsurfServerEntry(server) {
8985
8985
  return entry;
8986
8986
  }
8987
8987
 
8988
+ // ../../packages/install-planner/src/adapters/claudeDesktop.ts
8989
+ var CLAUDE_DESKTOP_HOST_ID = "claude-desktop";
8990
+ var CLAUDE_DESKTOP_TIER = "A";
8991
+ var claudeDesktopAdapter = {
8992
+ id: CLAUDE_DESKTOP_HOST_ID,
8993
+ tier: CLAUDE_DESKTOP_TIER,
8994
+ createPlan(ctx, upstream) {
8995
+ return buildInstallPlan({ ...ctx, host: CLAUDE_DESKTOP_HOST_ID, tier: CLAUDE_DESKTOP_TIER }, upstream);
8996
+ },
8997
+ validatePlan(plan) {
8998
+ return validatePlan(plan);
8999
+ },
9000
+ applyPlan(plan, ctx) {
9001
+ return applyPlan({
9002
+ plan,
9003
+ approvalDigest: ctx.approvalDigest,
9004
+ configPath: ctx.configPath,
9005
+ backupPath: ctx.backupPath,
9006
+ lockPath: ctx.lockPath,
9007
+ fs: ctx.fs,
9008
+ now: ctx.now
9009
+ });
9010
+ }
9011
+ };
9012
+
9013
+ // ../../packages/install-planner/src/adapters/vscode.ts
9014
+ var VSCODE_HOST_ID = "vscode";
9015
+ var VSCODE_TIER = "A";
9016
+ var vscodeAdapter = {
9017
+ id: VSCODE_HOST_ID,
9018
+ tier: VSCODE_TIER,
9019
+ createPlan(ctx, upstream) {
9020
+ return buildInstallPlan({ ...ctx, host: VSCODE_HOST_ID, tier: VSCODE_TIER }, upstream);
9021
+ },
9022
+ validatePlan(plan) {
9023
+ return validatePlan(plan);
9024
+ },
9025
+ applyPlan(plan, ctx) {
9026
+ return applyPlan({
9027
+ plan,
9028
+ approvalDigest: ctx.approvalDigest,
9029
+ configPath: ctx.configPath,
9030
+ backupPath: ctx.backupPath,
9031
+ lockPath: ctx.lockPath,
9032
+ fs: ctx.fs,
9033
+ now: ctx.now
9034
+ });
9035
+ }
9036
+ };
9037
+
8988
9038
  // ../../packages/install-planner/src/index.ts
8989
9039
  var HOST_ADAPTERS = {
8990
9040
  [claudeCodeAdapter.id]: claudeCodeAdapter,
8991
9041
  [cursorAdapter.id]: cursorAdapter,
8992
- [windsurfAdapter.id]: windsurfAdapter
9042
+ [windsurfAdapter.id]: windsurfAdapter,
9043
+ [claudeDesktopAdapter.id]: claudeDesktopAdapter,
9044
+ [vscodeAdapter.id]: vscodeAdapter
8993
9045
  };
8994
9046
  function getHostAdapter(host) {
8995
9047
  return HOST_ADAPTERS[host] ?? null;
@@ -10495,8 +10547,192 @@ function usage(msg) {
10495
10547
  return { stdout: "", stderr: msg, exitCode: EXIT.USAGE };
10496
10548
  }
10497
10549
 
10550
+ // ../../packages/telemetry-contract/src/events.ts
10551
+ var ALLOWED_EVENTS = [
10552
+ "install_completed",
10553
+ "preflight_completed",
10554
+ "decision_safe",
10555
+ "decision_review",
10556
+ "decision_block",
10557
+ "decision_unknown",
10558
+ "approval_created",
10559
+ "apply_completed",
10560
+ "verify_completed",
10561
+ "rollback_completed",
10562
+ "guard_drift_detected",
10563
+ "trust_page_viewed",
10564
+ "trust_page_to_install",
10565
+ "partner_api_called",
10566
+ "badge_rendered"
10567
+ ];
10568
+ var FORBIDDEN_FIELDS = [
10569
+ "rawConfig",
10570
+ "command",
10571
+ "environmentValue",
10572
+ "secret",
10573
+ "fileContents",
10574
+ "privateRepository",
10575
+ "userPrompt",
10576
+ "findingEvidenceText"
10577
+ ];
10578
+ var SOURCES = ["cli", "ci", "server", "install"];
10579
+ var RESULTS2 = ["SAFE", "REVIEW", "BLOCK", "UNKNOWN"];
10580
+ var EVENT_VERSION = "1.0.0";
10581
+
10582
+ // ../../packages/telemetry-contract/src/tiers.ts
10583
+ var TIER_POLICY = {
10584
+ server: {
10585
+ source: "server",
10586
+ defaultEnabled: true,
10587
+ requiresNotice: false,
10588
+ rationale: "Own web/API property; no user machine involved (funnel top)."
10589
+ },
10590
+ install: {
10591
+ source: "install",
10592
+ defaultEnabled: true,
10593
+ requiresNotice: false,
10594
+ rationale: "Attributed at the click/redirect surface, not on the machine."
10595
+ },
10596
+ ci: {
10597
+ source: "ci",
10598
+ defaultEnabled: true,
10599
+ requiresNotice: true,
10600
+ rationale: "The installing org is the controller; standard for CI tooling; disableable."
10601
+ },
10602
+ cli: {
10603
+ source: "cli",
10604
+ defaultEnabled: false,
10605
+ requiresNotice: true,
10606
+ rationale: "Personal machine \u2192 consent-first (opt-in, default off, first-run consent)."
10607
+ }
10608
+ };
10609
+ function isEnabledByDefault(source) {
10610
+ return TIER_POLICY[source].defaultEnabled;
10611
+ }
10612
+
10613
+ // ../../packages/telemetry-contract/src/sanitize.ts
10614
+ function bucketDuration(ms) {
10615
+ if (ms == null || !Number.isFinite(ms) || ms < 0) return void 0;
10616
+ if (ms < 100) return "<100ms";
10617
+ if (ms < 500) return "100-500ms";
10618
+ if (ms < 2e3) return "500-2000ms";
10619
+ return ">2000ms";
10620
+ }
10621
+ var isNonEmptyString = (v) => typeof v === "string" && v.length > 0;
10622
+ function sanitizeEvent(input) {
10623
+ for (const f of FORBIDDEN_FIELDS) {
10624
+ if (f in input) {
10625
+ throw new Error(`telemetry: forbidden field "${f}" present on event \u2014 refusing to sanitize`);
10626
+ }
10627
+ }
10628
+ if (!ALLOWED_EVENTS.includes(input.eventName)) {
10629
+ throw new Error(`telemetry: unknown eventName "${input.eventName}"`);
10630
+ }
10631
+ if (!SOURCES.includes(input.source)) {
10632
+ throw new Error(`telemetry: unknown source "${input.source}"`);
10633
+ }
10634
+ if (input.result != null && !RESULTS2.includes(input.result)) {
10635
+ throw new Error(`telemetry: unknown result "${input.result}"`);
10636
+ }
10637
+ const out = {
10638
+ eventVersion: EVENT_VERSION,
10639
+ eventName: input.eventName,
10640
+ timestamp: isNonEmptyString(input.timestamp) ? input.timestamp : "",
10641
+ source: input.source
10642
+ };
10643
+ if (isNonEmptyString(input.hostFamily)) out.hostFamily = input.hostFamily;
10644
+ if (input.result != null) out.result = input.result;
10645
+ const bucket = bucketDuration(input.durationMs);
10646
+ if (bucket) out.durationBucket = bucket;
10647
+ if (isNonEmptyString(input.inputKind)) out.inputKind = input.inputKind;
10648
+ if (isNonEmptyString(input.anonymousInstallationId)) {
10649
+ out.anonymousInstallationId = input.anonymousInstallationId;
10650
+ }
10651
+ if (isNonEmptyString(input.productVersion)) out.productVersion = input.productVersion;
10652
+ return out;
10653
+ }
10654
+
10655
+ // ../../packages/telemetry-emit/src/gate.ts
10656
+ var DISABLED_VALUES = /* @__PURE__ */ new Set(["0", "false", "off", "no"]);
10657
+ function isTelemetryDisabledByEnv(env) {
10658
+ const v = env["CALLLINT_TELEMETRY"];
10659
+ return v != null && DISABLED_VALUES.has(v.trim().toLowerCase());
10660
+ }
10661
+ function shouldEmit(source, state = {}) {
10662
+ const env = state.env ?? {};
10663
+ if (isTelemetryDisabledByEnv(env)) return false;
10664
+ if (source === "cli") return state.consented === true;
10665
+ return isEnabledByDefault(source);
10666
+ }
10667
+
10668
+ // ../../packages/telemetry-emit/src/sink.ts
10669
+ var noopSink = {
10670
+ kind: "noop",
10671
+ write() {
10672
+ }
10673
+ };
10674
+
10675
+ // ../../packages/telemetry-emit/src/emitter.ts
10676
+ function createEmitter(config) {
10677
+ const sink = config.sink ?? noopSink;
10678
+ const gate = { consented: config.consented, env: config.env };
10679
+ return {
10680
+ source: config.source,
10681
+ emit(input) {
10682
+ if (!shouldEmit(config.source, gate)) {
10683
+ return { status: "gated", reason: "disabled-or-no-consent" };
10684
+ }
10685
+ let event;
10686
+ try {
10687
+ event = sanitizeEvent({ ...input, source: config.source });
10688
+ } catch (err2) {
10689
+ return { status: "dropped", reason: err2 instanceof Error ? err2.message : String(err2) };
10690
+ }
10691
+ sink.write(event);
10692
+ return { status: "emitted", event };
10693
+ }
10694
+ };
10695
+ }
10696
+
10697
+ // src/telemetry.ts
10698
+ var VERDICT_EVENT = {
10699
+ SAFE: "decision_safe",
10700
+ REVIEW: "decision_review",
10701
+ BLOCK: "decision_block",
10702
+ UNKNOWN: "decision_unknown"
10703
+ };
10704
+ function eventFor(signal) {
10705
+ if (signal.event) return signal.event;
10706
+ if (signal.verdict) return VERDICT_EVENT[signal.verdict];
10707
+ return null;
10708
+ }
10709
+ function buildCliEmitter(env, opts = {}) {
10710
+ return createEmitter({ source: "cli", env, sink: opts.sink, consented: opts.consented });
10711
+ }
10712
+ function emitCommandSignal(emitter, signal, productVersion) {
10713
+ if (!emitter || !signal) return;
10714
+ try {
10715
+ const eventName = eventFor(signal);
10716
+ if (!eventName) return;
10717
+ const input = {
10718
+ eventName,
10719
+ ...signal.verdict ? { result: signal.verdict } : {},
10720
+ ...signal.hostFamily ? { hostFamily: signal.hostFamily } : {},
10721
+ ...signal.inputKind ? { inputKind: signal.inputKind } : {},
10722
+ ...productVersion ? { productVersion } : {}
10723
+ };
10724
+ emitter.emit(input);
10725
+ } catch {
10726
+ }
10727
+ }
10728
+
10498
10729
  // src/run.ts
10499
10730
  function run(argv, deps) {
10731
+ const result = dispatch(argv, deps);
10732
+ emitCommandSignal(deps.emitter, result.telemetry, deps.toolVersion);
10733
+ return result;
10734
+ }
10735
+ function dispatch(argv, deps) {
10500
10736
  const args = parseArgs(argv);
10501
10737
  const cmd = args.command;
10502
10738
  if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
@@ -10914,6 +11150,7 @@ async function main() {
10914
11150
  await breathe(argv);
10915
11151
  } catch {
10916
11152
  }
11153
+ const emitter = buildCliEmitter(process.env);
10917
11154
  const result = run(argv, {
10918
11155
  cwd: process.cwd(),
10919
11156
  readStdin,
@@ -10921,7 +11158,8 @@ async function main() {
10921
11158
  generatedAt,
10922
11159
  online,
10923
11160
  toolVersion: resolveToolVersion(),
10924
- getChangedFilesDiff: () => gitChangedFiles(process.cwd())
11161
+ getChangedFilesDiff: () => gitChangedFiles(process.cwd()),
11162
+ emitter
10925
11163
  });
10926
11164
  if (result.stdout) process.stdout.write(result.stdout + "\n");
10927
11165
  if (result.stderr) process.stderr.write(result.stderr + "\n");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "calllint",
3
- "version": "1.7.1",
3
+ "version": "1.7.3",
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",
@@ -63,6 +63,8 @@
63
63
  "@calllint/report-renderer": "workspace:*",
64
64
  "@calllint/resolver": "workspace:*",
65
65
  "@calllint/signature": "workspace:*",
66
+ "@calllint/telemetry-contract": "workspace:*",
67
+ "@calllint/telemetry-emit": "workspace:*",
66
68
  "@calllint/types": "workspace:*",
67
69
  "esbuild": "^0.21.5"
68
70
  }