calllint 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +1159 -427
  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 readFileSync20 } from "node:fs";
4
+ import { readFileSync as readFileSync21 } from "node:fs";
5
5
  import { execFileSync } from "node:child_process";
6
6
 
7
7
  // src/args.ts
@@ -74,6 +74,10 @@ COMMANDS
74
74
  diagnostics [target] Emit editor/agent-host diagnostics JSON (calllint.diagnostics.v0)
75
75
  baseline [target] Record the approved risk surface as a baseline
76
76
  approve Record the repo-wide capability surface as approved state (L4)
77
+ 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 status Show baseline / disable / installed-hook state
80
+ guard disable Turn Continuous Guard off (.calllint/guard.json)
77
81
  receipt verify <f> Validate a calllint.receipt.v0 (structure + signature if present)
78
82
  receipt sign <f> Sign a receipt with a local key (--key; development/testing)
79
83
  receipt keygen Generate a local ed25519 keypair (--out; development/testing)
@@ -106,6 +110,10 @@ SCAN OPTIONS
106
110
  --badge Emit a shields.io endpoint badge JSON (SAFE/REVIEW/UNKNOWN/BLOCK)
107
111
  --receipt Generate a cryptographically-verifiable audit receipt (for CI/compliance)
108
112
  --receipt-out <f> Receipt output path (default: calllint-receipt.json)
113
+ --evidence <file> Attach an external scanner report (e.g. SkillSpector JSON/SARIF)
114
+ as supporting evidence; shown side-by-side in a joint Trust
115
+ Packet. Never re-scored, never changes the CallLint verdict.
116
+ --evidence-format <fmt> Force evidence format (json|sarif); default auto-detect
109
117
  --policy <file> Use a policy file (default: built-in defaults)
110
118
  --stdin Read config JSON from stdin
111
119
  --ci Exit non-zero per policy (BLOCK=30, UNKNOWN=20, REVIEW=10 if enabled)
@@ -118,6 +126,13 @@ VERIFY OPTIONS
118
126
  --ci Exit 40 if the surface drifted (from baseline or approved state)
119
127
  --json Emit the drift report JSON
120
128
 
129
+ GUARD OPTIONS
130
+ --host <h> guard install target: git (pre-commit) | github (workflow)
131
+ --approved [file] Approved baseline to diff against (default: .calllint/approved.json)
132
+ --json Emit the guard assessment / status JSON
133
+ (exit) silent/note=0, REVIEW=10, UNKNOWN=20, BLOCK=30; guard self-failure is non-zero
134
+ (env) CALLLINT_GUARD=0 disables the guard
135
+
121
136
  EXAMPLES
122
137
  calllint inventory # List discovered agent configs
123
138
  calllint scan --auto # Discover and scan all agents
@@ -130,11 +145,14 @@ EXAMPLES
130
145
  calllint scan .cursor/mcp.json --markdown
131
146
  calllint scan .cursor/mcp.json --badge > calllint-badge.json
132
147
  calllint scan .cursor/mcp.json --receipt # Generate audit receipt
148
+ calllint scan .cursor/mcp.json --evidence skillspector-report.json # Joint Trust Packet
133
149
  calllint receipt verify calllint-receipt.json # Verify receipt integrity
134
150
  calllint action inspect payment.json
135
151
  calllint inbox inspect gmail-reply.normalized.json
136
152
  calllint verify ./mcp.json --ci
137
153
  calllint approve && calllint verify --approved --ci
154
+ calllint approve && calllint guard install --host git # re-decide on every commit
155
+ calllint guard --json # authority-change assessment (silent when unchanged)
138
156
  calllint explain filesystem
