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.
- package/dist/index.js +1159 -427
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as 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
|
|
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/
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
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/
|
|
4462
|
-
|
|
4463
|
-
|
|
4464
|
-
|
|
4465
|
-
|
|
4466
|
-
|
|
4467
|
-
"
|
|
4468
|
-
|
|
4469
|
-
".
|
|
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
|
|
4482
|
-
|
|
4483
|
-
|
|
4484
|
-
|
|
4485
|
-
const
|
|
4486
|
-
|
|
4487
|
-
|
|
4488
|
-
|
|
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
|
-
|
|
4505
|
-
|
|
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 (
|
|
4509
|
-
|
|
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
|
-
|
|
4525
|
-
|
|
4526
|
-
|
|
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
|
|
4554
|
-
const
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
4558
|
-
|
|
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
|
|
4561
|
-
const
|
|
4562
|
-
|
|
4563
|
-
|
|
4564
|
-
|
|
4565
|
-
|
|
4566
|
-
|
|
4567
|
-
|
|
4568
|
-
|
|
4569
|
-
|
|
4570
|
-
|
|
4571
|
-
|
|
4572
|
-
|
|
4573
|
-
|
|
4574
|
-
|
|
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
|
-
|
|
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
|
-
|
|
4581
|
-
|
|
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
|
|
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
|
-
|
|
5099
|
-
|
|
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
|
-
|
|
7677
|
-
|
|
7678
|
-
|
|
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
|
|
7698
|
-
|
|
7699
|
-
|
|
7700
|
-
|
|
7701
|
-
|
|
7702
|
-
|
|
7703
|
-
|
|
7704
|
-
|
|
7705
|
-
|
|
7706
|
-
|
|
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
|
|
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 =
|
|
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
|
|
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
|
-
|
|
9233
|
+
const payload = showFlows ? { preparation, flows } : preparation;
|
|
9234
|
+
return { stdout: JSON.stringify(payload, null, 2), stderr: "", exitCode };
|
|
8762
9235
|
}
|
|
8763
|
-
|
|
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
|
|
10354
|
+
import { readFileSync as readFileSync20 } from "node:fs";
|
|
9623
10355
|
import { fileURLToPath } from "node:url";
|
|
9624
|
-
import { dirname as
|
|
10356
|
+
import { dirname as dirname7, join as join18 } from "node:path";
|
|
9625
10357
|
function resolveToolVersion() {
|
|
9626
10358
|
try {
|
|
9627
|
-
const pkgPath =
|
|
9628
|
-
const pkg = JSON.parse(
|
|
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
|
|
10370
|
+
return readFileSync21(0, "utf8");
|
|
9639
10371
|
} catch {
|
|
9640
10372
|
return "";
|
|
9641
10373
|
}
|