139
157
  `;
140
158
  function helpCommand() {
@@ -402,7 +420,10 @@ var REASON_CODES = [
402
420
  "PROMPT_METADATA_INSTRUCTION",
403
421
  "OAUTH_SCOPE_UNKNOWN_OR_EXPANDED",
404
422
  "TOOL_DESCRIPTOR_CHANGED",
405
- "LONG_RUNNING_GATEWAY_RUNTIME"
423
+ "LONG_RUNNING_GATEWAY_RUNTIME",
424
+ // #13 — appended last (ADR 0044) so the frozen order 0–11 is unchanged. Backed by
425
+ // the flow object (calllint.flow.v0), not a static detector; see REASON_CODE_META.
426
+ "TOXIC_FLOW_COMPOSITION"
406
427
  ];
407
428
  var REASON_CODE_META = {
408
429
  UNPINNED_PACKAGE: {
@@ -466,6 +487,16 @@ var REASON_CODE_META = {
466
487
  backedBy: ["runtime.gateway"],
467
488
  status: "wired",
468
489
  label: "Long-running gateway runtime"
490
+ },
491
+ TOXIC_FLOW_COMPOSITION: {
492
+ // Backed by the flow object (calllint.flow.v0, built by the flow-analyzer package),
493
+ // NOT a static detector Finding. The synthetic backing id keeps every wired code
494
+ // naming its backing without inventing a new status value. No detector emits this id,
495
+ // so findingsToReasonCodes never fabricates the code — it is produced only by the
496
+ // explicit flow-fold step. See ADR 0044 / 0040.
497
+ backedBy: ["flow:toxic-composition"],
498
+ status: "wired",
499
+ label: "Cross-tool toxic-flow composition"
469
500
  }
470
501
  };
471
502
  function reasonCodeForFinding(findingId) {
@@ -486,6 +517,9 @@ var VERDICT_NEXT_ACTION = {
486
517
  // ../../packages/types/src/authority.ts
487
518
  var AUTHORITY_SCHEMA_VERSION = "calllint.authority.v0";
488
519
 
520
+ // ../../packages/types/src/flow.ts
521
+ var FLOW_SCHEMA_VERSION = "calllint.flow.v0";
522
+
489
523
  // ../../packages/types/src/trustDecision.ts
490
524
  var DECISION_SCHEMA_VERSION = "calllint.decision.v0";
491
525
 
@@ -619,11 +653,13 @@ function evidenceFloor(evidence) {
619
653
  function decideOverAuthority(input) {
620
654
  const { authority, policy } = input;
621
655
  const evidence = input.evidence ?? [];
622
- const reasons = authority.capabilities.map((c) => {
656
+ const capabilityReasons = authority.capabilities.map((c) => {
623
657
  const code = reasonCodeFor(c);
624
658
  const contributes = moreSevere(baseVerdict(c), policyFloor(code, policy));
625
659
  return { code, evidenceSource: c.evidenceSource, contributes };
626
660
  });
661
+ const flowReasons = input.flowReasons ?? [];
662
+ const reasons = [...capabilityReasons, ...flowReasons];
627
663
  const unknowns = [...authority.unknowns];
628
664
  const contributions = reasons.map((r) => r.contributes);
629
665
  if (authority.subject.artifactDigest === null) {
@@ -682,7 +718,8 @@ function resolveScanOptions(opts) {
682
718
  now: opts?.now ?? 0,
683
719
  generatedAt: opts?.generatedAt ?? "1970-01-01T00:00:00.000Z",
684
720
  extraFindings: opts?.extraFindings ?? {},
685
- surfaces: opts?.surfaces ?? []
721
+ surfaces: opts?.surfaces ?? [],
722
+ evidence: opts?.evidence ?? []
686
723
  };
687
724
  }
688
725
 
@@ -1857,6 +1894,21 @@ function analyzeDocumentSurfaces(surfaces) {
1857
1894
  ];
1858
1895
  }
1859
1896
 
1897
+ // ../../packages/static-analyzer/src/trustSource.ts
1898
+ function classifyTrustSource(cap) {
1899
+ if (cap.action === "read" && cap.resource === "secret") return "sensitive.secret";
1900
+ if (cap.action === "execute" && cap.resource === "process" && cap.evidenceSource === "server.command") {
1901
+ return "trusted.local_project";
1902
+ }
1903
+ return "unknown";
1904
+ }
1905
+ function withTrustSource(caps) {
1906
+ return caps.map((c) => {
1907
+ const ts = classifyTrustSource(c);
1908
+ return ts === "unknown" ? { ...c } : { ...c, trustSource: ts };
1909
+ });
1910
+ }
1911
+
1860
1912
  // ../../packages/static-analyzer/src/instructionAuthority.ts
1861
1913
  var RULES = [
1862
1914
  // 1. privilege-escalation → execute × process (irreversible, must be approved)
@@ -2062,7 +2114,7 @@ function extractInstructionAuthority(surfaces) {
2062
2114
  }
2063
2115
  }
2064
2116
  }
2065
- return sortCapabilities([...seen.values()]);
2117
+ return withTrustSource(sortCapabilities([...seen.values()]));
2066
2118
  }
2067
2119
  function sortCapabilities(caps) {
2068
2120
  return caps.sort((a, b) => {
@@ -2149,7 +2201,7 @@ function deriveConfigCapabilities(server) {
2149
2201
  completeness: "complete"
2150
2202
  });
2151
2203
  }
2152
- return sortCapabilities(caps);
2204
+ return sortCapabilities(withTrustSource(caps));
2153
2205
  }
2154
2206
 
2155
2207
  // ../../packages/risk-engine/src/computeRiskClass.ts
@@ -2670,7 +2722,7 @@ function enrichEvidencePositions(reports, positions) {
2670
2722
  }
2671
2723
 
2672
2724
  // ../../packages/core/src/scanConfig.ts
2673
- function aggregate(configPath, reports, generatedAt) {
2725
+ function aggregate(configPath, reports, generatedAt, evidence) {
2674
2726
  const counts = { SAFE: 0, REVIEW: 0, BLOCK: 0, UNKNOWN: 0 };
2675
2727
  for (const r of reports) counts[r.verdict]++;
2676
2728
  const verdict = reports.length === 0 ? "UNKNOWN" : mostSevereVerdict(reports.map((r) => r.verdict));
@@ -2683,18 +2735,23 @@ function aggregate(configPath, reports, generatedAt) {
2683
2735
  counts,
2684
2736
  reports,
2685
2737
  diagnostics: [],
2686
- generatedAt
2738
+ generatedAt,
2739
+ // Additive projection (ADR 0034). Attached only when the CLI imported evidence
2740
+ // via `scan --evidence`; omitted otherwise so default scan output is
2741
+ // byte-identical (the offline corpus never attaches evidence). Never re-scored:
2742
+ // the `verdict` above is unaffected by these findings.
2743
+ ...evidence.length > 0 ? { evidence } : {}
2687
2744
  };
2688
2745
  }
2689
2746
  function scanParsed(parsed, opts) {
2690
- const { generatedAt, surfaces } = resolveScanOptions(opts);
2747
+ const { generatedAt, surfaces, evidence } = resolveScanOptions(opts);
2691
2748
  const reports = parsed.servers.map(
2692
2749
  (server) => scanServer({ server, targetKind: parsed.kind }, opts)
2693
2750
  );
2694
2751
  enrichEvidencePositions(reports, parsed.positions);
2695
2752
  const surfaceReport = scanDocumentSurfaces(surfaces, parsed.configPath, generatedAt);
2696
2753
  if (surfaceReport) reports.push(surfaceReport);
2697
- return aggregate(parsed.configPath, reports, generatedAt);
2754
+ return aggregate(parsed.configPath, reports, generatedAt, evidence);
2698
2755
  }
2699
2756
  function scanConfigText(text, configPath, opts) {
2700
2757
  return scanParsed(parseConfigText(text, configPath), opts);
@@ -3355,6 +3412,60 @@ function escalate(current) {
3355
3412
  return current === "BLOCK" || current === "UNKNOWN" ? current : "REVIEW";
3356
3413
  }
3357
3414
 
3415
+ // ../../packages/core/src/state/continuousGuard.ts
3416
+ function assessGuardDrift(report) {
3417
+ if (!report.drifted) {
3418
+ return {
3419
+ action: "silent",
3420
+ verdict: "SAFE",
3421
+ drifted: false,
3422
+ summary: "No authority change since the approved baseline."
3423
+ };
3424
+ }
3425
+ const changed = report.entries.filter((e) => e.status !== "unchanged");
3426
+ const n = changed.length;
3427
+ const surfaces = `${n} surface${n === 1 ? "" : "s"}`;
3428
+ switch (report.verdict) {
3429
+ case "BLOCK":
3430
+ return {
3431
+ action: "refuse",
3432
+ verdict: "BLOCK",
3433
+ drifted: true,
3434
+ summary: `Authority changed on ${surfaces}; the new surface is blocked by policy.`
3435
+ };
3436
+ case "UNKNOWN":
3437
+ return {
3438
+ action: "request-evidence",
3439
+ verdict: "UNKNOWN",
3440
+ drifted: true,
3441
+ summary: `Authority changed on ${surfaces}; the new surface could not be verified \u2014 evidence or approval required.`
3442
+ };
3443
+ case "REVIEW":
3444
+ return {
3445
+ action: "prompt",
3446
+ verdict: "REVIEW",
3447
+ drifted: true,
3448
+ summary: `Authority changed on ${surfaces}; review the new surface before proceeding.`
3449
+ };
3450
+ case "SAFE":
3451
+ return {
3452
+ action: "note",
3453
+ verdict: "SAFE",
3454
+ drifted: true,
3455
+ summary: `Authority changed on ${surfaces}; no new blockers observed.`
3456
+ };
3457
+ }
3458
+ }
3459
+ function guardFailClosed(failure) {
3460
+ return {
3461
+ action: "fail-closed",
3462
+ verdict: "UNKNOWN",
3463
+ drifted: false,
3464
+ summary: "Continuous Guard could not verify the authority surface; failing closed.",
3465
+ failure
3466
+ };
3467
+ }
3468
+
3358
3469
  // ../../packages/core/src/distribution/agentRule.ts
3359
3470
  var RELEVANT_SURFACES = [
3360
3471
  ".cursor/mcp.json",
@@ -3488,6 +3599,64 @@ ${rule()}
3488
3599
  }
3489
3600
  }
3490
3601
 
3602
+ // ../../packages/core/src/distribution/ciGate.ts
3603
+ var CI_GATE_PATHS = [
3604
+ "**/mcp.json",
3605
+ "**/.mcp.json",
3606
+ "**/settings.json",
3607
+ "**/config.toml",
3608
+ ".cursor/**",
3609
+ ".vscode/**",
3610
+ "claude_desktop_config.json",
3611
+ "CLAUDE.md",
3612
+ "AGENTS.md",
3613
+ ".calllint/approved.json"
3614
+ ];
3615
+ function pathsBlock() {
3616
+ return CI_GATE_PATHS.map((p) => ` - ${JSON.stringify(p)}`).join("\n");
3617
+ }
3618
+ var DRIFT_STEP = ` - name: CallLint drift gate
3619
+ shell: bash
3620
+ run: |
3621
+ if [ -f .calllint/approved.json ]; then
3622
+ npx -y calllint verify --approved --ci --no-emoji
3623
+ else
3624
+ echo "No .calllint/approved.json \u2014 seed it with: npx calllint approve"
3625
+ echo "Running report-only scan-all (not gating)."
3626
+ npx -y calllint scan-all --no-emoji || true
3627
+ fi`;
3628
+ var SCAN_ALL_STEP = ` - name: CallLint scan-all (report-only)
3629
+ shell: bash
3630
+ run: npx -y calllint scan-all --no-emoji || true`;
3631
+ function renderCiGate(opts = {}) {
3632
+ const mode = opts.mode ?? "drift";
3633
+ const step = mode === "scan-all" ? SCAN_ALL_STEP : DRIFT_STEP;
3634
+ const headline = mode === "scan-all" ? "CallLint agent-tool surface report (report-only; never gates)." : "CallLint agent-tool surface drift gate (fails on approved-state drift).";
3635
+ return `# ${headline}
3636
+ # Generated by \`calllint\` (generate_ci_gate_snippet). CallLint is static and
3637
+ # NEVER executes a scanned server.
3638
+ name: calllint
3639
+
3640
+ on:
3641
+ pull_request:
3642
+ paths:
3643
+ ${pathsBlock()}
3644
+
3645
+ permissions:
3646
+ contents: read
3647
+
3648
+ jobs:
3649
+ surface-gate:
3650
+ runs-on: ubuntu-latest
3651
+ steps:
3652
+ - uses: actions/checkout@v6
3653
+ - uses: actions/setup-node@v6
3654
+ with:
3655
+ node-version: 20
3656
+ ${step}
3657
+ `;
3658
+ }
3659
+
3491
3660
  // ../../packages/core/src/receipt/createReceipt.ts
3492
3661
  import { randomBytes } from "node:crypto";
3493
3662
  function newReceiptId() {
@@ -4442,144 +4611,413 @@ function renderHtml(summary) {
4442
4611
  </html>`;
4443
4612
  }
4444
4613
 
4445
- // src/exitCode.ts
4446
- function exitCodeFor(summary, policy) {
4447
- const verdict = summary.verdict;
4448
- if (!shouldFailCi(verdict, policy)) return EXIT.OK;
4449
- switch (verdict) {
4450
- case "BLOCK":
4451
- return EXIT.BLOCK;
4452
- case "UNKNOWN":
4453
- return EXIT.UNKNOWN;
4454
- case "REVIEW":
4455
- return EXIT.REVIEW;
4456
- case "SAFE":
4457
- return EXIT.OK;
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
+ }
4458
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");
4459
4659
  }
4460
4660
 
4461
- // src/commands/resolveInput.ts
4462
- import { existsSync as existsSync3, readFileSync as readFileSync5 } from "node:fs";
4463
- import { join as join4 } from "node:path";
4464
- var DEFAULT_CONFIG_PATHS = [
4465
- ".cursor/mcp.json",
4466
- ".mcp.json",
4467
- "mcp.json",
4468
- ".claude/settings.json",
4469
- ".vscode/mcp.json"
4470
- ];
4471
- function findDefaultConfig(cwd) {
4472
- for (const rel of DEFAULT_CONFIG_PATHS) {
4473
- const p = join4(cwd, rel);
4474
- if (existsSync3(p)) return p;
4475
- }
4476
- return void 0;
4477
- }
4478
- function isInputError(v) {
4479
- return "error" in v;
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;
4480
4671
  }
4481
- function resolveConfigInput(args, deps) {
4482
- if (flagBool(args.flags, "stdin")) {
4483
- return { text: deps.readStdin(), configPath: "<stdin>" };
4484
- }
4485
- const given = args.positionals[0];
4486
- if (given) {
4487
- const spec = parseTargetSpec(given);
4488
- if (spec.kind === "npm") {
4489
- if (!spec.packageSpec) {
4490
- return { error: "Empty npm target. Use npm:<package>[@version].", exitCode: EXIT.USAGE };
4491
- }
4492
- return synthesizeNpmConfig(spec.packageSpec);
4493
- }
4494
- if (spec.kind === "github") {
4495
- return {
4496
- error: "GitHub targets require network access. Re-run with --online to fetch repo MCP configs.",
4497
- exitCode: EXIT.USAGE
4498
- };
4499
- }
4500
- }
4501
- const resolved = given ?? findDefaultConfig(deps.cwd);
4502
- if (!resolved) {
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");
4503
4680
  return {
4504
- error: "No config given and none found. Pass a path or use --stdin.\nLooked in: " + DEFAULT_CONFIG_PATHS.join(", "),
4505
- exitCode: EXIT.USAGE
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)
4506
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";
4507
4692
  }
4508
- if (!existsSync3(resolved)) {
4509
- return { error: `File not found: ${resolved}`, exitCode: EXIT.USAGE };
4510
- }
4511
- return { text: readFileSync5(resolved, "utf8"), configPath: resolved };
4512
- }
4513
-
4514
- // src/commands/changedConfigs.ts
4515
- import { basename as basename3, resolve } from "node:path";
4516
- import { existsSync as existsSync4 } from "node:fs";
4517
- function changedConfigPaths(cwd, diff) {
4518
- let raw;
4519
- try {
4520
- raw = diff();
4521
- } catch {
4522
- return [];
4693
+ if (raw.partial === true) {
4694
+ degradedReasons.push("SkillSpector reported a partial scan");
4695
+ completenessHint = completenessHint ?? "partial";
4523
4696
  }
4524
- const knownLeaves = new Set(DEFAULT_CONFIG_PATHS.map((p) => basename3(p)));
4525
- const knownPaths = new Set(DEFAULT_CONFIG_PATHS);
4526
- return raw.split("\n").map((l) => l.trim()).filter(
4527
- (f) => f.length > 0 && (knownPaths.has(f) || DEFAULT_CONFIG_PATHS.some((p) => f.endsWith("/" + p)) || knownLeaves.has(basename3(f)))
4528
- ).map((f) => resolve(cwd, f)).filter((abs) => existsSync4(abs));
4529
- }
4530
-
4531
- // src/commands/surfaces.ts
4532
- import { existsSync as existsSync5, readFileSync as readFileSync6, statSync as statSync2 } from "node:fs";
4533
- import { join as join5 } from "node:path";
4534
- var SURFACE_SIZE_CAP = 256 * 1024;
4535
- var SURFACE_FILES = [
4536
- { file: "README.md", kind: "readme" },
4537
- { file: "SKILL.md", kind: "skill" },
4538
- { file: "AGENTS.md", kind: "agents" }
4539
- ];
4540
- function readCapped2(path) {
4541
- try {
4542
- const st = statSync2(path);
4543
- if (!st.isFile()) return void 0;
4544
- const raw = readFileSync6(path, "utf8");
4545
- if (raw.length > SURFACE_SIZE_CAP) {
4546
- return { text: raw.slice(0, SURFACE_SIZE_CAP), truncated: true };
4547
- }
4548
- return { text: raw, truncated: false };
4549
- } catch {
4550
- return void 0;
4697
+ if (raw.degraded === true) {
4698
+ degradedReasons.push("SkillSpector reported a degraded scan");
4699
+ completenessHint = "degraded";
4551
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
+ };
4552
4711
  }
4553
- function readDocumentSurfaces(dir) {
4554
- const surfaces = [];
4555
- if (!existsSync5(dir)) return surfaces;
4556
- for (const { file, kind } of SURFACE_FILES) {
4557
- const got = readCapped2(join5(dir, file));
4558
- if (got) surfaces.push({ path: file, kind, text: got.text, truncated: got.truncated });
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
+ };
4559
4721
  }
4560
- const pkgPath = join5(dir, "package.json");
4561
- const pkgRaw = readCapped2(pkgPath);
4562
- if (pkgRaw) {
4563
- try {
4564
- const pkg = JSON.parse(pkgRaw.text);
4565
- const description = pkg && typeof pkg === "object" && !Array.isArray(pkg) ? pkg.description : void 0;
4566
- if (typeof description === "string") {
4567
- surfaces.push({
4568
- path: "package.json",
4569
- kind: "package-description",
4570
- text: description,
4571
- truncated: pkgRaw.truncated
4572
- });
4573
- }
4574
- } catch {
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
+ });
4575
4743
  }
4576
4744
  }
4577
- return surfaces;
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
+ };
4578
4754
  }
4579
-
4580
- // src/commands/scan.ts
4581
- import { readFileSync as readFileSync12, writeFileSync as writeFileSync3 } from "node:fs";
4582
-
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
+ ]);
4820
+ }
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
+ ]);
4835
+ }
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
+ };
4851
+ }
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);
4862
+ }
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);
4881
+ }
4882
+
4883
+ // src/exitCode.ts
4884
+ function exitCodeFor(summary, policy) {
4885
+ const verdict = summary.verdict;
4886
+ if (!shouldFailCi(verdict, policy)) return EXIT.OK;
4887
+ switch (verdict) {
4888
+ case "BLOCK":
4889
+ return EXIT.BLOCK;
4890
+ case "UNKNOWN":
4891
+ return EXIT.UNKNOWN;
4892
+ case "REVIEW":
4893
+ return EXIT.REVIEW;
4894
+ case "SAFE":
4895
+ return EXIT.OK;
4896
+ }
4897
+ }
4898
+
4899
+ // src/commands/resolveInput.ts
4900
+ import { existsSync as existsSync3, readFileSync as readFileSync5 } from "node:fs";
4901
+ import { join as join4 } from "node:path";
4902
+ var DEFAULT_CONFIG_PATHS = [
4903
+ ".cursor/mcp.json",
4904
+ ".mcp.json",
4905
+ "mcp.json",
4906
+ ".claude/settings.json",
4907
+ ".vscode/mcp.json"
4908
+ ];
4909
+ function findDefaultConfig(cwd) {
4910
+ for (const rel of DEFAULT_CONFIG_PATHS) {
4911
+ const p = join4(cwd, rel);
4912
+ if (existsSync3(p)) return p;
4913
+ }
4914
+ return void 0;
4915
+ }
4916
+ function isInputError(v) {
4917
+ return "error" in v;
4918
+ }
4919
+ function resolveConfigInput(args, deps) {
4920
+ if (flagBool(args.flags, "stdin")) {
4921
+ return { text: deps.readStdin(), configPath: "<stdin>" };
4922
+ }
4923
+ const given = args.positionals[0];
4924
+ if (given) {
4925
+ const spec = parseTargetSpec(given);
4926
+ if (spec.kind === "npm") {
4927
+ if (!spec.packageSpec) {
4928
+ return { error: "Empty npm target. Use npm:<package>[@version].", exitCode: EXIT.USAGE };
4929
+ }
4930
+ return synthesizeNpmConfig(spec.packageSpec);
4931
+ }
4932
+ if (spec.kind === "github") {
4933
+ return {
4934
+ error: "GitHub targets require network access. Re-run with --online to fetch repo MCP configs.",
4935
+ exitCode: EXIT.USAGE
4936
+ };
4937
+ }
4938
+ }
4939
+ const resolved = given ?? findDefaultConfig(deps.cwd);
4940
+ if (!resolved) {
4941
+ return {
4942
+ error: "No config given and none found. Pass a path or use --stdin.\nLooked in: " + DEFAULT_CONFIG_PATHS.join(", "),
4943
+ exitCode: EXIT.USAGE
4944
+ };
4945
+ }
4946
+ if (!existsSync3(resolved)) {
4947
+ return { error: `File not found: ${resolved}`, exitCode: EXIT.USAGE };
4948
+ }
4949
+ return { text: readFileSync5(resolved, "utf8"), configPath: resolved };
4950
+ }
4951
+
4952
+ // src/commands/changedConfigs.ts
4953
+ import { basename as basename3, resolve } from "node:path";
4954
+ import { existsSync as existsSync4 } from "node:fs";
4955
+ function changedConfigPaths(cwd, diff) {
4956
+ let raw;
4957
+ try {
4958
+ raw = diff();
4959
+ } catch {
4960
+ return [];
4961
+ }
4962
+ const knownLeaves = new Set(DEFAULT_CONFIG_PATHS.map((p) => basename3(p)));
4963
+ const knownPaths = new Set(DEFAULT_CONFIG_PATHS);
4964
+ return raw.split("\n").map((l) => l.trim()).filter(
4965
+ (f) => f.length > 0 && (knownPaths.has(f) || DEFAULT_CONFIG_PATHS.some((p) => f.endsWith("/" + p)) || knownLeaves.has(basename3(f)))
4966
+ ).map((f) => resolve(cwd, f)).filter((abs) => existsSync4(abs));
4967
+ }
4968
+
4969
+ // src/commands/surfaces.ts
4970
+ import { existsSync as existsSync5, readFileSync as readFileSync6, statSync as statSync2 } from "node:fs";
4971
+ import { join as join5 } from "node:path";
4972
+ var SURFACE_SIZE_CAP = 256 * 1024;
4973
+ var SURFACE_FILES = [
4974
+ { file: "README.md", kind: "readme" },
4975
+ { file: "SKILL.md", kind: "skill" },
4976
+ { file: "AGENTS.md", kind: "agents" }
4977
+ ];
4978
+ function readCapped2(path) {
4979
+ try {
4980
+ const st = statSync2(path);
4981
+ if (!st.isFile()) return void 0;
4982
+ const raw = readFileSync6(path, "utf8");
4983
+ if (raw.length > SURFACE_SIZE_CAP) {
4984
+ return { text: raw.slice(0, SURFACE_SIZE_CAP), truncated: true };
4985
+ }
4986
+ return { text: raw, truncated: false };
4987
+ } catch {
4988
+ return void 0;
4989
+ }
4990
+ }
4991
+ function readDocumentSurfaces(dir) {
4992
+ const surfaces = [];
4993
+ if (!existsSync5(dir)) return surfaces;
4994
+ for (const { file, kind } of SURFACE_FILES) {
4995
+ const got = readCapped2(join5(dir, file));
4996
+ if (got) surfaces.push({ path: file, kind, text: got.text, truncated: got.truncated });
4997
+ }
4998
+ const pkgPath = join5(dir, "package.json");
4999
+ const pkgRaw = readCapped2(pkgPath);
5000
+ if (pkgRaw) {
5001
+ try {
5002
+ const pkg = JSON.parse(pkgRaw.text);
5003
+ const description = pkg && typeof pkg === "object" && !Array.isArray(pkg) ? pkg.description : void 0;
5004
+ if (typeof description === "string") {
5005
+ surfaces.push({
5006
+ path: "package.json",
5007
+ kind: "package-description",
5008
+ text: description,
5009
+ truncated: pkgRaw.truncated
5010
+ });
5011
+ }
5012
+ } catch {
5013
+ }
5014
+ }
5015
+ return surfaces;
5016
+ }
5017
+
5018
+ // src/commands/scan.ts
5019
+ import { readFileSync as readFileSync12, writeFileSync as writeFileSync3 } from "node:fs";
5020
+
4583
5021
  // ../../packages/discovery/src/registry.ts
4584
5022
  var ExtractorRegistry = class {
4585
5023
  extractors = /* @__PURE__ */ new Map();
@@ -5030,6 +5468,15 @@ function scanCommand(args, deps) {
5030
5468
  function scanOneConfig(text, configPath, policy, args, deps, allowReceipt = true) {
5031
5469
  const surfaceDir = flagStr(args.flags, "surface-dir");
5032
5470
  const surfaces = surfaceDir ? readDocumentSurfaces(resolve3(deps.cwd, surfaceDir)) : void 0;
5471
+ const evidenceFile = flagStr(args.flags, "evidence");
5472
+ let evidence;
5473
+ if (evidenceFile) {
5474
+ const loaded = loadEvidence(evidenceFile, args, deps);
5475
+ if ("error" in loaded) {
5476
+ return { stdout: "", stderr: loaded.error, exitCode: loaded.exitCode };
5477
+ }
5478
+ evidence = [loaded];
5479
+ }
5033
5480
  let summary;
5034
5481
  try {
5035
5482
  summary = scanConfigText(text, configPath, {
@@ -5037,7 +5484,8 @@ function scanOneConfig(text, configPath, policy, args, deps, allowReceipt = true
5037
5484
  now: deps.now,
5038
5485
  generatedAt: deps.generatedAt,
5039
5486
  extraFindings: deps.online?.extraFindings,
5040
- surfaces
5487
+ surfaces,
5488
+ evidence
5041
5489
  });
5042
5490
  } catch (err2) {
5043
5491
  if (err2 instanceof ConfigParseError) {
@@ -5055,7 +5503,7 @@ function scanOneConfig(text, configPath, policy, args, deps, allowReceipt = true
5055
5503
  } catch {
5056
5504
  }
5057
5505
  }
5058
- const stdout = renderSummary(summary, args);
5506
+ const stdout = renderSummary(summary, args, deps.toolVersion);
5059
5507
  if (allowReceipt && flagBool(args.flags, "receipt")) {
5060
5508
  const err2 = writeReceiptFile(summary, text, configPath, policy, args, deps);
5061
5509
  if (err2) return { stdout: "", stderr: err2, exitCode: EXIT.ERROR };
@@ -5088,15 +5536,31 @@ function writeReceiptFile(summary, text, configPath, policy, args, deps) {
5088
5536
  }
5089
5537
  return void 0;
5090
5538
  }
5091
- function renderSummary(summary, args) {
5539
+ function loadEvidence(file, args, deps) {
5540
+ let rawText;
5541
+ try {
5542
+ rawText = readFileSync12(resolve3(deps.cwd, file), "utf8");
5543
+ } catch (err2) {
5544
+ const e = err2;
5545
+ return {
5546
+ error: e.code === "ENOENT" ? `Evidence file not found: ${file}` : e.message,
5547
+ exitCode: EXIT.USAGE
5548
+ };
5549
+ }
5550
+ const fmt = flagStr(args.flags, "evidence-format");
5551
+ const format = fmt === "sarif" ? "sarif" : fmt === "json" ? "json" : void 0;
5552
+ return importEvidence(rawText, { format });
5553
+ }
5554
+ function renderSummary(summary, args, toolVersion) {
5092
5555
  const style = flagBool(args.flags, "no-emoji") ? NO_EMOJI_STYLE : DEFAULT_STYLE;
5093
5556
  if (flagBool(args.flags, "json")) return renderJson(summary);
5094
5557
  if (flagBool(args.flags, "sarif")) return renderSarif(summary);
5095
5558
  if (flagBool(args.flags, "markdown")) return renderMarkdown(summary);
5096
5559
  if (flagBool(args.flags, "badge")) return renderBadge(summary);
5097
5560
  if (flagBool(args.flags, "html")) return renderHtml(summary);
5098
- if (flagBool(args.flags, "compact")) return renderCompact(summary, style);
5099
- return renderTerminal(summary, style);
5561
+ const base = flagBool(args.flags, "compact") ? renderCompact(summary, style) : renderTerminal(summary, style);
5562
+ const packet = renderTrustPacket(summary, toolVersion ?? "0.0.0-dev", style);
5563
+ return packet ? base + "\n" + packet : base;
5100
5564
  }
5101
5565
  function scanChangedCommand(args, deps) {
5102
5566
  const policyPath = flagStr(args.flags, "policy");
@@ -7441,291 +7905,67 @@ function inventoryCommand(args, deps) {
7441
7905
  lines.push("Searched agents: Cursor, Claude Code, Claude Desktop");
7442
7906
  lines.push("To scan a specific config: calllint scan --config <path>");
7443
7907
  } else {
7444
- lines.push(`Discovered ${result.discovered.length} agent config(s):`);
7445
- lines.push("");
7446
- const byAgent = /* @__PURE__ */ new Map();
7447
- for (const config of result.discovered) {
7448
- const existing = byAgent.get(config.agentType) || [];
7449
- existing.push(config);
7450
- byAgent.set(config.agentType, existing);
7451
- }
7452
- const sortedAgents = Array.from(byAgent.keys()).sort((a, b) => {
7453
- const priorityA = result.discovered.find((c) => c.agentType === a)?.priority || "P3";
7454
- const priorityB = result.discovered.find((c) => c.agentType === b)?.priority || "P3";
7455
- return priorityA.localeCompare(priorityB);
7456
- });
7457
- for (const agentType of sortedAgents) {
7458
- const configs = byAgent.get(agentType);
7459
- const priority = configs[0].priority;
7460
- const agentLabel = formatAgentType(agentType);
7461
- lines.push(`${agentLabel} (${priority}):`);
7462
- for (const config of configs) {
7463
- lines.push(` ${config.configPath}`);
7464
- }
7465
- lines.push("");
7466
- }
7467
- lines.push(`To scan all: calllint scan --auto`);
7468
- lines.push(`To scan one: calllint scan --agent ${result.discovered[0].agentType}`);
7469
- }
7470
- return {
7471
- exitCode: 0,
7472
- stdout: lines.join("\n") + "\n",
7473
- stderr: ""
7474
- };
7475
- } catch (error) {
7476
- const message = error instanceof Error ? error.message : String(error);
7477
- return {
7478
- exitCode: 1,
7479
- stdout: "",
7480
- stderr: `Error discovering configs: ${message}
7481
- `
7482
- };
7483
- }
7484
- }
7485
- function formatAgentType(agentType) {
7486
- switch (agentType) {
7487
- case "cursor":
7488
- return "Cursor";
7489
- case "claude-code":
7490
- return "Claude Code";
7491
- case "claude-desktop":
7492
- return "Claude Desktop";
7493
- case "vscode":
7494
- return "VS Code";
7495
- case "windsurf":
7496
- return "Windsurf";
7497
- default:
7498
- return agentType;
7499
- }
7500
- }
7501
-
7502
- // src/commands/evidence.ts
7503
- import { readFileSync as readFileSync16 } from "node:fs";
7504
- import { resolve as resolve8 } from "node:path";
7505
-
7506
- // ../../packages/evidence/src/types.ts
7507
- var EVIDENCE_SCHEMA_VERSION = "calllint.evidence-provider.v0";
7508
-
7509
- // ../../packages/evidence/src/providers/skillspector.ts
7510
- function pinnedVersion(raw) {
7511
- const tool = raw.tool;
7512
- const commit = typeof raw.commit === "string" && raw.commit || tool && typeof tool.commit === "string" && tool.commit || "";
7513
- if (commit) return `git:${commit}`;
7514
- const version = typeof raw.version === "string" && raw.version || tool && typeof tool.version === "string" && tool.version || "";
7515
- return version || void 0;
7516
- }
7517
- function parseSkillSpectorJson(parsed) {
7518
- const raw = asObject(parsed, "SkillSpector JSON root");
7519
- const degradedReasons = [];
7520
- const usedLlm = raw.llm_used === true || raw.llmUsed === true;
7521
- const scanMode = usedLlm ? "llm" : "static";
7522
- const rawFindings = arrayOf(raw.findings) ?? arrayOf(raw.results) ?? [];
7523
- const findings = rawFindings.map((f) => {
7524
- const o = asObject(f, "SkillSpector finding");
7525
- return {
7526
- providerRuleId: String(o.rule_id ?? o.ruleId ?? o.id ?? "unknown"),
7527
- providerSeverity: String(o.severity ?? o.level ?? "unknown"),
7528
- message: typeof o.message === "string" ? o.message : void 0,
7529
- locations: extractLocations(o)
7530
- };
7531
- });
7532
- let completenessHint;
7533
- const status = String(raw.status ?? raw.scan_status ?? "").toLowerCase();
7534
- if (status && status !== "complete" && status !== "completed" && status !== "success") {
7535
- degradedReasons.push(`SkillSpector reported status "${status}"`);
7536
- completenessHint = status === "partial" ? "partial" : "degraded";
7537
- }
7538
- if (raw.partial === true) {
7539
- degradedReasons.push("SkillSpector reported a partial scan");
7540
- completenessHint = completenessHint ?? "partial";
7541
- }
7542
- if (raw.degraded === true) {
7543
- degradedReasons.push("SkillSpector reported a degraded scan");
7544
- completenessHint = "degraded";
7545
- }
7546
- const coverage = (arrayOf(raw.categories) ?? arrayOf(raw.coverage) ?? []).map((c) => String(c));
7547
- return {
7548
- provider: "skillspector",
7549
- providerVersion: pinnedVersion(raw),
7550
- scanMode,
7551
- coverage,
7552
- findings,
7553
- degradedReasons,
7554
- completenessHint
7555
- };
7556
- }
7557
- function parseSkillSpectorSarif(parsed) {
7558
- const raw = asObject(parsed, "SARIF root");
7559
- const runs = arrayOf(raw.runs) ?? [];
7560
- if (runs.length === 0) {
7561
- return {
7562
- provider: "skillspector",
7563
- findings: [],
7564
- degradedReasons: ["SARIF had no runs"]
7565
- };
7566
- }
7567
- const degradedReasons = [];
7568
- const findings = [];
7569
- let providerVersion;
7570
- const coverage = [];
7571
- for (const runUnknown of runs) {
7572
- const run2 = asObject(runUnknown, "SARIF run");
7573
- const tool = asObject(run2.tool ?? {}, "SARIF tool");
7574
- const driver = asObject(tool.driver ?? {}, "SARIF tool.driver");
7575
- if (!providerVersion) {
7576
- const v = typeof driver.semanticVersion === "string" && driver.semanticVersion || typeof driver.version === "string" && driver.version || "";
7577
- if (v) providerVersion = v;
7578
- }
7579
- const results = arrayOf(run2.results) ?? [];
7580
- for (const resUnknown of results) {
7581
- const res = asObject(resUnknown, "SARIF result");
7582
- findings.push({
7583
- providerRuleId: String(res.ruleId ?? "unknown"),
7584
- providerSeverity: String(res.level ?? "warning"),
7585
- message: extractSarifMessage(res),
7586
- locations: extractSarifLocations(res)
7587
- });
7588
- }
7589
- }
7590
- degradedReasons.push("SARIF import: provider-specific fields may be lost vs JSON form");
7591
- return {
7592
- provider: "skillspector",
7593
- providerVersion,
7594
- scanMode: "static",
7595
- coverage,
7596
- findings,
7597
- degradedReasons
7598
- };
7599
- }
7600
- function asObject(v, what) {
7601
- if (v && typeof v === "object" && !Array.isArray(v)) return v;
7602
- throw new Error(`expected object for ${what}`);
7603
- }
7604
- function arrayOf(v) {
7605
- return Array.isArray(v) ? v : void 0;
7606
- }
7607
- function extractLocations(o) {
7608
- const loc = o.location ?? o.locations ?? o.path;
7609
- if (typeof loc === "string") return [loc];
7610
- if (Array.isArray(loc)) return loc.map((x) => String(x));
7611
- return void 0;
7612
- }
7613
- function extractSarifMessage(res) {
7614
- const msg = res.message;
7615
- if (msg && typeof msg.text === "string") return msg.text;
7616
- return void 0;
7617
- }
7618
- function extractSarifLocations(res) {
7619
- const locs = arrayOf(res.locations);
7620
- if (!locs) return void 0;
7621
- const out = [];
7622
- for (const l of locs) {
7623
- const lo = l;
7624
- const phys = lo?.physicalLocation;
7625
- const art = phys?.artifactLocation;
7626
- const uri = art && typeof art.uri === "string" ? art.uri : void 0;
7627
- const region = phys?.region;
7628
- const line = region && typeof region.startLine === "number" ? region.startLine : void 0;
7629
- if (uri) out.push(line ? `${uri}:${line}` : uri);
7630
- }
7631
- return out.length > 0 ? out : void 0;
7632
- }
7633
-
7634
- // ../../packages/evidence/src/importEvidence.ts
7635
- var ZERO_DIGEST = `sha256:${"0".repeat(64)}`;
7636
- function detectProvider(raw, explicit) {
7637
- if (explicit) return explicit;
7638
- const r = raw;
7639
- if (!r) return "unknown";
7640
- const topTool = r.tool;
7641
- const jsonName = typeof r.scanner === "string" && r.scanner || topTool && typeof topTool.name === "string" && topTool.name || "";
7642
- let sarifName = "";
7643
- const runs = r.runs;
7644
- if (Array.isArray(runs) && runs.length > 0) {
7645
- const run2 = runs[0];
7646
- const tool = run2?.tool;
7647
- const driver = tool?.driver;
7648
- if (driver && typeof driver.name === "string") sarifName = driver.name;
7649
- }
7650
- if (/skillspector/i.test(String(jsonName)) || /skillspector/i.test(sarifName)) {
7651
- return "skillspector";
7652
- }
7653
- return "unknown";
7654
- }
7655
- function importEvidence(rawText, opts = {}) {
7656
- const format = opts.format ?? (looksLikeSarif(rawText) ? "sarif" : "json");
7657
- const rawReportDigest = sha256(rawText);
7658
- let parsed;
7659
- try {
7660
- parsed = JSON.parse(rawText);
7661
- } catch {
7662
- return failClosed(opts.provider ?? "unknown", rawReportDigest, [
7663
- `report is not valid JSON (format=${format})`
7664
- ]);
7665
- }
7666
- const provider = detectProvider(parsed, opts.provider);
7667
- let result;
7668
- try {
7669
- if (provider === "skillspector") {
7670
- result = format === "sarif" ? parseSkillSpectorSarif(parsed) : parseSkillSpectorJson(parsed);
7671
- } else {
7672
- return failClosed(provider, rawReportDigest, [
7673
- `no adapter for provider "${provider}"; evidence not interpreted`
7674
- ]);
7908
+ lines.push(`Discovered ${result.discovered.length} agent config(s):`);
7909
+ lines.push("");
7910
+ const byAgent = /* @__PURE__ */ new Map();
7911
+ for (const config of result.discovered) {
7912
+ const existing = byAgent.get(config.agentType) || [];
7913
+ existing.push(config);
7914
+ byAgent.set(config.agentType, existing);
7915
+ }
7916
+ const sortedAgents = Array.from(byAgent.keys()).sort((a, b) => {
7917
+ const priorityA = result.discovered.find((c) => c.agentType === a)?.priority || "P3";
7918
+ const priorityB = result.discovered.find((c) => c.agentType === b)?.priority || "P3";
7919
+ return priorityA.localeCompare(priorityB);
7920
+ });
7921
+ for (const agentType of sortedAgents) {
7922
+ const configs = byAgent.get(agentType);
7923
+ const priority = configs[0].priority;
7924
+ const agentLabel = formatAgentType(agentType);
7925
+ lines.push(`${agentLabel} (${priority}):`);
7926
+ for (const config of configs) {
7927
+ lines.push(` ${config.configPath}`);
7928
+ }
7929
+ lines.push("");
7930
+ }
7931
+ lines.push(`To scan all: calllint scan --auto`);
7932
+ lines.push(`To scan one: calllint scan --agent ${result.discovered[0].agentType}`);
7675
7933
  }
7676
- } catch (err2) {
7677
- return failClosed(provider, rawReportDigest, [
7678
- `adapter error: ${err2.message}`
7679
- ]);
7934
+ return {
7935
+ exitCode: 0,
7936
+ stdout: lines.join("\n") + "\n",
7937
+ stderr: ""
7938
+ };
7939
+ } catch (error) {
7940
+ const message = error instanceof Error ? error.message : String(error);
7941
+ return {
7942
+ exitCode: 1,
7943
+ stdout: "",
7944
+ stderr: `Error discovering configs: ${message}
7945
+ `
7946
+ };
7680
7947
  }
7681
- return finalizeEnvelope(result, rawReportDigest);
7682
- }
7683
- function failClosed(provider, rawReportDigest, reasons) {
7684
- return {
7685
- schema_version: EVIDENCE_SCHEMA_VERSION,
7686
- provider: provider || "unknown",
7687
- providerVersion: "unknown",
7688
- artifactDigest: ZERO_DIGEST,
7689
- scanMode: "static",
7690
- coverage: [],
7691
- completeness: "failed",
7692
- findings: [],
7693
- rawReportDigest,
7694
- degradedReasons: reasons
7695
- };
7696
7948
  }
7697
- function finalizeEnvelope(r, rawReportDigest) {
7698
- const degradedReasons = [...r.degradedReasons ?? []];
7699
- const rank = { complete: 0, partial: 1, degraded: 2, failed: 3 };
7700
- const rankToCompleteness = ["complete", "partial", "degraded", "failed"];
7701
- let level = rank[r.completenessHint ?? (degradedReasons.length > 0 ? "degraded" : "complete")];
7702
- let providerVersion = r.providerVersion?.trim() || "";
7703
- if (!providerVersion) {
7704
- providerVersion = "unknown";
7705
- degradedReasons.push("provider version not pinned (no release/commit reported)");
7706
- level = Math.max(level, rank.degraded);
7949
+ function formatAgentType(agentType) {
7950
+ switch (agentType) {
7951
+ case "cursor":
7952
+ return "Cursor";
7953
+ case "claude-code":
7954
+ return "Claude Code";
7955
+ case "claude-desktop":
7956
+ return "Claude Desktop";
7957
+ case "vscode":
7958
+ return "VS Code";
7959
+ case "windsurf":
7960
+ return "Windsurf";
7961
+ default:
7962
+ return agentType;
7707
7963
  }
7708
- const completeness = rankToCompleteness[level];
7709
- return {
7710
- schema_version: EVIDENCE_SCHEMA_VERSION,
7711
- provider: r.provider,
7712
- providerVersion,
7713
- artifactDigest: r.artifactDigest ?? ZERO_DIGEST,
7714
- scanMode: r.scanMode ?? "static",
7715
- coverage: r.coverage ?? [],
7716
- completeness,
7717
- findings: r.findings,
7718
- rawReportDigest,
7719
- startedAt: r.startedAt,
7720
- finishedAt: r.finishedAt,
7721
- degradedReasons
7722
- };
7723
- }
7724
- function looksLikeSarif(text) {
7725
- return /"runs"\s*:/.test(text) && /sarif/i.test(text);
7726
7964
  }
7727
7965
 
7728
7966
  // src/commands/evidence.ts
7967
+ import { readFileSync as readFileSync16 } from "node:fs";
7968
+ import { resolve as resolve8 } from "node:path";
7729
7969
  function evidenceCommand(args, deps) {
7730
7970
  const subcommand = args.positionals[0];
7731
7971
  if (!subcommand || subcommand === "help") {
@@ -7836,6 +8076,235 @@ import { existsSync as existsSync11, mkdirSync as mkdirSync5, readdirSync as rea
7836
8076
  import { basename as basename4, join as join16, resolve as resolvePath4 } from "node:path";
7837
8077
  import { homedir, userInfo } from "node:os";
7838
8078
 
8079
+ // ../../packages/flow-analyzer/src/flowRules.ts
8080
+ var UNTRUSTED_OR_SENSITIVE = /* @__PURE__ */ new Set([
8081
+ "sensitive.secret",
8082
+ "sensitive.private_data",
8083
+ "untrusted.public_content",
8084
+ "untrusted.tool_output",
8085
+ "untrusted.peer_agent",
8086
+ "untrusted.memory"
8087
+ ]);
8088
+ var TRUSTED = /* @__PURE__ */ new Set([
8089
+ "trusted.policy",
8090
+ "trusted.user_explicit",
8091
+ "trusted.local_project",
8092
+ "trusted.signed_component"
8093
+ ]);
8094
+ function isUntrustedOrSensitive(ts) {
8095
+ return ts !== void 0 && UNTRUSTED_OR_SENSITIVE.has(ts);
8096
+ }
8097
+ function isTrusted(ts) {
8098
+ return ts !== void 0 && TRUSTED.has(ts);
8099
+ }
8100
+ function sinkKind(sink) {
8101
+ const key = `${sink.action}:${sink.resource}`;
8102
+ if (key === "send:network" || key === "connect:network") return "network";
8103
+ if (key === "send:message") return "message";
8104
+ if (key === "spend:financial") return "financial";
8105
+ return "other";
8106
+ }
8107
+ function hasExternalDestination(sink) {
8108
+ return typeof sink.destination === "string" && sink.destination.length > 0;
8109
+ }
8110
+ var FLOW_RULES = [
8111
+ // CL-FLOW-001 — the canonical toxic flow: untrusted/sensitive data → external network
8112
+ // egress with a concrete outbound destination. The motivating incident (new9): a
8113
+ // secret / injected public content leaving to an attacker-controlled host.
8114
+ {
8115
+ id: "CL-FLOW-001",
8116
+ when: (s, k) => isUntrustedOrSensitive(s.trustSource) && sinkKind(k) === "network" && hasExternalDestination(k),
8117
+ decisionHint: "BLOCK",
8118
+ riskClass: "critical"
8119
+ },
8120
+ // CL-FLOW-002 — untrusted/sensitive data → financial action (spend). Money leaving is
8121
+ // irreversible; a tainted source driving it is a blocker regardless of destination.
8122
+ {
8123
+ id: "CL-FLOW-002",
8124
+ when: (s, k) => isUntrustedOrSensitive(s.trustSource) && sinkKind(k) === "financial",
8125
+ decisionHint: "BLOCK",
8126
+ riskClass: "critical"
8127
+ },
8128
+ // CL-FLOW-003 — untrusted/sensitive data → external network egress WITHOUT a pinned
8129
+ // destination (host unknown). Still dangerous, but the sink is less determined, so it
8130
+ // warrants human review rather than an outright block.
8131
+ {
8132
+ id: "CL-FLOW-003",
8133
+ when: (s, k) => isUntrustedOrSensitive(s.trustSource) && sinkKind(k) === "network" && !hasExternalDestination(k),
8134
+ decisionHint: "REVIEW",
8135
+ riskClass: "high"
8136
+ },
8137
+ // CL-FLOW-004 — untrusted/sensitive data → outbound messaging (email/chat). Exfiltration
8138
+ // via a messaging channel; review-worthy.
8139
+ {
8140
+ id: "CL-FLOW-004",
8141
+ when: (s, k) => isUntrustedOrSensitive(s.trustSource) && sinkKind(k) === "message",
8142
+ decisionHint: "REVIEW",
8143
+ riskClass: "high"
8144
+ },
8145
+ // CL-FLOW-ALLOW-001 — an established TRUSTED source reaching an egress sink is the
8146
+ // benign counterpart (ADR 0041 §2: trusted.user_explicit → send is routine). Only fires
8147
+ // when the source is positively trusted — never on unknown (I-04).
8148
+ {
8149
+ id: "CL-FLOW-ALLOW-001",
8150
+ when: (s, k) => isTrusted(s.trustSource) && sinkKind(k) !== "other",
8151
+ decisionHint: "ALLOW",
8152
+ riskClass: "none"
8153
+ },
8154
+ // CL-FLOW-REVIEW-000 — fail-safe catch-all. Any composition not positively classified
8155
+ // above (including one whose source trust could not be established) is REVIEW, never
8156
+ // ALLOW/SAFE. This keeps the classifier total and dangerous-flow-never-SAFE.
8157
+ {
8158
+ id: "CL-FLOW-REVIEW-000",
8159
+ when: () => true,
8160
+ decisionHint: "REVIEW",
8161
+ riskClass: "medium"
8162
+ }
8163
+ ];
8164
+ function classifyFlow(source, sink) {
8165
+ for (const rule2 of FLOW_RULES) {
8166
+ if (rule2.when(source, sink)) {
8167
+ return { ruleId: rule2.id, decisionHint: rule2.decisionHint, riskClass: rule2.riskClass };
8168
+ }
8169
+ }
8170
+ return { ruleId: "CL-FLOW-REVIEW-000", decisionHint: "REVIEW", riskClass: "medium" };
8171
+ }
8172
+
8173
+ // ../../packages/flow-analyzer/src/buildFlows.ts
8174
+ var CLASSIFIED_SOURCE_TRUST = /* @__PURE__ */ new Set([
8175
+ "sensitive.secret",
8176
+ "sensitive.private_data",
8177
+ "untrusted.public_content",
8178
+ "untrusted.tool_output",
8179
+ "untrusted.peer_agent",
8180
+ "untrusted.memory",
8181
+ "trusted.policy",
8182
+ "trusted.user_explicit",
8183
+ "trusted.local_project",
8184
+ "trusted.signed_component"
8185
+ ]);
8186
+ var EGRESS_SINKS = /* @__PURE__ */ new Set([
8187
+ "send:network",
8188
+ "send:message",
8189
+ "connect:network",
8190
+ "spend:financial"
8191
+ ]);
8192
+ function isSource(c) {
8193
+ return c.action === "read" && c.trustSource !== void 0 && CLASSIFIED_SOURCE_TRUST.has(c.trustSource);
8194
+ }
8195
+ function isSink(c) {
8196
+ return EGRESS_SINKS.has(`${c.action}:${c.resource}`);
8197
+ }
8198
+ function sourceFamily(ts) {
8199
+ if (ts.startsWith("sensitive.")) return "sensitive";
8200
+ if (ts.startsWith("trusted.")) return "trusted";
8201
+ return "untrusted";
8202
+ }
8203
+ var SEVERITY = {
8204
+ none: 0,
8205
+ low: 20,
8206
+ medium: 50,
8207
+ high: 75,
8208
+ critical: 95
8209
+ };
8210
+ function cmp3(a, b) {
8211
+ return a < b ? -1 : a > b ? 1 : 0;
8212
+ }
8213
+ function capKey(t) {
8214
+ const c = t.cap;
8215
+ return [
8216
+ t.manifestDigest,
8217
+ c.trustSource ?? "",
8218
+ c.action,
8219
+ c.resource,
8220
+ c.scope ?? "",
8221
+ c.destination ?? "",
8222
+ c.evidenceSource
8223
+ ].join("|");
8224
+ }
8225
+ function sealFlow(flow) {
8226
+ return { ...flow, digest: hashJson(flow) };
8227
+ }
8228
+ function buildFlows(manifests) {
8229
+ const tagged = manifests.flatMap((m) => m.capabilities.map((cap) => ({ cap, manifestDigest: m.digest }))).sort((a, b) => cmp3(capKey(a), capKey(b)));
8230
+ const sources = tagged.filter((t) => isSource(t.cap));
8231
+ const sinks = tagged.filter((t) => isSink(t.cap));
8232
+ const byDigest = /* @__PURE__ */ new Map();
8233
+ for (const src of sources) {
8234
+ for (const snk of sinks) {
8235
+ if (src.cap === snk.cap) continue;
8236
+ const ts = src.cap.trustSource;
8237
+ const outcome = classifyFlow(src.cap, snk.cap);
8238
+ const risk = {
8239
+ class: outcome.riskClass,
8240
+ severity: SEVERITY[outcome.riskClass]
8241
+ };
8242
+ const sinkTag = `${snk.cap.action}-${snk.cap.resource}`;
8243
+ const flowId = `flow:${sourceFamily(ts)}-to-${sinkTag}`;
8244
+ const evidence = [.../* @__PURE__ */ new Set([src.cap.evidenceSource, snk.cap.evidenceSource])].sort();
8245
+ const authorityDigests = [.../* @__PURE__ */ new Set([src.manifestDigest, snk.manifestDigest])].sort();
8246
+ const flow = sealFlow({
8247
+ schema: FLOW_SCHEMA_VERSION,
8248
+ flowId,
8249
+ source: { trustSource: ts, evidence: [src.cap.evidenceSource] },
8250
+ steps: [{ action: src.cap.action, resource: src.cap.resource, scope: src.cap.scope }],
8251
+ sink: {
8252
+ action: snk.cap.action,
8253
+ resource: snk.cap.resource,
8254
+ destination: snk.cap.destination
8255
+ },
8256
+ risk,
8257
+ decisionHint: outcome.decisionHint,
8258
+ evidence,
8259
+ authorityDigests
8260
+ });
8261
+ if (!byDigest.has(flow.digest)) byDigest.set(flow.digest, flow);
8262
+ }
8263
+ }
8264
+ return [...byDigest.values()].sort(
8265
+ (a, b) => cmp3(a.flowId, b.flowId) || cmp3(a.digest, b.digest)
8266
+ );
8267
+ }
8268
+
8269
+ // ../../packages/flow-analyzer/src/foldFlows.ts
8270
+ function hintToVerdict(hint) {
8271
+ switch (hint) {
8272
+ case "BLOCK":
8273
+ return "BLOCK";
8274
+ case "REVIEW":
8275
+ return "REVIEW";
8276
+ default:
8277
+ return null;
8278
+ }
8279
+ }
8280
+ function foldFlowsIntoReasons(flows) {
8281
+ const byKey = /* @__PURE__ */ new Map();
8282
+ for (const flow of flows) {
8283
+ const contributes = hintToVerdict(flow.decisionHint);
8284
+ if (contributes === null) continue;
8285
+ const firstEvidence = flow.evidence[0];
8286
+ const evidenceSource = firstEvidence ? `${flow.flowId} (${firstEvidence})` : flow.flowId;
8287
+ const reason = {
8288
+ code: "TOXIC_FLOW_COMPOSITION",
8289
+ evidenceSource,
8290
+ contributes
8291
+ };
8292
+ const prev = byKey.get(evidenceSource);
8293
+ if (!prev || severity(contributes) > severity(prev.contributes)) {
8294
+ byKey.set(evidenceSource, reason);
8295
+ }
8296
+ }
8297
+ return [...byKey.values()].sort(
8298
+ (a, b) => cmp4(a.evidenceSource, b.evidenceSource) || severity(b.contributes) - severity(a.contributes)
8299
+ );
8300
+ }
8301
+ function severity(v) {
8302
+ return v === "BLOCK" ? 3 : v === "UNKNOWN" ? 2 : v === "REVIEW" ? 1 : 0;
8303
+ }
8304
+ function cmp4(a, b) {
8305
+ return a < b ? -1 : a > b ? 1 : 0;
8306
+ }
8307
+
7839
8308
  // ../../packages/install-planner/src/buildPlan.ts
7840
8309
  function ptr(segment) {
7841
8310
  return segment.replace(/~/g, "~0").replace(/\//g, "~1");
@@ -8681,7 +9150,7 @@ function writePlanFile(plan, deps) {
8681
9150
  writeFileSync10(file, JSON.stringify(plan, null, 2) + "\n", "utf8");
8682
9151
  return file;
8683
9152
  }
8684
- function loadEvidence(file, deps, format, provider) {
9153
+ function loadEvidence2(file, deps, format, provider) {
8685
9154
  let rawText;
8686
9155
  try {
8687
9156
  rawText = readFileSync18(resolvePath4(deps.cwd, file), "utf8");
@@ -8720,7 +9189,7 @@ function trustPrepare(args, deps) {
8720
9189
  const providerFlag = typeof args.flags["provider"] === "string" ? args.flags["provider"] : void 0;
8721
9190
  let evidence;
8722
9191
  if (evidenceFile) {
8723
- const imported = loadEvidence(evidenceFile, deps, format, providerFlag);
9192
+ const imported = loadEvidence2(evidenceFile, deps, format, providerFlag);
8724
9193
  if ("error" in imported) {
8725
9194
  return { stdout: "", stderr: `Error: ${imported.error}`, exitCode: imported.exitCode };
8726
9195
  }
@@ -8730,7 +9199,9 @@ function trustPrepare(args, deps) {
8730
9199
  const authority = buildAuthorityForTarget(input, artifact.digest ?? null);
8731
9200
  const policyPath = flagStr(args.flags, "policy");
8732
9201
  const policy = loadPolicyOrDefault(policyPath ? resolvePath4(deps.cwd, policyPath) : void 0);
8733
- const decision = artifact.resolution === "resolved" ? decideOverAuthority({ authority, evidence, policy }) : void 0;
9202
+ const flows = buildFlows([authority]);
9203
+ const flowReasons = foldFlowsIntoReasons(flows);
9204
+ const decision = artifact.resolution === "resolved" ? decideOverAuthority({ authority, evidence, policy, flowReasons }) : void 0;
8734
9205
  let plan;
8735
9206
  const hostFlag = flagStr(args.flags, "host");
8736
9207
  if (hostFlag && decision && decision.verdict !== "UNKNOWN") {
@@ -8757,10 +9228,18 @@ function trustPrepare(args, deps) {
8757
9228
  plan written: ${file}
8758
9229
  `;
8759
9230
  }
9231
+ const showFlows = flagBool(args.flags, "flows");
8760
9232
  if (flagBool(args.flags, "json")) {
8761
- return { stdout: JSON.stringify(preparation, null, 2), stderr: "", exitCode };
9233
+ const payload = showFlows ? { preparation, flows } : preparation;
9234
+ return { stdout: JSON.stringify(payload, null, 2), stderr: "", exitCode };
8762
9235
  }
8763
- return { stdout: renderPreparation(preparation) + planNote, stderr: "", exitCode };
9236
+ const flowNote = showFlows ? renderFlows(flows) : "";
9237
+ const conversion = renderConversionPrompt(preparation);
9238
+ return {
9239
+ stdout: renderPreparation(preparation) + flowNote + planNote + conversion,
9240
+ stderr: "",
9241
+ exitCode
9242
+ };
8764
9243
  }
8765
9244
  function loadPreparation(args, deps) {
8766
9245
  const file = args.positionals[1];
@@ -9055,6 +9534,46 @@ This is the READ-ONLY half of the Trust Gateway. It touched no live config
9055
9534
  `;
9056
9535
  return out;
9057
9536
  }
9537
+ function renderConversionPrompt(p) {
9538
+ const d = p.decision;
9539
+ if (!d || d.verdict === "BLOCK" || d.verdict === "UNKNOWN") return "";
9540
+ return `
9541
+ Next step (nothing is persisted unless you run one of these):
9542
+ \u2022 Record this surface as approved: calllint approve
9543
+ \u2022 Re-decide on every authority change: calllint guard install --host git
9544
+ \u2022 Gate pull requests in CI: calllint guard install --host github
9545
+ \u2022 Teach your agent the safety rule: calllint gen-rule --host claude --write
9546
+ `;
9547
+ }
9548
+ var FLOW_HINT_SYMBOL = {
9549
+ BLOCK: "\u26D4 BLOCK",
9550
+ REVIEW: "\u26A0 REVIEW",
9551
+ ALLOW: "\u{1F6E1} ALLOW"
9552
+ };
9553
+ function renderFlows(flows) {
9554
+ if (flows.length === 0) {
9555
+ return `
9556
+ toxic-flows: (none \u2014 no cross-capability composition detected)
9557
+ `;
9558
+ }
9559
+ let out = `
9560
+ toxic-flows: ${flows.length} path(s) [calllint.flow.v0]
9561
+ `;
9562
+ for (const f of flows) {
9563
+ const dst = f.sink.destination ? ` \u2192 ${f.sink.destination}` : "";
9564
+ out += ` ${FLOW_HINT_SYMBOL[f.decisionHint] ?? f.decisionHint} ${f.flowId} (${f.risk.class})
9565
+ `;
9566
+ out += ` source: ${f.source.trustSource} \u2192 sink: ${f.sink.action} ${f.sink.resource}${dst}
9567
+ `;
9568
+ out += ` evidence: ${f.evidence.join(", ")}
9569
+ `;
9570
+ }
9571
+ out += ` A dangerous flow is folded into the decision as a TOXIC_FLOW_COMPOSITION reason;
9572
+ `;
9573
+ out += ` it can only raise the verdict, never lower it. An ALLOW flow contributes nothing.
9574
+ `;
9575
+ return out;
9576
+ }
9058
9577
  function renderExplanation(p) {
9059
9578
  let out = `
9060
9579
  CallLint trust explain
@@ -9224,6 +9743,9 @@ OPTIONS (prepare)
9224
9743
  only for a non-blocking decision (SAFE/REVIEW).
9225
9744
  --host-config <path> Override the host config path (default: ~/.claude.json)
9226
9745
  --write-plan Persist the plan to .calllint/plans/<plan-id>.json
9746
+ --flows Show static toxic-flow paths (calllint.flow.v0) behind the
9747
+ decision's TOXIC_FLOW_COMPOSITION reasons. With --json, emits
9748
+ { preparation, flows }. A dangerous flow only raises the verdict.
9227
9749
  --no-llm Default posture: no LLM in the verdict path (accepted, no-op)
9228
9750
  --json Emit the raw calllint.trust-preparation.v0 document
9229
9751
 
@@ -9268,6 +9790,209 @@ SEE ALSO
9268
9790
  `;
9269
9791
  }
9270
9792
 
9793
+ // src/commands/guard.ts
9794
+ import { existsSync as existsSync12, readFileSync as readFileSync19, writeFileSync as writeFileSync11, mkdirSync as mkdirSync6 } from "node:fs";
9795
+ import { dirname as dirname6, join as join17, resolve as resolve10 } from "node:path";
9796
+ var GUARD_HOSTS = ["git", "github"];
9797
+ function guardConfigPath(cwd) {
9798
+ return join17(cwd, ".calllint", "guard.json");
9799
+ }
9800
+ function readGuardConfig(cwd) {
9801
+ const path = guardConfigPath(cwd);
9802
+ if (!existsSync12(path)) return void 0;
9803
+ try {
9804
+ return JSON.parse(readFileSync19(path, "utf8"));
9805
+ } catch {
9806
+ return void 0;
9807
+ }
9808
+ }
9809
+ function isDisabled(cwd, env) {
9810
+ if (env["CALLLINT_GUARD"] === "0") return true;
9811
+ return readGuardConfig(cwd)?.enabled === false;
9812
+ }
9813
+ function guardCommand(args, deps) {
9814
+ const sub = args.positionals[0];
9815
+ const env = deps.env ?? process.env;
9816
+ switch (sub) {
9817
+ case "install":
9818
+ return guardInstall(args, deps);
9819
+ case "status":
9820
+ return guardStatus(args, deps, env);
9821
+ case "disable":
9822
+ return guardSetEnabled(deps, false);
9823
+ case "enable":
9824
+ return guardSetEnabled(deps, true);
9825
+ case void 0:
9826
+ return guardRun(args, deps, env);
9827
+ default:
9828
+ return {
9829
+ stdout: "",
9830
+ stderr: `Unknown guard subcommand: ${sub}
9831
+ Run \`calllint guard\` (assess) or \`calllint guard install|status|disable|enable\`.`,
9832
+ exitCode: EXIT.USAGE
9833
+ };
9834
+ }
9835
+ }
9836
+ function exitForAction(a) {
9837
+ switch (a.action) {
9838
+ case "silent":
9839
+ case "note":
9840
+ return EXIT.OK;
9841
+ case "prompt":
9842
+ return EXIT.REVIEW;
9843
+ case "request-evidence":
9844
+ return EXIT.UNKNOWN;
9845
+ case "refuse":
9846
+ return EXIT.BLOCK;
9847
+ case "fail-closed":
9848
+ return EXIT.ERROR;
9849
+ }
9850
+ }
9851
+ function guardRun(args, deps, env) {
9852
+ const json = flagBool(args.flags, "json");
9853
+ if (isDisabled(deps.cwd, env)) {
9854
+ const note = "Continuous Guard is disabled (CALLLINT_GUARD=0 or .calllint/guard.json).";
9855
+ return {
9856
+ stdout: json ? JSON.stringify({ enabled: false, note }) : note,
9857
+ exitCode: EXIT.OK
9858
+ };
9859
+ }
9860
+ const approvedPath = flagStr(args.flags, "approved") ?? defaultApprovedPath(deps.cwd);
9861
+ const approved = readApproved(approvedPath);
9862
+ if (!approved) {
9863
+ return {
9864
+ stdout: "",
9865
+ stderr: `No approved baseline at ${approvedPath}. Run \`calllint approve\` first.`,
9866
+ exitCode: EXIT.USAGE
9867
+ };
9868
+ }
9869
+ let assessment;
9870
+ try {
9871
+ const current = decideRepoSurfaces(deps.cwd, { now: deps.now, generatedAt: deps.generatedAt });
9872
+ const drift = verifyApproved(current, approved, deps.generatedAt);
9873
+ assessment = assessGuardDrift(drift);
9874
+ } catch (err2) {
9875
+ assessment = guardFailClosed(err2 instanceof Error ? err2.message : String(err2));
9876
+ }
9877
+ const exitCode = exitForAction(assessment);
9878
+ if (json) {
9879
+ return { stdout: JSON.stringify(assessment), exitCode };
9880
+ }
9881
+ if (assessment.action === "silent") {
9882
+ return { stdout: "", exitCode: EXIT.OK };
9883
+ }
9884
+ const prefix = assessment.action === "fail-closed" ? "GUARD FAILED CLOSED" : assessment.action === "refuse" ? "BLOCK" : assessment.action === "request-evidence" ? "UNKNOWN" : assessment.action === "prompt" ? "REVIEW" : "NOTE";
9885
+ const failLine = assessment.failure ? `
9886
+ reason: ${assessment.failure}` : "";
9887
+ return { stdout: `${prefix}: ${assessment.summary}${failLine}`, exitCode };
9888
+ }
9889
+ function isGuardHost(v) {
9890
+ return v !== void 0 && GUARD_HOSTS.includes(v);
9891
+ }
9892
+ var GIT_HOOK = `#!/usr/bin/env bash
9893
+ # CallLint Continuous Guard (ADR 0045). Re-decides the agent-tool authority
9894
+ # surface on commit; silent when nothing changed. Generated by \`calllint guard install\`.
9895
+ # CallLint is static and NEVER executes a scanned server.
9896
+ npx -y calllint guard --no-emoji
9897
+ `;
9898
+ function guardArtifact(host) {
9899
+ if (host === "git") {
9900
+ return { path: join17(".git", "hooks", "pre-commit"), content: GIT_HOOK, label: "git pre-commit hook" };
9901
+ }
9902
+ return {
9903
+ path: join17(".github", "workflows", "calllint.yml"),
9904
+ content: renderCiGate({ mode: "drift" }),
9905
+ label: "GitHub Actions drift-gate workflow"
9906
+ };
9907
+ }
9908
+ function guardInstall(args, deps) {
9909
+ const host = flagStr(args.flags, "host");
9910
+ if (!host) {
9911
+ const list = GUARD_HOSTS.map((h2) => ` ${h2.padEnd(8)} \u2192 ${guardArtifact(h2).label}`).join("\n");
9912
+ return {
9913
+ stdout: `Usage: calllint guard install --host <host>
9914
+
9915
+ Hosts:
9916
+ ${list}`,
9917
+ exitCode: EXIT.OK
9918
+ };
9919
+ }
9920
+ if (!isGuardHost(host)) {
9921
+ return {
9922
+ stdout: "",
9923
+ stderr: `Unknown guard host: ${host}
9924
+ Run \`calllint guard install\` to list hosts.`,
9925
+ exitCode: EXIT.USAGE
9926
+ };
9927
+ }
9928
+ const art = guardArtifact(host);
9929
+ const rel = flagStr(args.flags, "out") ?? art.path;
9930
+ if (deps.writeFile === false) {
9931
+ return { stdout: `Would write ${rel} (${art.label})`, exitCode: EXIT.OK };
9932
+ }
9933
+ const abs = resolve10(deps.cwd, rel);
9934
+ try {
9935
+ mkdirSync6(dirname6(abs), { recursive: true });
9936
+ writeFileSync11(abs, art.content, "utf8");
9937
+ } catch (e) {
9938
+ return {
9939
+ stdout: "",
9940
+ stderr: `Failed to write ${rel}: ${e instanceof Error ? e.message : String(e)}`,
9941
+ exitCode: EXIT.ERROR
9942
+ };
9943
+ }
9944
+ const note = host === "git" ? `
9945
+ Make it executable: chmod +x ${rel}` : "";
9946
+ return { stdout: `Wrote ${rel} (${art.label})${note}`, exitCode: EXIT.OK };
9947
+ }
9948
+ function guardStatus(args, deps, env) {
9949
+ const approvedPath = flagStr(args.flags, "approved") ?? defaultApprovedPath(deps.cwd);
9950
+ const hasBaseline = existsSync12(approvedPath);
9951
+ const disabled = isDisabled(deps.cwd, env);
9952
+ const gitHook = existsSync12(join17(deps.cwd, ".git", "hooks", "pre-commit"));
9953
+ const ghWorkflow = existsSync12(join17(deps.cwd, ".github", "workflows", "calllint.yml"));
9954
+ const status = {
9955
+ enabled: !disabled,
9956
+ disabledBy: disabled ? env["CALLLINT_GUARD"] === "0" ? "env:CALLLINT_GUARD=0" : "flag:.calllint/guard.json" : null,
9957
+ approvedBaseline: hasBaseline ? approvedPath : null,
9958
+ installedHooks: {
9959
+ "git:pre-commit": gitHook,
9960
+ "github:workflow": ghWorkflow
9961
+ }
9962
+ };
9963
+ if (flagBool(args.flags, "json")) {
9964
+ return { stdout: JSON.stringify(status), exitCode: EXIT.OK };
9965
+ }
9966
+ const lines = [
9967
+ `Continuous Guard: ${status.enabled ? "enabled" : `disabled (${status.disabledBy})`}`,
9968
+ `Approved baseline: ${hasBaseline ? approvedPath : "none \u2014 run `calllint approve`"}`,
9969
+ `git pre-commit hook: ${gitHook ? "installed" : "not installed"}`,
9970
+ `GitHub workflow: ${ghWorkflow ? "installed" : "not installed"}`
9971
+ ];
9972
+ return { stdout: lines.join("\n"), exitCode: EXIT.OK };
9973
+ }
9974
+ function guardSetEnabled(deps, enabled) {
9975
+ const cfg = { schemaVersion: "calllint.guard-config.v0", enabled };
9976
+ if (deps.writeFile === false) {
9977
+ return { stdout: enabled ? "Would enable Continuous Guard" : "Would disable Continuous Guard", exitCode: EXIT.OK };
9978
+ }
9979
+ const path = guardConfigPath(deps.cwd);
9980
+ try {
9981
+ mkdirSync6(dirname6(path), { recursive: true });
9982
+ writeFileSync11(path, JSON.stringify(cfg, null, 2), "utf8");
9983
+ } catch (e) {
9984
+ return {
9985
+ stdout: "",
9986
+ stderr: `Failed to write ${path}: ${e instanceof Error ? e.message : String(e)}`,
9987
+ exitCode: EXIT.ERROR
9988
+ };
9989
+ }
9990
+ return {
9991
+ stdout: enabled ? "Continuous Guard enabled (.calllint/guard.json)." : "Continuous Guard disabled (.calllint/guard.json). Re-enable with `calllint guard enable`.",
9992
+ exitCode: EXIT.OK
9993
+ };
9994
+ }
9995
+
9271
9996
  // src/run.ts
9272
9997
  function run(argv, deps) {
9273
9998
  const args = parseArgs(argv);
@@ -9346,6 +10071,13 @@ function run(argv, deps) {
9346
10071
  return evidenceCommand(args, { cwd: deps.cwd });
9347
10072
  case "trust":
9348
10073
  return trustCommand(args, { cwd: deps.cwd, generatedAt: deps.generatedAt, toolVersion: deps.toolVersion });
10074
+ case "guard":
10075
+ return guardCommand(args, {
10076
+ cwd: deps.cwd,
10077
+ now: deps.now,
10078
+ generatedAt: deps.generatedAt,
10079
+ writeFile: deps.writeCacheFile
10080
+ });
9349
10081
  case "gen-rule":
9350
10082
  return genRuleCommand(args, { cwd: deps.cwd });
9351
10083
  case "policy":
@@ -9619,13 +10351,13 @@ async function breathe(argv, deps = {}) {
9619
10351
  }
9620
10352
 
9621
10353
  // src/version.ts
9622
- import { readFileSync as readFileSync19 } from "node:fs";
10354
+ import { readFileSync as readFileSync20 } from "node:fs";
9623
10355
  import { fileURLToPath } from "node:url";
9624
- import { dirname as dirname6, join as join17 } from "node:path";
10356
+ import { dirname as dirname7, join as join18 } from "node:path";
9625
10357
  function resolveToolVersion() {
9626
10358
  try {
9627
- const pkgPath = join17(dirname6(fileURLToPath(import.meta.url)), "..", "package.json");
9628
- const pkg = JSON.parse(readFileSync19(pkgPath, "utf8"));
10359
+ const pkgPath = join18(dirname7(fileURLToPath(import.meta.url)), "..", "package.json");
10360
+ const pkg = JSON.parse(readFileSync20(pkgPath, "utf8"));
9629
10361
  return typeof pkg.version === "string" && pkg.version.length > 0 ? pkg.version : "unknown";
9630
10362
  } catch {
9631
10363
  return "unknown";
@@ -9635,7 +10367,7 @@ function resolveToolVersion() {
9635
10367
  // src/index.ts
9636
10368
  function readStdin() {
9637
10369
  try {
9638
- return readFileSync20(0, "utf8");
10370
+ return readFileSync21(0, "utf8");
9639
10371
  } catch {
9640
10372
  return "";
9641
10373
  }