calllint 1.0.1 → 1.3.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 +3450 -311
- package/package.json +6 -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 readFileSync20 } from "node:fs";
|
|
5
5
|
import { execFileSync } from "node:child_process";
|
|
6
6
|
|
|
7
7
|
// src/args.ts
|
|
@@ -58,16 +58,25 @@ USAGE
|
|
|
58
58
|
|
|
59
59
|
COMMANDS
|
|
60
60
|
check [target] Compact safety decision for an MCP config or install snippet
|
|
61
|
+
inventory List all discovered agent configs (auto-discovery, no scan)
|
|
61
62
|
scan-all Scan every agent-tool surface in the repo (compact table)
|
|
62
63
|
explain <server> Explain the verdict for one server from the last scan
|
|
63
64
|
verify [target] Compare a fresh scan against the baseline (drift / rug-pull)
|
|
64
65
|
|
|
65
66
|
Advanced:
|
|
66
67
|
scan [target] Full ScanReport for an MCP config / npm:<pkg> / github:<repo>
|
|
68
|
+
scan --auto Discover and scan all agent configs (auto-discovery)
|
|
69
|
+
scan --agent <type> Discover and scan a specific agent (cursor, claude-code, claude-desktop, vscode, windsurf)
|
|
70
|
+
action inspect <f> Preflight a planned external action (calllint.action.v0)
|
|
71
|
+
inbox inspect <f> Preflight a normalized agent inbox event
|
|
72
|
+
evidence import <f> Import a third-party scanner report as evidence (no re-scoring)
|
|
73
|
+
trust prepare <target> Read-only Trust Gateway preview: resolve to a digest-pinned identity
|
|
67
74
|
diagnostics [target] Emit editor/agent-host diagnostics JSON (calllint.diagnostics.v0)
|
|
68
75
|
baseline [target] Record the approved risk surface as a baseline
|
|
69
76
|
approve Record the repo-wide capability surface as approved state (L4)
|
|
70
|
-
receipt verify <f>
|
|
77
|
+
receipt verify <f> Validate a calllint.receipt.v0 (structure + signature if present)
|
|
78
|
+
receipt sign <f> Sign a receipt with a local key (--key; development/testing)
|
|
79
|
+
receipt keygen Generate a local ed25519 keypair (--out; development/testing)
|
|
71
80
|
gen-rule --host <h> Emit the CallLint agent-safety rule for a host (CLAUDE.md, etc.)
|
|
72
81
|
policy init Write a default calllint.policy.json
|
|
73
82
|
policy explain Show the effective policy
|
|
@@ -85,6 +94,8 @@ TARGETS
|
|
|
85
94
|
github:<owner/repo>[@ref] A GitHub repo (requires --online)
|
|
86
95
|
|
|
87
96
|
SCAN OPTIONS
|
|
97
|
+
--auto Discover and scan all agent configs (P0+P1: Cursor, Claude Code, Claude Desktop, VS Code, Windsurf)
|
|
98
|
+
--agent <type> Discover and scan a specific agent type
|
|
88
99
|
--changed Scan only the agent-tool configs changed in the git diff
|
|
89
100
|
--json Emit the ScanReport JSON (stable, emoji-free)
|
|
90
101
|
--compact One line per server
|
|
@@ -93,7 +104,7 @@ SCAN OPTIONS
|
|
|
93
104
|
--markdown Emit Markdown for PR comments / GitHub Step Summary
|
|
94
105
|
--html Emit a self-contained HTML report
|
|
95
106
|
--badge Emit a shields.io endpoint badge JSON (SAFE/REVIEW/UNKNOWN/BLOCK)
|
|
96
|
-
--receipt
|
|
107
|
+
--receipt Generate a cryptographically-verifiable audit receipt (for CI/compliance)
|
|
97
108
|
--receipt-out <f> Receipt output path (default: calllint-receipt.json)
|
|
98
109
|
--policy <file> Use a policy file (default: built-in defaults)
|
|
99
110
|
--stdin Read config JSON from stdin
|
|
@@ -108,6 +119,9 @@ VERIFY OPTIONS
|
|
|
108
119
|
--json Emit the drift report JSON
|
|
109
120
|
|
|
110
121
|
EXAMPLES
|
|
122
|
+
calllint inventory # List discovered agent configs
|
|
123
|
+
calllint scan --auto # Discover and scan all agents
|
|
124
|
+
calllint scan --agent cursor # Scan only Cursor configs
|
|
111
125
|
calllint check .cursor/mcp.json
|
|
112
126
|
calllint check npm:mcp-weather@1.0.0
|
|
113
127
|
echo "npx -y demo-mcp@1.2.3" | calllint check --stdin
|
|
@@ -115,7 +129,10 @@ EXAMPLES
|
|
|
115
129
|
calllint check ./mcp.json --json
|
|
116
130
|
calllint scan .cursor/mcp.json --markdown
|
|
117
131
|
calllint scan .cursor/mcp.json --badge > calllint-badge.json
|
|
118
|
-
calllint scan .cursor/mcp.json --receipt
|
|
132
|
+
calllint scan .cursor/mcp.json --receipt # Generate audit receipt
|
|
133
|
+
calllint receipt verify calllint-receipt.json # Verify receipt integrity
|
|
134
|
+
calllint action inspect payment.json
|
|
135
|
+
calllint inbox inspect gmail-reply.normalized.json
|
|
119
136
|
calllint verify ./mcp.json --ci
|
|
120
137
|
calllint approve && calllint verify --approved --ci
|
|
121
138
|
calllint explain filesystem
|
|
@@ -125,7 +142,7 @@ function helpCommand() {
|
|
|
125
142
|
}
|
|
126
143
|
|
|
127
144
|
// src/commands/scan.ts
|
|
128
|
-
import { join as
|
|
145
|
+
import { join as join11, resolve as resolve3 } from "node:path";
|
|
129
146
|
|
|
130
147
|
// ../../packages/policy/src/defaultPolicy.ts
|
|
131
148
|
function defaultPolicy() {
|
|
@@ -271,11 +288,11 @@ function applyPolicy(rawVerdict, serverName, blockingFindings, policy, now) {
|
|
|
271
288
|
if (rawVerdict !== "BLOCK") {
|
|
272
289
|
return { verdict: rawVerdict, changed: false };
|
|
273
290
|
}
|
|
274
|
-
const
|
|
291
|
+
const override2 = policy.overrides.find(
|
|
275
292
|
(o) => o.target === serverName && isOverrideActive(o, now)
|
|
276
293
|
);
|
|
277
|
-
if (!
|
|
278
|
-
const allowed = new Set(
|
|
294
|
+
if (!override2) return { verdict: rawVerdict, changed: false };
|
|
295
|
+
const allowed = new Set(override2.allow ?? []);
|
|
279
296
|
const blockingSymbols = new Set(
|
|
280
297
|
blockingFindings.filter((f) => f.blocker).map((f) => f.symbol)
|
|
281
298
|
);
|
|
@@ -284,7 +301,7 @@ function applyPolicy(rawVerdict, serverName, blockingFindings, policy, now) {
|
|
|
284
301
|
return {
|
|
285
302
|
verdict: "REVIEW",
|
|
286
303
|
changed: true,
|
|
287
|
-
note: `Policy decision: override for "${serverName}" (expires ${
|
|
304
|
+
note: `Policy decision: override for "${serverName}" (expires ${override2.expiresAt}${override2.owner ? `, owner: ${override2.owner}` : ""}) \u2014 ${override2.reason}`
|
|
288
305
|
};
|
|
289
306
|
}
|
|
290
307
|
function shouldFailCi(verdict, policy) {
|
|
@@ -292,17 +309,6 @@ function shouldFailCi(verdict, policy) {
|
|
|
292
309
|
return policy.ci.failOn.includes(verdict);
|
|
293
310
|
}
|
|
294
311
|
|
|
295
|
-
// ../../packages/core/src/options.ts
|
|
296
|
-
function resolveScanOptions(opts) {
|
|
297
|
-
return {
|
|
298
|
-
policy: opts?.policy ?? defaultPolicy(),
|
|
299
|
-
now: opts?.now ?? 0,
|
|
300
|
-
generatedAt: opts?.generatedAt ?? "1970-01-01T00:00:00.000Z",
|
|
301
|
-
extraFindings: opts?.extraFindings ?? {},
|
|
302
|
-
surfaces: opts?.surfaces ?? []
|
|
303
|
-
};
|
|
304
|
-
}
|
|
305
|
-
|
|
306
312
|
// ../../packages/types/src/verdict.ts
|
|
307
313
|
var VERDICT_SEVERITY = {
|
|
308
314
|
SAFE: 0,
|
|
@@ -477,6 +483,209 @@ var VERDICT_NEXT_ACTION = {
|
|
|
477
483
|
UNKNOWN: "gather_more_evidence"
|
|
478
484
|
};
|
|
479
485
|
|
|
486
|
+
// ../../packages/types/src/authority.ts
|
|
487
|
+
var AUTHORITY_SCHEMA_VERSION = "calllint.authority.v0";
|
|
488
|
+
|
|
489
|
+
// ../../packages/types/src/trustDecision.ts
|
|
490
|
+
var DECISION_SCHEMA_VERSION = "calllint.decision.v0";
|
|
491
|
+
|
|
492
|
+
// ../../packages/types/src/applyResult.ts
|
|
493
|
+
var APPLY_RESULT_SCHEMA = "calllint.apply-result.v1";
|
|
494
|
+
|
|
495
|
+
// ../../packages/types/src/trustGateway.ts
|
|
496
|
+
var TRUST_PREPARATION_SCHEMA = "calllint.trust-preparation.v0";
|
|
497
|
+
|
|
498
|
+
// ../../packages/fingerprint/src/hashJson.ts
|
|
499
|
+
import { createHash } from "node:crypto";
|
|
500
|
+
function sha256(input) {
|
|
501
|
+
return "sha256:" + createHash("sha256").update(input, "utf8").digest("hex");
|
|
502
|
+
}
|
|
503
|
+
function stableStringify(value) {
|
|
504
|
+
return JSON.stringify(sortValue(value));
|
|
505
|
+
}
|
|
506
|
+
function sortValue(value) {
|
|
507
|
+
if (Array.isArray(value)) return value.map(sortValue);
|
|
508
|
+
if (value && typeof value === "object") {
|
|
509
|
+
const out = {};
|
|
510
|
+
for (const key of Object.keys(value).sort()) {
|
|
511
|
+
out[key] = sortValue(value[key]);
|
|
512
|
+
}
|
|
513
|
+
return out;
|
|
514
|
+
}
|
|
515
|
+
return value;
|
|
516
|
+
}
|
|
517
|
+
function hashJson(value) {
|
|
518
|
+
return sha256(stableStringify(value));
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// ../../packages/fingerprint/src/computeFingerprints.ts
|
|
522
|
+
function computeFingerprints(input) {
|
|
523
|
+
const { server, binding, symbols, findingIds } = input;
|
|
524
|
+
const targetSpec = {
|
|
525
|
+
command: binding.declaredCommand,
|
|
526
|
+
args: binding.declaredArgs,
|
|
527
|
+
envKeys: [...server.envKeys].sort(),
|
|
528
|
+
remoteUrl: binding.remoteUrl
|
|
529
|
+
};
|
|
530
|
+
const packageSpec = binding.packageName !== void 0 ? `${binding.packageName}@${binding.packageVersionSpec ?? ""}` : void 0;
|
|
531
|
+
const riskSurface = {
|
|
532
|
+
symbols: [...symbols].sort(),
|
|
533
|
+
findingIds: [...findingIds].sort()
|
|
534
|
+
};
|
|
535
|
+
const sourceText = server.instructions ?? (server.providedTools.length > 0 ? JSON.stringify(server.providedTools) : void 0);
|
|
536
|
+
const toolMetadata = server.providedTools.length > 0 ? server.providedTools : void 0;
|
|
537
|
+
const fp = {
|
|
538
|
+
configHash: hashJson(server.raw),
|
|
539
|
+
targetSpecHash: hashJson(targetSpec),
|
|
540
|
+
riskSurfaceHash: hashJson(riskSurface)
|
|
541
|
+
};
|
|
542
|
+
if (packageSpec !== void 0) fp.packageSpecHash = sha256(packageSpec);
|
|
543
|
+
if (sourceText !== void 0) fp.sourceHash = sha256(sourceText);
|
|
544
|
+
if (toolMetadata !== void 0) fp.toolMetadataHash = hashJson(toolMetadata);
|
|
545
|
+
return fp;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// ../../packages/policy/src/decideOverAuthority.ts
|
|
549
|
+
function reasonCodeFor(c) {
|
|
550
|
+
switch (c.pattern) {
|
|
551
|
+
case "privilege-escalation":
|
|
552
|
+
case "auto-exec-bypass":
|
|
553
|
+
return "SHELL_OR_DOCKER_EXECUTION";
|
|
554
|
+
case "sensitive-file-read":
|
|
555
|
+
return "SECRET_IN_WORKSPACE_CONFIG";
|
|
556
|
+
case "data-exfil":
|
|
557
|
+
return "UNKNOWN_REMOTE";
|
|
558
|
+
case "hidden-override":
|
|
559
|
+
return "PROMPT_METADATA_INSTRUCTION";
|
|
560
|
+
case "messaging-financial":
|
|
561
|
+
return c.resource === "financial" ? "MONEY_OR_PAYMENT_CAPABILITY" : "MESSAGING_OR_EMAIL_SEND";
|
|
562
|
+
default:
|
|
563
|
+
break;
|
|
564
|
+
}
|
|
565
|
+
if (c.resource === "financial") return "MONEY_OR_PAYMENT_CAPABILITY";
|
|
566
|
+
if (c.resource === "message") return "MESSAGING_OR_EMAIL_SEND";
|
|
567
|
+
if (c.resource === "secret") return "SECRET_IN_WORKSPACE_CONFIG";
|
|
568
|
+
if (c.resource === "network") return "UNKNOWN_REMOTE";
|
|
569
|
+
if (c.resource === "process") return "SHELL_OR_DOCKER_EXECUTION";
|
|
570
|
+
if (c.resource === "filesystem") return "BROAD_FILESYSTEM_ACCESS";
|
|
571
|
+
return "EXTERNAL_MUTATION_UNKNOWN";
|
|
572
|
+
}
|
|
573
|
+
function baseVerdict(c) {
|
|
574
|
+
switch (c.approvalRequirement) {
|
|
575
|
+
case "block":
|
|
576
|
+
return "BLOCK";
|
|
577
|
+
case "review":
|
|
578
|
+
return "REVIEW";
|
|
579
|
+
default:
|
|
580
|
+
return "SAFE";
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
function policyFloor(code, policy) {
|
|
584
|
+
const d = policy.defaults;
|
|
585
|
+
const knob = {
|
|
586
|
+
UNKNOWN_REMOTE: d.unknownSource,
|
|
587
|
+
UNPINNED_PACKAGE: d.unpinnedPackage,
|
|
588
|
+
BROAD_FILESYSTEM_ACCESS: d.broadFilesystemAccess,
|
|
589
|
+
SHELL_OR_DOCKER_EXECUTION: d.arbitraryCommandExecution,
|
|
590
|
+
PROMPT_METADATA_INSTRUCTION: d.promptPoisoning,
|
|
591
|
+
EXTERNAL_MUTATION_UNKNOWN: d.externalMutation,
|
|
592
|
+
MONEY_OR_PAYMENT_CAPABILITY: d.financialAction
|
|
593
|
+
}[code];
|
|
594
|
+
if (knob === "deny") return "BLOCK";
|
|
595
|
+
if (knob === "warn") return "REVIEW";
|
|
596
|
+
return "SAFE";
|
|
597
|
+
}
|
|
598
|
+
function moreSevere(a, b) {
|
|
599
|
+
return VERDICT_SEVERITY[a] >= VERDICT_SEVERITY[b] ? a : b;
|
|
600
|
+
}
|
|
601
|
+
function evidenceFloor(evidence) {
|
|
602
|
+
let verdict = "SAFE";
|
|
603
|
+
let note = null;
|
|
604
|
+
for (const e of evidence) {
|
|
605
|
+
if (e.completeness === "degraded" || e.completeness === "failed") {
|
|
606
|
+
if (VERDICT_SEVERITY.UNKNOWN > VERDICT_SEVERITY[verdict]) {
|
|
607
|
+
verdict = "UNKNOWN";
|
|
608
|
+
note = `external evidence from ${e.provider} is ${e.completeness} \u2014 cannot yield SAFE`;
|
|
609
|
+
}
|
|
610
|
+
} else if (e.completeness === "partial") {
|
|
611
|
+
if (VERDICT_SEVERITY.REVIEW > VERDICT_SEVERITY[verdict]) {
|
|
612
|
+
verdict = "REVIEW";
|
|
613
|
+
note = `external evidence from ${e.provider} is partial`;
|
|
614
|
+
}
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
return { verdict, note };
|
|
618
|
+
}
|
|
619
|
+
function decideOverAuthority(input) {
|
|
620
|
+
const { authority, policy } = input;
|
|
621
|
+
const evidence = input.evidence ?? [];
|
|
622
|
+
const reasons = authority.capabilities.map((c) => {
|
|
623
|
+
const code = reasonCodeFor(c);
|
|
624
|
+
const contributes = moreSevere(baseVerdict(c), policyFloor(code, policy));
|
|
625
|
+
return { code, evidenceSource: c.evidenceSource, contributes };
|
|
626
|
+
});
|
|
627
|
+
const unknowns = [...authority.unknowns];
|
|
628
|
+
const contributions = reasons.map((r) => r.contributes);
|
|
629
|
+
if (authority.subject.artifactDigest === null) {
|
|
630
|
+
contributions.push("UNKNOWN");
|
|
631
|
+
unknowns.push("decision made over an unpinned artifact (no digest)");
|
|
632
|
+
}
|
|
633
|
+
if (authority.completeness === "partial") {
|
|
634
|
+
contributions.push("UNKNOWN");
|
|
635
|
+
unknowns.push("authority manifest is partial \u2014 capabilities may be under-counted");
|
|
636
|
+
}
|
|
637
|
+
const ev = evidenceFloor(evidence);
|
|
638
|
+
if (ev.verdict !== "SAFE") {
|
|
639
|
+
contributions.push(ev.verdict);
|
|
640
|
+
if (ev.note) unknowns.push(ev.note);
|
|
641
|
+
}
|
|
642
|
+
const verdict = mostSevereVerdict(contributions);
|
|
643
|
+
const order = (c) => REASON_CODES.indexOf(c);
|
|
644
|
+
const sortedReasons = dedupeReasons(reasons).sort(
|
|
645
|
+
(a, b) => order(a.code) - order(b.code) || cmp(a.evidenceSource, b.evidenceSource) || VERDICT_SEVERITY[b.contributes] - VERDICT_SEVERITY[a.contributes]
|
|
646
|
+
);
|
|
647
|
+
const completeness = authority.completeness === "partial" || unknowns.length > 0 || ev.verdict === "UNKNOWN" ? "partial" : "complete";
|
|
648
|
+
const evidenceDigests = [...new Set(evidence.map((e) => e.rawReportDigest))].sort();
|
|
649
|
+
const sealed = {
|
|
650
|
+
schema: DECISION_SCHEMA_VERSION,
|
|
651
|
+
artifactDigest: authority.subject.artifactDigest,
|
|
652
|
+
authorityDigest: authority.digest,
|
|
653
|
+
policyDigest: hashJson(policy),
|
|
654
|
+
evidenceDigests,
|
|
655
|
+
verdict,
|
|
656
|
+
reasons: sortedReasons,
|
|
657
|
+
requiredApprovals: [...authority.approval.required].sort(),
|
|
658
|
+
unknowns: [...new Set(unknowns)].sort(),
|
|
659
|
+
completeness
|
|
660
|
+
};
|
|
661
|
+
return { ...sealed, digest: hashJson(sealed) };
|
|
662
|
+
}
|
|
663
|
+
function dedupeReasons(reasons) {
|
|
664
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
665
|
+
for (const r of reasons) {
|
|
666
|
+
const k = `${r.code}|${r.evidenceSource}`;
|
|
667
|
+
const prev = byKey.get(k);
|
|
668
|
+
if (!prev || VERDICT_SEVERITY[r.contributes] > VERDICT_SEVERITY[prev.contributes]) {
|
|
669
|
+
byKey.set(k, r);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
return [...byKey.values()];
|
|
673
|
+
}
|
|
674
|
+
function cmp(a, b) {
|
|
675
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// ../../packages/core/src/options.ts
|
|
679
|
+
function resolveScanOptions(opts) {
|
|
680
|
+
return {
|
|
681
|
+
policy: opts?.policy ?? defaultPolicy(),
|
|
682
|
+
now: opts?.now ?? 0,
|
|
683
|
+
generatedAt: opts?.generatedAt ?? "1970-01-01T00:00:00.000Z",
|
|
684
|
+
extraFindings: opts?.extraFindings ?? {},
|
|
685
|
+
surfaces: opts?.surfaces ?? []
|
|
686
|
+
};
|
|
687
|
+
}
|
|
688
|
+
|
|
480
689
|
// ../../packages/resolver/src/npmSpec.ts
|
|
481
690
|
var PACKAGE_RUNNERS = /* @__PURE__ */ new Set(["npx", "pnpm", "yarn", "bunx", "uvx", "pipx"]);
|
|
482
691
|
var SHELL_COMMANDS = /* @__PURE__ */ new Set([
|
|
@@ -597,6 +806,58 @@ function resolveRuntimeBinding(server) {
|
|
|
597
806
|
};
|
|
598
807
|
}
|
|
599
808
|
|
|
809
|
+
// ../../packages/resolver/src/resolveArtifactIdentity.ts
|
|
810
|
+
function digestTree(entries) {
|
|
811
|
+
const sorted = [...entries].sort((a, b) => a.path < b.path ? -1 : a.path > b.path ? 1 : 0);
|
|
812
|
+
const canonical = sorted.map((e) => `${e.path}\0${e.content.length}\0${e.content}`).join("\n");
|
|
813
|
+
return sha256(canonical);
|
|
814
|
+
}
|
|
815
|
+
var LOCAL_TYPES = /* @__PURE__ */ new Set(["dir", "file", "mcp-config"]);
|
|
816
|
+
function resolveArtifactIdentity(input) {
|
|
817
|
+
const reasons = [...input.resolutionReasons ?? []];
|
|
818
|
+
let digest = null;
|
|
819
|
+
if (input.entries && input.entries.length > 0) {
|
|
820
|
+
digest = digestTree(input.entries);
|
|
821
|
+
} else if (typeof input.content === "string") {
|
|
822
|
+
digest = sha256(input.content);
|
|
823
|
+
} else {
|
|
824
|
+
reasons.push("no fetched bytes available to digest");
|
|
825
|
+
}
|
|
826
|
+
let resolvedRef = input.resolvedRef ?? null;
|
|
827
|
+
if (!resolvedRef) {
|
|
828
|
+
if (LOCAL_TYPES.has(input.sourceType) && digest) {
|
|
829
|
+
resolvedRef = `content:${digest}`;
|
|
830
|
+
} else {
|
|
831
|
+
resolvedRef = null;
|
|
832
|
+
if (input.sourceType === "git" || input.sourceType === "npm") {
|
|
833
|
+
reasons.push(
|
|
834
|
+
`remote ${input.sourceType} target could not be pinned to an immutable ref (offline or unresolved)`
|
|
835
|
+
);
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
let resolution;
|
|
840
|
+
if (resolvedRef && digest) {
|
|
841
|
+
resolution = "resolved";
|
|
842
|
+
} else if (digest || resolvedRef) {
|
|
843
|
+
resolution = "partial";
|
|
844
|
+
} else {
|
|
845
|
+
resolution = "unresolved";
|
|
846
|
+
}
|
|
847
|
+
const identity = {
|
|
848
|
+
schema: "calllint.artifact.v1",
|
|
849
|
+
sourceType: input.sourceType,
|
|
850
|
+
source: input.source,
|
|
851
|
+
requestedRef: input.requestedRef ?? null,
|
|
852
|
+
resolvedRef,
|
|
853
|
+
digest,
|
|
854
|
+
resolvedAt: input.resolvedAt,
|
|
855
|
+
resolution
|
|
856
|
+
};
|
|
857
|
+
if (reasons.length > 0) identity.resolutionReasons = reasons;
|
|
858
|
+
return identity;
|
|
859
|
+
}
|
|
860
|
+
|
|
600
861
|
// ../../packages/static-analyzer/src/detectors/unpinnedPackage.ts
|
|
601
862
|
function detectUnpinnedPackage(ctx) {
|
|
602
863
|
const { binding } = ctx;
|
|
@@ -1596,6 +1857,301 @@ function analyzeDocumentSurfaces(surfaces) {
|
|
|
1596
1857
|
];
|
|
1597
1858
|
}
|
|
1598
1859
|
|
|
1860
|
+
// ../../packages/static-analyzer/src/instructionAuthority.ts
|
|
1861
|
+
var RULES = [
|
|
1862
|
+
// 1. privilege-escalation → execute × process (irreversible, must be approved)
|
|
1863
|
+
esc(/\bsudo\s+\S/),
|
|
1864
|
+
esc(/\brun(?:ning)?\s+as\s+(?:root|administrator|admin|superuser|super\s?user)\b/),
|
|
1865
|
+
esc(/\bas\s+root\b/),
|
|
1866
|
+
esc(/\b(?:disable|bypass|turn\s+off)\s+(?:the\s+)?sandbox\b/),
|
|
1867
|
+
esc(/\bescalate\s+privileg/),
|
|
1868
|
+
esc(/\bwith\s+root\s+privileg/),
|
|
1869
|
+
// 2. auto-exec-bypass → execute × process (removing the approval gate is the grant)
|
|
1870
|
+
bypass(/\bwithout\s+(?:asking|confirmation|prompting|permission|approval)\b/),
|
|
1871
|
+
bypass(/\bauto[-\s]?(?:approve|approving|run|execute|confirm)\b/),
|
|
1872
|
+
bypass(/\b(?:run|execute)\s+automatically\b/),
|
|
1873
|
+
bypass(/\bautomatically\s+(?:run|execute|approve)\b/),
|
|
1874
|
+
bypass(/\bskip\s+(?:the\s+)?(?:confirmation|approval|prompt)\b/),
|
|
1875
|
+
bypass(/\bdon['’]?t\s+ask\b/),
|
|
1876
|
+
bypass(/\bdo\s+not\s+ask\b/),
|
|
1877
|
+
bypass(/\bno\s+confirmation\s+(?:needed|required)\b/),
|
|
1878
|
+
// 3. sensitive-file-read → read × secret (concrete paths are near-zero FP)
|
|
1879
|
+
secretRead(/(?:^|[\s"'`(/~])\.ssh\b/, "high"),
|
|
1880
|
+
secretRead(/\bid_(?:rsa|ed25519|ecdsa|dsa)\b/, "high"),
|
|
1881
|
+
secretRead(/(?:^|[\s"'`(/])\.env\b/, "high"),
|
|
1882
|
+
secretRead(/\.aws[/\\]credentials\b/, "high"),
|
|
1883
|
+
secretRead(/\baws\s+credentials\b/, "high"),
|
|
1884
|
+
secretRead(/\.git-credentials\b/, "high"),
|
|
1885
|
+
secretRead(/\/etc\/(?:passwd|shadow)\b/, "high"),
|
|
1886
|
+
secretRead(/\bprivate\s+key\b/, "medium"),
|
|
1887
|
+
secretRead(/\bread\b[^.\n]{0,30}\b(?:secret|credential|api[-\s]?key|password)s?\b/, "medium"),
|
|
1888
|
+
// 4. data-exfil → send × network (destination pulled off the line)
|
|
1889
|
+
exfil(/\bexfiltrat/, "high"),
|
|
1890
|
+
exfil(/\b(?:send|upload|post|transmit)\b[^.\n]{0,50}\bto\s+https?:\/\/\S+/, "high"),
|
|
1891
|
+
exfil(/\b(?:send|upload|post)\b[^.\n]{0,30}\b(?:the\s+)?(?:contents?|file|data|output|workspace)\b[^.\n]{0,30}\bto\b/, "medium"),
|
|
1892
|
+
// 5a. messaging-financial (messaging) → send × message
|
|
1893
|
+
messaging(/\bsend\b[^.\n]{0,20}\b(?:an?\s+)?(?:e[-\s]?mail|message|sms|text)\b/),
|
|
1894
|
+
messaging(/\bpost\b[^.\n]{0,20}\bto\s+(?:slack|discord|teams|telegram)\b/),
|
|
1895
|
+
// 5b. messaging-financial (financial) → spend × financial (irreversible, must be approved)
|
|
1896
|
+
financial(/\bmake\b[^.\n]{0,20}\ba?\s*payment\b/),
|
|
1897
|
+
financial(/\btransfer\b[^.\n]{0,20}\bfunds?\b/),
|
|
1898
|
+
financial(/\b(?:charge|bill)\b[^.\n]{0,20}\b(?:the\s+)?(?:card|account|customer)\b/),
|
|
1899
|
+
financial(/\bwire\b[^.\n]{0,20}\b(?:money|funds|payment)\b/),
|
|
1900
|
+
financial(/\bsend\b[^.\n]{0,20}\b(?:money|funds|payment)\b/),
|
|
1901
|
+
// 6. hidden-override (phrase half) → mutate × agent. The obfuscated-character
|
|
1902
|
+
// half is handled separately via findHiddenContent (see extractLine).
|
|
1903
|
+
override(/\bignore\s+(?:all\s+)?(?:previous|prior|the\s+above)\s+instructions\b/),
|
|
1904
|
+
override(/\bdisregard\s+(?:all\s+)?(?:previous|prior)\s+instructions\b/),
|
|
1905
|
+
override(/\boverride\s+(?:the\s+)?system\s+prompt\b/),
|
|
1906
|
+
override(/\bdo\s+not\s+(?:tell|inform|notify)\s+the\s+user\b/),
|
|
1907
|
+
override(/\bwithout\s+(?:telling|informing)\s+the\s+user\b/)
|
|
1908
|
+
];
|
|
1909
|
+
function ci(re) {
|
|
1910
|
+
return re.flags.includes("i") ? re : new RegExp(re.source, re.flags + "i");
|
|
1911
|
+
}
|
|
1912
|
+
function esc(rawTest) {
|
|
1913
|
+
const test = ci(rawTest);
|
|
1914
|
+
return {
|
|
1915
|
+
test,
|
|
1916
|
+
pattern: "privilege-escalation",
|
|
1917
|
+
action: "execute",
|
|
1918
|
+
resource: "process",
|
|
1919
|
+
mutability: "mutating",
|
|
1920
|
+
reversibility: "irreversible",
|
|
1921
|
+
approvalRequirement: "block",
|
|
1922
|
+
confidence: "high"
|
|
1923
|
+
};
|
|
1924
|
+
}
|
|
1925
|
+
function bypass(rawTest) {
|
|
1926
|
+
const test = ci(rawTest);
|
|
1927
|
+
return {
|
|
1928
|
+
test,
|
|
1929
|
+
pattern: "auto-exec-bypass",
|
|
1930
|
+
action: "execute",
|
|
1931
|
+
resource: "process",
|
|
1932
|
+
mutability: "mutating",
|
|
1933
|
+
reversibility: "irreversible",
|
|
1934
|
+
approvalRequirement: "block",
|
|
1935
|
+
confidence: "high"
|
|
1936
|
+
};
|
|
1937
|
+
}
|
|
1938
|
+
function secretRead(rawTest, confidence) {
|
|
1939
|
+
const test = ci(rawTest);
|
|
1940
|
+
return {
|
|
1941
|
+
test,
|
|
1942
|
+
pattern: "sensitive-file-read",
|
|
1943
|
+
action: "read",
|
|
1944
|
+
resource: "secret",
|
|
1945
|
+
mutability: "read-only",
|
|
1946
|
+
reversibility: "n/a",
|
|
1947
|
+
approvalRequirement: "review",
|
|
1948
|
+
confidence
|
|
1949
|
+
};
|
|
1950
|
+
}
|
|
1951
|
+
function exfil(rawTest, confidence) {
|
|
1952
|
+
const test = ci(rawTest);
|
|
1953
|
+
return {
|
|
1954
|
+
test,
|
|
1955
|
+
pattern: "data-exfil",
|
|
1956
|
+
action: "send",
|
|
1957
|
+
resource: "network",
|
|
1958
|
+
mutability: "mutating",
|
|
1959
|
+
reversibility: "irreversible",
|
|
1960
|
+
approvalRequirement: "block",
|
|
1961
|
+
confidence,
|
|
1962
|
+
extractDestination: true
|
|
1963
|
+
};
|
|
1964
|
+
}
|
|
1965
|
+
function messaging(rawTest) {
|
|
1966
|
+
const test = ci(rawTest);
|
|
1967
|
+
return {
|
|
1968
|
+
test,
|
|
1969
|
+
pattern: "messaging-financial",
|
|
1970
|
+
action: "send",
|
|
1971
|
+
resource: "message",
|
|
1972
|
+
mutability: "mutating",
|
|
1973
|
+
reversibility: "irreversible",
|
|
1974
|
+
approvalRequirement: "review",
|
|
1975
|
+
confidence: "high"
|
|
1976
|
+
};
|
|
1977
|
+
}
|
|
1978
|
+
function financial(rawTest) {
|
|
1979
|
+
const test = ci(rawTest);
|
|
1980
|
+
return {
|
|
1981
|
+
test,
|
|
1982
|
+
pattern: "messaging-financial",
|
|
1983
|
+
action: "spend",
|
|
1984
|
+
resource: "financial",
|
|
1985
|
+
mutability: "mutating",
|
|
1986
|
+
reversibility: "irreversible",
|
|
1987
|
+
approvalRequirement: "block",
|
|
1988
|
+
confidence: "high"
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
function override(rawTest) {
|
|
1992
|
+
const test = ci(rawTest);
|
|
1993
|
+
return {
|
|
1994
|
+
test,
|
|
1995
|
+
pattern: "hidden-override",
|
|
1996
|
+
action: "mutate",
|
|
1997
|
+
resource: "agent",
|
|
1998
|
+
mutability: "mutating",
|
|
1999
|
+
reversibility: "irreversible",
|
|
2000
|
+
approvalRequirement: "block",
|
|
2001
|
+
confidence: "medium"
|
|
2002
|
+
};
|
|
2003
|
+
}
|
|
2004
|
+
function extractHost(line) {
|
|
2005
|
+
const m = line.match(/https?:\/\/([^\s/"')]+)/i);
|
|
2006
|
+
return m ? m[1] : null;
|
|
2007
|
+
}
|
|
2008
|
+
function capabilityFrom(t, evidenceSource, line) {
|
|
2009
|
+
return {
|
|
2010
|
+
action: t.action,
|
|
2011
|
+
resource: t.resource,
|
|
2012
|
+
scope: null,
|
|
2013
|
+
destination: t.extractDestination ? extractHost(line) : null,
|
|
2014
|
+
mutability: t.mutability,
|
|
2015
|
+
reversibility: t.reversibility,
|
|
2016
|
+
monetaryLimit: null,
|
|
2017
|
+
approvalRequirement: t.approvalRequirement,
|
|
2018
|
+
evidenceSource,
|
|
2019
|
+
confidence: t.confidence,
|
|
2020
|
+
completeness: "complete",
|
|
2021
|
+
pattern: t.pattern
|
|
2022
|
+
};
|
|
2023
|
+
}
|
|
2024
|
+
function keyOf(c) {
|
|
2025
|
+
return [c.pattern, c.action, c.resource, c.destination ?? "", c.evidenceSource].join("|");
|
|
2026
|
+
}
|
|
2027
|
+
function extractInstructionAuthority(surfaces) {
|
|
2028
|
+
const seen = /* @__PURE__ */ new Map();
|
|
2029
|
+
for (const surface of surfaces) {
|
|
2030
|
+
const lines = surface.text.split(/\r?\n/);
|
|
2031
|
+
for (let i = 0; i < lines.length; i++) {
|
|
2032
|
+
const line = lines[i];
|
|
2033
|
+
const evidenceSource = `${surface.path}:${i + 1}`;
|
|
2034
|
+
for (const rule2 of RULES) {
|
|
2035
|
+
if (rule2.test.test(line)) {
|
|
2036
|
+
const cap = capabilityFrom(rule2, evidenceSource, line);
|
|
2037
|
+
const k = keyOf(cap);
|
|
2038
|
+
if (!seen.has(k)) seen.set(k, cap);
|
|
2039
|
+
}
|
|
2040
|
+
}
|
|
2041
|
+
const smuggling = findHiddenContent(line).filter(
|
|
2042
|
+
(c) => c !== "embedded HTML/XML comment"
|
|
2043
|
+
);
|
|
2044
|
+
if (smuggling.length > 0) {
|
|
2045
|
+
const cap = {
|
|
2046
|
+
action: "mutate",
|
|
2047
|
+
resource: "agent",
|
|
2048
|
+
scope: null,
|
|
2049
|
+
destination: null,
|
|
2050
|
+
mutability: "mutating",
|
|
2051
|
+
reversibility: "irreversible",
|
|
2052
|
+
monetaryLimit: null,
|
|
2053
|
+
approvalRequirement: "block",
|
|
2054
|
+
evidenceSource,
|
|
2055
|
+
confidence: "high",
|
|
2056
|
+
// structural, near-zero FP
|
|
2057
|
+
completeness: "complete",
|
|
2058
|
+
pattern: "hidden-override"
|
|
2059
|
+
};
|
|
2060
|
+
const k = keyOf(cap);
|
|
2061
|
+
if (!seen.has(k)) seen.set(k, cap);
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
}
|
|
2065
|
+
return sortCapabilities([...seen.values()]);
|
|
2066
|
+
}
|
|
2067
|
+
function sortCapabilities(caps) {
|
|
2068
|
+
return caps.sort((a, b) => {
|
|
2069
|
+
return cmp2(a.pattern ?? "", b.pattern ?? "") || cmp2(a.evidenceSource, b.evidenceSource) || cmp2(a.action, b.action) || cmp2(a.resource, b.resource) || cmp2(a.destination ?? "", b.destination ?? "");
|
|
2070
|
+
});
|
|
2071
|
+
}
|
|
2072
|
+
function cmp2(a, b) {
|
|
2073
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
// ../../packages/static-analyzer/src/configAuthority.ts
|
|
2077
|
+
var SECRET_HINTS2 = [
|
|
2078
|
+
"TOKEN",
|
|
2079
|
+
"SECRET",
|
|
2080
|
+
"PASSWORD",
|
|
2081
|
+
"PASSWD",
|
|
2082
|
+
"API_KEY",
|
|
2083
|
+
"APIKEY",
|
|
2084
|
+
"ACCESS_KEY",
|
|
2085
|
+
"PRIVATE_KEY",
|
|
2086
|
+
"CREDENTIAL",
|
|
2087
|
+
"AUTH",
|
|
2088
|
+
"SESSION"
|
|
2089
|
+
];
|
|
2090
|
+
function looksSecret2(key) {
|
|
2091
|
+
const upper = key.toUpperCase();
|
|
2092
|
+
return SECRET_HINTS2.some((h2) => upper.includes(h2));
|
|
2093
|
+
}
|
|
2094
|
+
function hostOf2(url) {
|
|
2095
|
+
try {
|
|
2096
|
+
return new URL(url).host || url;
|
|
2097
|
+
} catch {
|
|
2098
|
+
return url;
|
|
2099
|
+
}
|
|
2100
|
+
}
|
|
2101
|
+
function deriveConfigCapabilities(server) {
|
|
2102
|
+
const caps = [];
|
|
2103
|
+
if (server.command) {
|
|
2104
|
+
caps.push({
|
|
2105
|
+
action: "execute",
|
|
2106
|
+
resource: "process",
|
|
2107
|
+
scope: server.command,
|
|
2108
|
+
destination: null,
|
|
2109
|
+
mutability: "mutating",
|
|
2110
|
+
reversibility: "irreversible",
|
|
2111
|
+
monetaryLimit: null,
|
|
2112
|
+
// Running a local process is expected for a stdio server; the policy, not
|
|
2113
|
+
// the manifest, decides whether that warrants review. Mark it review-worthy
|
|
2114
|
+
// only where the command itself is unknown — here just record it as routine.
|
|
2115
|
+
approvalRequirement: "none",
|
|
2116
|
+
evidenceSource: "server.command",
|
|
2117
|
+
confidence: "high",
|
|
2118
|
+
completeness: "complete"
|
|
2119
|
+
});
|
|
2120
|
+
}
|
|
2121
|
+
if (server.url) {
|
|
2122
|
+
caps.push({
|
|
2123
|
+
action: "connect",
|
|
2124
|
+
resource: "network",
|
|
2125
|
+
scope: hostOf2(server.url),
|
|
2126
|
+
destination: hostOf2(server.url),
|
|
2127
|
+
mutability: "mutating",
|
|
2128
|
+
reversibility: "irreversible",
|
|
2129
|
+
monetaryLimit: null,
|
|
2130
|
+
approvalRequirement: "review",
|
|
2131
|
+
evidenceSource: "server.url",
|
|
2132
|
+
confidence: "high",
|
|
2133
|
+
completeness: "complete"
|
|
2134
|
+
});
|
|
2135
|
+
}
|
|
2136
|
+
for (const key of server.envKeys) {
|
|
2137
|
+
if (!looksSecret2(key)) continue;
|
|
2138
|
+
caps.push({
|
|
2139
|
+
action: "read",
|
|
2140
|
+
resource: "secret",
|
|
2141
|
+
scope: key,
|
|
2142
|
+
destination: null,
|
|
2143
|
+
mutability: "read-only",
|
|
2144
|
+
reversibility: "n/a",
|
|
2145
|
+
monetaryLimit: null,
|
|
2146
|
+
approvalRequirement: "review",
|
|
2147
|
+
evidenceSource: `server.env.${key}`,
|
|
2148
|
+
confidence: "medium",
|
|
2149
|
+
completeness: "complete"
|
|
2150
|
+
});
|
|
2151
|
+
}
|
|
2152
|
+
return sortCapabilities(caps);
|
|
2153
|
+
}
|
|
2154
|
+
|
|
1599
2155
|
// ../../packages/risk-engine/src/computeRiskClass.ts
|
|
1600
2156
|
function computeRiskClass(findings, binding) {
|
|
1601
2157
|
const classes = findings.map((f) => f.riskClass);
|
|
@@ -1719,56 +2275,6 @@ function assessServer(findings, binding) {
|
|
|
1719
2275
|
};
|
|
1720
2276
|
}
|
|
1721
2277
|
|
|
1722
|
-
// ../../packages/fingerprint/src/hashJson.ts
|
|
1723
|
-
import { createHash } from "node:crypto";
|
|
1724
|
-
function sha256(input) {
|
|
1725
|
-
return "sha256:" + createHash("sha256").update(input, "utf8").digest("hex");
|
|
1726
|
-
}
|
|
1727
|
-
function stableStringify(value) {
|
|
1728
|
-
return JSON.stringify(sortValue(value));
|
|
1729
|
-
}
|
|
1730
|
-
function sortValue(value) {
|
|
1731
|
-
if (Array.isArray(value)) return value.map(sortValue);
|
|
1732
|
-
if (value && typeof value === "object") {
|
|
1733
|
-
const out = {};
|
|
1734
|
-
for (const key of Object.keys(value).sort()) {
|
|
1735
|
-
out[key] = sortValue(value[key]);
|
|
1736
|
-
}
|
|
1737
|
-
return out;
|
|
1738
|
-
}
|
|
1739
|
-
return value;
|
|
1740
|
-
}
|
|
1741
|
-
function hashJson(value) {
|
|
1742
|
-
return sha256(stableStringify(value));
|
|
1743
|
-
}
|
|
1744
|
-
|
|
1745
|
-
// ../../packages/fingerprint/src/computeFingerprints.ts
|
|
1746
|
-
function computeFingerprints(input) {
|
|
1747
|
-
const { server, binding, symbols, findingIds } = input;
|
|
1748
|
-
const targetSpec = {
|
|
1749
|
-
command: binding.declaredCommand,
|
|
1750
|
-
args: binding.declaredArgs,
|
|
1751
|
-
envKeys: [...server.envKeys].sort(),
|
|
1752
|
-
remoteUrl: binding.remoteUrl
|
|
1753
|
-
};
|
|
1754
|
-
const packageSpec = binding.packageName !== void 0 ? `${binding.packageName}@${binding.packageVersionSpec ?? ""}` : void 0;
|
|
1755
|
-
const riskSurface = {
|
|
1756
|
-
symbols: [...symbols].sort(),
|
|
1757
|
-
findingIds: [...findingIds].sort()
|
|
1758
|
-
};
|
|
1759
|
-
const sourceText = server.instructions ?? (server.providedTools.length > 0 ? JSON.stringify(server.providedTools) : void 0);
|
|
1760
|
-
const toolMetadata = server.providedTools.length > 0 ? server.providedTools : void 0;
|
|
1761
|
-
const fp = {
|
|
1762
|
-
configHash: hashJson(server.raw),
|
|
1763
|
-
targetSpecHash: hashJson(targetSpec),
|
|
1764
|
-
riskSurfaceHash: hashJson(riskSurface)
|
|
1765
|
-
};
|
|
1766
|
-
if (packageSpec !== void 0) fp.packageSpecHash = sha256(packageSpec);
|
|
1767
|
-
if (sourceText !== void 0) fp.sourceHash = sha256(sourceText);
|
|
1768
|
-
if (toolMetadata !== void 0) fp.toolMetadataHash = hashJson(toolMetadata);
|
|
1769
|
-
return fp;
|
|
1770
|
-
}
|
|
1771
|
-
|
|
1772
2278
|
// ../../packages/core/src/summarize.ts
|
|
1773
2279
|
function summarize(name, verdict, a, policyApplied) {
|
|
1774
2280
|
const symbolText = a.symbols.length > 0 ? a.symbols.map((s) => RISK_SYMBOL_LABEL[s]).join(", ") : "no risk surface observed";
|
|
@@ -1982,16 +2488,16 @@ function readString(c) {
|
|
|
1982
2488
|
while (c.i < c.text.length) {
|
|
1983
2489
|
const ch = advance(c);
|
|
1984
2490
|
if (ch === "\\") {
|
|
1985
|
-
const
|
|
1986
|
-
if (
|
|
1987
|
-
else if (
|
|
1988
|
-
else if (
|
|
1989
|
-
else if (
|
|
2491
|
+
const esc3 = advance(c);
|
|
2492
|
+
if (esc3 === "n") out += "\n";
|
|
2493
|
+
else if (esc3 === "t") out += " ";
|
|
2494
|
+
else if (esc3 === "r") out += "\r";
|
|
2495
|
+
else if (esc3 === "u") {
|
|
1990
2496
|
let hex = "";
|
|
1991
2497
|
for (let k = 0; k < 4; k++) hex += advance(c);
|
|
1992
2498
|
const code = Number.parseInt(hex, 16);
|
|
1993
2499
|
out += Number.isNaN(code) ? "" : String.fromCharCode(code);
|
|
1994
|
-
} else out +=
|
|
2500
|
+
} else out += esc3;
|
|
1995
2501
|
} else if (ch === '"') {
|
|
1996
2502
|
break;
|
|
1997
2503
|
} else {
|
|
@@ -3123,31 +3629,220 @@ function verifyReceipt(input) {
|
|
|
3123
3629
|
return { valid: errors.length === 0, errors, signed };
|
|
3124
3630
|
}
|
|
3125
3631
|
|
|
3126
|
-
// ../../packages/
|
|
3127
|
-
var
|
|
3128
|
-
|
|
3129
|
-
|
|
3130
|
-
|
|
3131
|
-
|
|
3132
|
-
|
|
3133
|
-
|
|
3134
|
-
|
|
3135
|
-
|
|
3136
|
-
|
|
3137
|
-
|
|
3138
|
-
|
|
3139
|
-
|
|
3140
|
-
// ../../packages/report-renderer/src/renderJson.ts
|
|
3141
|
-
function renderJson(summary) {
|
|
3142
|
-
return JSON.stringify(summary, null, 2);
|
|
3632
|
+
// ../../packages/core/src/gateway/prepare.ts
|
|
3633
|
+
var COMPLETENESS_RANK = {
|
|
3634
|
+
complete: 0,
|
|
3635
|
+
partial: 1,
|
|
3636
|
+
degraded: 2,
|
|
3637
|
+
failed: 3
|
|
3638
|
+
};
|
|
3639
|
+
function worstCompleteness(evidence) {
|
|
3640
|
+
let worst = "complete";
|
|
3641
|
+
for (const e of evidence) {
|
|
3642
|
+
if (COMPLETENESS_RANK[e.completeness] > COMPLETENESS_RANK[worst]) worst = e.completeness;
|
|
3643
|
+
}
|
|
3644
|
+
return worst;
|
|
3143
3645
|
}
|
|
3144
|
-
|
|
3145
|
-
|
|
3146
|
-
|
|
3147
|
-
const
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
|
|
3646
|
+
function prepare(input) {
|
|
3647
|
+
const { artifact, preparedAt } = input;
|
|
3648
|
+
const evidence = input.evidence ?? [];
|
|
3649
|
+
const notes = [];
|
|
3650
|
+
let state;
|
|
3651
|
+
if (artifact.resolution === "resolved") {
|
|
3652
|
+
if (evidence.length === 0) {
|
|
3653
|
+
state = "PLAN_READY";
|
|
3654
|
+
notes.push("no external evidence attached (optional); decision will rely on CallLint's own analysis");
|
|
3655
|
+
} else {
|
|
3656
|
+
const worst = worstCompleteness(evidence);
|
|
3657
|
+
switch (worst) {
|
|
3658
|
+
case "complete":
|
|
3659
|
+
state = "PLAN_READY";
|
|
3660
|
+
break;
|
|
3661
|
+
case "partial":
|
|
3662
|
+
state = "EVIDENCE_PARTIAL";
|
|
3663
|
+
notes.push("attached evidence is partial \u2014 gaps remain; not a clean pass");
|
|
3664
|
+
break;
|
|
3665
|
+
default:
|
|
3666
|
+
state = "EVIDENCE_FAILED";
|
|
3667
|
+
notes.push(
|
|
3668
|
+
"attached evidence is degraded or failed \u2014 fail-closed; a degraded external scan never reads as a pass"
|
|
3669
|
+
);
|
|
3670
|
+
break;
|
|
3671
|
+
}
|
|
3672
|
+
}
|
|
3673
|
+
} else if (artifact.resolution === "partial") {
|
|
3674
|
+
state = "FETCH_REJECTED";
|
|
3675
|
+
notes.push(
|
|
3676
|
+
"artifact could not be fully pinned (missing immutable ref or bytes); not a verified target"
|
|
3677
|
+
);
|
|
3678
|
+
} else {
|
|
3679
|
+
state = "RESOLUTION_FAILED";
|
|
3680
|
+
notes.push("artifact could not be resolved to an immutable, digested identity");
|
|
3681
|
+
}
|
|
3682
|
+
for (const r of artifact.resolutionReasons ?? []) notes.push(r);
|
|
3683
|
+
for (const e of evidence) {
|
|
3684
|
+
for (const reason of e.degradedReasons) {
|
|
3685
|
+
notes.push(`evidence[${e.provider}]: ${reason}`);
|
|
3686
|
+
}
|
|
3687
|
+
}
|
|
3688
|
+
const authority = input.authority ?? null;
|
|
3689
|
+
if (authority) {
|
|
3690
|
+
if (state === "PLAN_READY") state = "AUTHORITY_NORMALIZED";
|
|
3691
|
+
const caps = authority.capabilities.length;
|
|
3692
|
+
notes.push(
|
|
3693
|
+
caps === 0 ? "authority normalized: no elevated capabilities detected" : `authority normalized: ${caps} capabilit${caps === 1 ? "y" : "ies"}${authority.approval.required.length > 0 ? `, approvals required: ${authority.approval.required.join(", ")}` : ""}`
|
|
3694
|
+
);
|
|
3695
|
+
if (authority.completeness === "partial") {
|
|
3696
|
+
notes.push("authority manifest is partial \u2014 some sources were not fully normalized");
|
|
3697
|
+
}
|
|
3698
|
+
}
|
|
3699
|
+
const decision = input.decision ?? null;
|
|
3700
|
+
if (decision) {
|
|
3701
|
+
if (state === "AUTHORITY_NORMALIZED") {
|
|
3702
|
+
state = decision.verdict === "UNKNOWN" ? "POLICY_UNKNOWN" : "DECIDED";
|
|
3703
|
+
}
|
|
3704
|
+
notes.push(
|
|
3705
|
+
`policy decision: ${decision.verdict}` + (decision.reasons.length > 0 ? ` (${[...new Set(decision.reasons.map((r) => r.code))].join(", ")})` : "")
|
|
3706
|
+
);
|
|
3707
|
+
if (decision.verdict === "UNKNOWN") {
|
|
3708
|
+
notes.push("verdict UNKNOWN \u2014 insufficient evidence; fail-closed, never a pass");
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
const plan = input.plan ?? null;
|
|
3712
|
+
if (plan) {
|
|
3713
|
+
if (state === "DECIDED") {
|
|
3714
|
+
state = "PLAN_READY";
|
|
3715
|
+
notes.push(
|
|
3716
|
+
`install plan computed for host "${plan.host}" (tier ${plan.tier}): ${plan.operations.length} operation(s), ${plan.rollback.length} rollback op(s); verdict ${decision?.verdict ?? "?"} \u2014 NOT applied (apply is a separate approved step)`
|
|
3717
|
+
);
|
|
3718
|
+
if (decision && decision.verdict !== "SAFE") {
|
|
3719
|
+
notes.push(
|
|
3720
|
+
`verdict ${decision.verdict} \u2014 applying this plan would require an explicit, digest-bound approval`
|
|
3721
|
+
);
|
|
3722
|
+
}
|
|
3723
|
+
} else {
|
|
3724
|
+
notes.push("install plan present but the gateway did not reach a confident decision \u2014 plan not activated");
|
|
3725
|
+
}
|
|
3726
|
+
}
|
|
3727
|
+
return {
|
|
3728
|
+
schema: TRUST_PREPARATION_SCHEMA,
|
|
3729
|
+
artifact,
|
|
3730
|
+
evidence: input.evidence ? [...evidence] : null,
|
|
3731
|
+
authority,
|
|
3732
|
+
decision,
|
|
3733
|
+
plan,
|
|
3734
|
+
state,
|
|
3735
|
+
notes,
|
|
3736
|
+
preparedAt
|
|
3737
|
+
};
|
|
3738
|
+
}
|
|
3739
|
+
function prepareExitCode(prep) {
|
|
3740
|
+
switch (prep.state) {
|
|
3741
|
+
case "PLAN_READY":
|
|
3742
|
+
return prep.decision?.verdict === "REVIEW" ? 10 : prep.decision?.verdict === "BLOCK" || prep.decision?.verdict === "UNKNOWN" ? 20 : 0;
|
|
3743
|
+
case "AUTHORITY_NORMALIZED":
|
|
3744
|
+
return 0;
|
|
3745
|
+
case "DECIDED":
|
|
3746
|
+
return prep.decision?.verdict === "REVIEW" ? 10 : prep.decision?.verdict === "BLOCK" ? 20 : 0;
|
|
3747
|
+
case "FETCH_REJECTED":
|
|
3748
|
+
case "EVIDENCE_PARTIAL":
|
|
3749
|
+
return 10;
|
|
3750
|
+
default:
|
|
3751
|
+
return 20;
|
|
3752
|
+
}
|
|
3753
|
+
}
|
|
3754
|
+
|
|
3755
|
+
// ../../packages/core/src/gateway/authority.ts
|
|
3756
|
+
function approvalLabel(c) {
|
|
3757
|
+
if (c.approvalRequirement === "none") return null;
|
|
3758
|
+
switch (c.pattern) {
|
|
3759
|
+
case "privilege-escalation":
|
|
3760
|
+
return "privilege-escalation";
|
|
3761
|
+
case "auto-exec-bypass":
|
|
3762
|
+
return "unattended-execution";
|
|
3763
|
+
case "data-exfil":
|
|
3764
|
+
return "data-exfiltration";
|
|
3765
|
+
case "hidden-override":
|
|
3766
|
+
return "instruction-override";
|
|
3767
|
+
case "sensitive-file-read":
|
|
3768
|
+
return "secret-read";
|
|
3769
|
+
case "messaging-financial":
|
|
3770
|
+
return c.resource === "financial" ? "financial-action" : "external-messaging";
|
|
3771
|
+
default:
|
|
3772
|
+
break;
|
|
3773
|
+
}
|
|
3774
|
+
if (c.action === "connect" && c.resource === "network") return "external-network-access";
|
|
3775
|
+
if (c.action === "read" && c.resource === "secret") return "secret-read";
|
|
3776
|
+
if (c.action === "execute" && c.resource === "process") return "process-exec";
|
|
3777
|
+
return "review";
|
|
3778
|
+
}
|
|
3779
|
+
function deriveLimits(caps) {
|
|
3780
|
+
let spendPerCall = null;
|
|
3781
|
+
for (const c of caps) {
|
|
3782
|
+
if (c.action === "spend" && typeof c.monetaryLimit === "number") {
|
|
3783
|
+
spendPerCall = spendPerCall === null ? c.monetaryLimit : Math.min(spendPerCall, c.monetaryLimit);
|
|
3784
|
+
}
|
|
3785
|
+
}
|
|
3786
|
+
return { spendPerCall, spendTotal: null };
|
|
3787
|
+
}
|
|
3788
|
+
function buildAuthorityManifest(input) {
|
|
3789
|
+
const servers = input.servers ?? [];
|
|
3790
|
+
const surfaces = input.surfaces ?? [];
|
|
3791
|
+
const configCaps = servers.flatMap((s) => deriveConfigCapabilities(s));
|
|
3792
|
+
const instructionCaps = extractInstructionAuthority(surfaces);
|
|
3793
|
+
const capabilities = sortCapabilities([...configCaps, ...instructionCaps]);
|
|
3794
|
+
const unknowns = [];
|
|
3795
|
+
if (input.artifactDigest === null) {
|
|
3796
|
+
unknowns.push(
|
|
3797
|
+
"artifact did not resolve to a digest \u2014 authority is over an unpinned target"
|
|
3798
|
+
);
|
|
3799
|
+
}
|
|
3800
|
+
for (const s of surfaces) {
|
|
3801
|
+
if (s.truncated) {
|
|
3802
|
+
unknowns.push(`surface truncated at size cap \u2014 authority past the cap is unread: ${s.path}`);
|
|
3803
|
+
}
|
|
3804
|
+
}
|
|
3805
|
+
unknowns.sort();
|
|
3806
|
+
const required = [...new Set(capabilities.map(approvalLabel).filter((l) => l !== null))].sort();
|
|
3807
|
+
const anyPartial = capabilities.some((c) => c.completeness === "partial");
|
|
3808
|
+
const completeness = anyPartial || unknowns.length > 0 ? "partial" : "complete";
|
|
3809
|
+
const sealed = {
|
|
3810
|
+
schema: AUTHORITY_SCHEMA_VERSION,
|
|
3811
|
+
subject: { artifactDigest: input.artifactDigest },
|
|
3812
|
+
capabilities,
|
|
3813
|
+
limits: deriveLimits(capabilities),
|
|
3814
|
+
approval: { required },
|
|
3815
|
+
unknowns,
|
|
3816
|
+
completeness
|
|
3817
|
+
};
|
|
3818
|
+
return { ...sealed, digest: hashJson(sealed) };
|
|
3819
|
+
}
|
|
3820
|
+
|
|
3821
|
+
// ../../packages/report-renderer/src/style.ts
|
|
3822
|
+
var DEFAULT_STYLE = { emoji: true };
|
|
3823
|
+
var NO_EMOJI_STYLE = { emoji: false };
|
|
3824
|
+
function verdictTag(verdict, style) {
|
|
3825
|
+
return style.emoji ? VERDICT_CLI_SYMBOL[verdict] : VERDICT_TEXT_SYMBOL[verdict];
|
|
3826
|
+
}
|
|
3827
|
+
function symbolTag(symbol, style) {
|
|
3828
|
+
return style.emoji ? `${RISK_SYMBOL_EMOJI[symbol]} ${symbol}` : symbol;
|
|
3829
|
+
}
|
|
3830
|
+
function symbolList(symbols, style) {
|
|
3831
|
+
if (symbols.length === 0) return "\u2014";
|
|
3832
|
+
return symbols.map((s) => symbolTag(s, style)).join(" ");
|
|
3833
|
+
}
|
|
3834
|
+
|
|
3835
|
+
// ../../packages/report-renderer/src/renderJson.ts
|
|
3836
|
+
function renderJson(summary) {
|
|
3837
|
+
return JSON.stringify(summary, null, 2);
|
|
3838
|
+
}
|
|
3839
|
+
|
|
3840
|
+
// ../../packages/report-renderer/src/renderTerminal.ts
|
|
3841
|
+
function renderFindingLine(f) {
|
|
3842
|
+
const lines = [];
|
|
3843
|
+
const flag = f.blocker ? "[BLOCKER] " : "";
|
|
3844
|
+
lines.push(` \u2022 ${flag}${f.title} (${f.id}, ${f.mode.toLowerCase()}, confidence ${f.confidence})`);
|
|
3845
|
+
if (f.evidence.length > 0) {
|
|
3151
3846
|
const e = f.evidence[0];
|
|
3152
3847
|
const ev = e.value ?? e.snippet ?? e.key ?? e.path ?? "";
|
|
3153
3848
|
lines.push(` evidence: ${e.key ?? e.type}${ev ? ` = ${ev}` : ""}`);
|
|
@@ -3626,7 +4321,7 @@ function renderDiagnostics(summary) {
|
|
|
3626
4321
|
var LOGO_REPORT_BASE64 = "iVBORw0KGgoAAAANSUhEUgAAADAAAAAwCAYAAABXAvmHAAAACXBIWXMAAAPoAAAD6AG1e1JrAAAKdElEQVRo3u1aeXAT1xlf35cs2bJ8yJYtbGxjG+rgkJCjdaBMSYBAhstAhwSYZDJ105CGnBwNSUlKmibNJDR0GJrJQELb1C2kgQ6dtJwFAgFyTAB7tVpprdvyjW3Zkvb4+r2nAzVtZ/qHhO2ZLrOjZd/bx/f73u87Hwzz/+u/X8AwSeSezABSgJmVNikFJ7+m3PINnLq8NfbdpKAN+f1So8kTVAarNbfM9YWqpDB2bFJo36Iu32tP14EDb4umYh9510YpNYFBoHCp5JfTGTc5GA103vU9qbN5gWTHZ0tB1WYydhLnTDgQRKCTYeHZwurHhLRC4HVTZD/LKX7eqpiLqmQhTQfmgsofRUC8yDDJE0H2pFhacAVVzwtIGXNOsTxy6qwS7LRDwCLAyJlPFbNKL9sQBKub8sI3QY+jiwxp0W5o0Jq0Vb91JuUBX2CURk6eUZQxPwgzm8E64w6QfT4EcV7hdZWSMxnnaCvbWH2tLrxOctt4eiizvnYpn1NmcTLZ0Nl4t0hoIw8Ng+2ehWBKzgdTSj7Yvn0vSIPXwc/xSmfTd0Qy15xTKphLqlvGxR44pjqD05St4LJLT9iIsabrlK4nt0iKKIG/gwNhxl3A4m6YMouAyyqmQKwNs8HfzoIiy+B9drskZOgUG6MGNlt/ukNTtlJgjJkRWiZM8IjhsZqKSkd26ag7txycyx6Sxr6+KgNeA3v3AZdXAaaMQmBRONs9C8A+bwk+5wKXWQycphz697xHpsLYlQ7ZuWKd5FYZwJZTOnxFa6yPUCrhQepqwxwVarjdOWsukUUeu/g52Ocuhg4mh1KGRYpY62+HgLUTgjYHCA130HcsjpE5tuaFMPrZZUDUsvPO+fhOY/bMfzAnocGOGNpVhkmnQapyRjHL5PW65iwC0TeqWOtvQyHSwZxrAKHudvBu2gqixwsKoiO36O2G7mdeAKF+NnA4h8y11MwEyR9QXN8lwNU9jurZBqoc/DcSZtQR7VhLan9mYzLAu/WnstjTA3y+EThVKZiYPOh99ZeUIooogixJoRufydX72lu4E3k4twy9VRUEPV3g3fYyBroM6CyZ9mosTePOffQaq0xq/UY2u/SQBenAl9UqYm8fDOz+DQqeg9ovA1N6IQlW4L/aHgKBwtMbnwNo3GZdVWiOuhy/yYaBXXtA7OsHS9k0wDUVdAqHTGrDRlOO4ftxoVPsAi5V6YnBHAO4NBWSY/5SOSjYqMfh9bVgwgDFZeuRHuWU696Nz4Z34Yb2vU9uRqpkUZpxOaXApRcBX1ILAZaDINqK/d7lijvPKA2gUbtVZefiYtCxAFDIY47iGhj78usA0ajv9DmwVjZSo+VyysCcpUffrgdTqo6+l3B3ojaAWrZWN9ExKnxWCZiRRqbUArBMvQV8p85SkL4LlwLOiukITncm7gDY5PwTbkMDKIoieZ/ahtrMoO6SahS1b47eKGCKFoaP/h0i18jx0xgLtFR4c+xc/Jas0cFk4g5tQXc2JnVV34pz884lAID2hAcBoOeQnA+sQaqokMsGYDE1MKFGuQgQNaFRJvT94u0oAPJMYwHSw0xAkLn4DZusprGBjDnvXwXSyEgYgDb+ALhk7fGu8umE05Jr9cOotTSwNy+A7m0vI40QBBEQeW0mAiXlg/exp6MAPPhMhDRrKugcAp5NzYPuLTvAhq64A9Mq18p1IPl8CKApQQBStcc9BgQQDEquNY9AOw73hV2m728nwLVkDfCFU4FL09Ixx+LllP/kcixdDejf0QMVgLmwGsdWwcgnx0Ou9ZXX4RqORQFMRQApCQGgO+Ypa8AdCErOZWvBgh5E7OkNGaoSEpVE3uHDR6HvzXdg8Hd/jBrx4IcHoe+NXTD88VE6h16yEjLw7h7qyQgoaXRUclfeQmzoH3FLKyIg0IN87CmZBuLwsOR+5HEYOnQkJAcJWOQOBqNAIhf5+7+9wz90bjjQkWvooyPg2tAK4uCgRByFKa3gaNwARMK6Ob14X7fGCGJvr+h94nnwPPpEFIASFiTi+0mAE3v7QwCIlvv76Ts5Zh6N0MFQjPC0PgUejB3Brm7Jie4VM9j347kDtGrC7sIrXiwVx65cE/t/tZfy1nfsVEhoFGT0/EXoeW47OBauBB557Fr7aNQG3A8/DpYpjeBY1ALdOIfMVXAXqP0cO03X6seIPnrlmuQk9bOmfEes8uLSZbAXVa/txEg6+Ps/yaNnL1BD9az7AYjuLnAtexDQRsIZpwbHMsFxf0sUgHPFQ/guhXoelmSsGLnJN2JXN7jXt9K1fGc+xbUPSS70UILOuCZuvaTINrprmuo5JjtI0gQ0XgUbVmh800CovRWFyqL+ncN8KOTX1eBGcFE3ijbTQaqwPGM08NGUu3om5lR1YM4n1OxTvEgljCFjjsoZjXE34qstLel8Uv4XHqysJL9fdi1eTYXgsopionEJBUGEJVSJXCRWkDqAi43auZTrdA0X8UC4pnvGncAzmq/a2tpSEtKs6sw17PRgkPKdvyhdR9dI6MDjTtBELnKHE7rBd/dHAQwe+AO+09A0+l/SDnVo7lDbRzBy4RLSB8tTjWFn3Pj/TRo5jQ0zeSY74EHeYt6iWGuaaEJGEjMiUGyiNvrZ51EA/mtsKN3OihGegMFvrUhBspZ7XSuYmWy/raJuVkJKy2gxk1lyyIUC+M0WaeDd9yn/I9QgGiYFfGdTM8iBQDQOEJ9P0g5TeBdCFCqn3w7iGmMmXnJhEtiZrT+YsLIyohGLvr7ZwqhE16r1GJFkxd58HzVaCgK1TCjRu+O1G0Eu7OtJJCZj1F7wJt/Ysd0CkqQ4V64HsqZJX9Oc0MI+srCQa9jrRm0OHfmrGOgwoUBYTpKuQwYWKUVVtBt3oyKjDQsQnS7gS2swmSsM0U2Fu3ilHa4fPioS7pM1b1pXQjA2lAipOqvDUA+BLq98/YMPqUZNaOD9b+8JaZ9QCHtAhELkOdp2SSlAKqlg8L0DEPR2yzZcA9fi2yvq9DelBR9tnxfXLRYYteyYt4SoWOnZvpMGMOL/ScsksgOR9GH08lcYDzbS4qVn80ukpaLY5j2gWBm1xJfULEq49v9TfmTRVm4lkdOz4YeU6N4fb6ZpgQljQ9+bu6NeqP8dLPyx3CRjXeE6AYMbthixKNJWbhmXE5yoUedX/NqDgmCCR5Ob7md+QrVMAtfQwcMwgqUloRfpBXk3PgfhOUE3juPBx56EtVL+1xPItpaWFHSJBzwoEO5AkNLppZ+HmldV36IdOlJxEdqQMe/TRHgV8JryAyfnzCGJ4vidZEZAnGTmpPKaiv0ERJhOysDe/cCma7C8xP7PW9Swla7WTVTzGL0/uDyLnl6O/zEsEYBQABhI4jXGXU4U0HnfckkKBOThv3yCacKfMVzIsmvZWnSXGAfyp+wG3LUJdYYcFibUucazMIFRyY7GuxW/zS4FPF2S/ba5ikAMVjd1W8R+JuJhX1LEO+Hpy1ILo+l1ldaBGzsZPJPbz+qmrposJ5UhF1vaWMum6y5g5L0kGKfXT7bDbkonL54jcNWz1XFPj2/GRQz7xTDXYYIcqU7I/63yT1NyvbeodjVcAAAAAElFTkSuQmCC";
|
|
3627
4322
|
|
|
3628
4323
|
// ../../packages/report-renderer/src/renderHtml.ts
|
|
3629
|
-
function
|
|
4324
|
+
function esc2(value) {
|
|
3630
4325
|
return String(value ?? "").replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
3631
4326
|
}
|
|
3632
4327
|
var VERDICT_COLOR = {
|
|
@@ -3636,50 +4331,50 @@ var VERDICT_COLOR = {
|
|
|
3636
4331
|
UNKNOWN: "#6e7781"
|
|
3637
4332
|
};
|
|
3638
4333
|
function badge(verdict) {
|
|
3639
|
-
return `<span class="badge" style="background:${VERDICT_COLOR[verdict]}">${
|
|
4334
|
+
return `<span class="badge" style="background:${VERDICT_COLOR[verdict]}">${esc2(verdict)}</span>`;
|
|
3640
4335
|
}
|
|
3641
4336
|
function findingRow(f) {
|
|
3642
4337
|
const ev = f.evidence.map((e) => {
|
|
3643
|
-
const loc = e.path ? ` (${
|
|
4338
|
+
const loc = e.path ? ` (${esc2(e.path)}${e.line ? `:${e.line}` : ""})` : "";
|
|
3644
4339
|
const detail = e.value ?? e.snippet ?? "";
|
|
3645
|
-
return `<li><code>${
|
|
4340
|
+
return `<li><code>${esc2(e.type)}</code> ${esc2(e.key ?? "")}${detail ? ` = ${esc2(detail)}` : ""}${loc}</li>`;
|
|
3646
4341
|
}).join("");
|
|
3647
4342
|
return `
|
|
3648
|
-
<tr class="sev-${
|
|
3649
|
-
<td>${f.blocker ? "\u26D4 " : ""}${
|
|
3650
|
-
<td><code>${
|
|
3651
|
-
<td>${
|
|
3652
|
-
<td>${
|
|
3653
|
-
<td>${
|
|
3654
|
-
<td>${
|
|
4343
|
+
<tr class="sev-${esc2(f.severity)}">
|
|
4344
|
+
<td>${f.blocker ? "\u26D4 " : ""}${esc2(f.title)}</td>
|
|
4345
|
+
<td><code>${esc2(f.id)}</code></td>
|
|
4346
|
+
<td>${esc2(RISK_SYMBOL_LABEL[f.symbol])} <small>(${esc2(f.symbol)})</small></td>
|
|
4347
|
+
<td>${esc2(f.riskClass)}</td>
|
|
4348
|
+
<td>${esc2(f.severity)}</td>
|
|
4349
|
+
<td>${esc2(f.mode)}</td>
|
|
3655
4350
|
<td>
|
|
3656
|
-
<div class="impact">${
|
|
3657
|
-
<div class="fix"><strong>Fix:</strong> ${
|
|
4351
|
+
<div class="impact">${esc2(f.impact)}</div>
|
|
4352
|
+
<div class="fix"><strong>Fix:</strong> ${esc2(f.fix)}</div>
|
|
3658
4353
|
${f.evidence.length ? `<ul class="evidence">${ev}</ul>` : ""}
|
|
3659
|
-
${f.falsePositiveNote ? `<div class="fp"><em>${
|
|
4354
|
+
${f.falsePositiveNote ? `<div class="fp"><em>${esc2(f.falsePositiveNote)}</em></div>` : ""}
|
|
3660
4355
|
</td>
|
|
3661
4356
|
</tr>`;
|
|
3662
4357
|
}
|
|
3663
4358
|
function serverCard(r) {
|
|
3664
|
-
const symbols = r.symbols.map((s) => `<span class="sym">${
|
|
4359
|
+
const symbols = r.symbols.map((s) => `<span class="sym">${esc2(RISK_SYMBOL_LABEL[s])}</span>`).join(" ");
|
|
3665
4360
|
const findings = r.findings.length ? `<table class="findings">
|
|
3666
4361
|
<thead><tr><th>Finding</th><th>Rule</th><th>Symbol</th><th>Class</th><th>Severity</th><th>Mode</th><th>Detail</th></tr></thead>
|
|
3667
4362
|
<tbody>${r.findings.map(findingRow).join("")}</tbody>
|
|
3668
4363
|
</table>` : `<p class="none">No findings.</p>`;
|
|
3669
4364
|
return `
|
|
3670
4365
|
<section class="server">
|
|
3671
|
-
<h2>${badge(r.verdict)} ${
|
|
4366
|
+
<h2>${badge(r.verdict)} ${esc2(r.target.name)}</h2>
|
|
3672
4367
|
<p class="meta">
|
|
3673
|
-
Class <strong>${
|
|
3674
|
-
\xB7 confidence ${
|
|
3675
|
-
\xB7 reproducibility ${
|
|
4368
|
+
Class <strong>${esc2(r.riskClass)}</strong> ${esc2(RISK_CLASS_LABEL[r.riskClass])}
|
|
4369
|
+
\xB7 confidence ${esc2(r.confidence)}
|
|
4370
|
+
\xB7 reproducibility ${esc2(r.reproducibility.level)}
|
|
3676
4371
|
</p>
|
|
3677
4372
|
<p class="symbols">${symbols || '<span class="sym none">no risk symbols</span>'}</p>
|
|
3678
|
-
<p class="summary">${
|
|
4373
|
+
<p class="summary">${esc2(r.summary)}</p>
|
|
3679
4374
|
${findings}
|
|
3680
|
-
<p class="policy">autonomous use: <strong>${
|
|
3681
|
-
\xB7 manual approval: <strong>${
|
|
3682
|
-
\xB7 sandbox: <strong>${
|
|
4375
|
+
<p class="policy">autonomous use: <strong>${esc2(r.policy.autonomousUse)}</strong>
|
|
4376
|
+
\xB7 manual approval: <strong>${esc2(r.policy.manualApproval)}</strong>
|
|
4377
|
+
\xB7 sandbox: <strong>${esc2(r.policy.sandbox)}</strong></p>
|
|
3683
4378
|
</section>`;
|
|
3684
4379
|
}
|
|
3685
4380
|
var LOGO_SRC = `data:image/png;base64,${LOGO_REPORT_BASE64}`;
|
|
@@ -3722,7 +4417,7 @@ function renderHtml(summary) {
|
|
|
3722
4417
|
<head>
|
|
3723
4418
|
<meta charset="utf-8">
|
|
3724
4419
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
3725
|
-
<title>CallLint report \u2014 ${
|
|
4420
|
+
<title>CallLint report \u2014 ${esc2(summary.configPath)}</title>
|
|
3726
4421
|
<style>${STYLE}</style>
|
|
3727
4422
|
</head>
|
|
3728
4423
|
<body>
|
|
@@ -3731,7 +4426,7 @@ function renderHtml(summary) {
|
|
|
3731
4426
|
<img class="brand-mark" src="${LOGO_SRC}" width="40" height="40" alt="CallLint">
|
|
3732
4427
|
<div>
|
|
3733
4428
|
<h1>CallLint report ${badge(summary.verdict)}</h1>
|
|
3734
|
-
<div class="sub">${
|
|
4429
|
+
<div class="sub">${esc2(summary.configPath)} \xB7 generated ${esc2(summary.generatedAt)}</div>
|
|
3735
4430
|
</div>
|
|
3736
4431
|
</div>
|
|
3737
4432
|
</header>
|
|
@@ -3883,11 +4578,437 @@ function readDocumentSurfaces(dir) {
|
|
|
3883
4578
|
}
|
|
3884
4579
|
|
|
3885
4580
|
// src/commands/scan.ts
|
|
3886
|
-
import { readFileSync as
|
|
4581
|
+
import { readFileSync as readFileSync12, writeFileSync as writeFileSync3 } from "node:fs";
|
|
4582
|
+
|
|
4583
|
+
// ../../packages/discovery/src/registry.ts
|
|
4584
|
+
var ExtractorRegistry = class {
|
|
4585
|
+
extractors = /* @__PURE__ */ new Map();
|
|
4586
|
+
/**
|
|
4587
|
+
* Register an agent extractor.
|
|
4588
|
+
*/
|
|
4589
|
+
register(extractor) {
|
|
4590
|
+
if (this.extractors.has(extractor.agentType)) {
|
|
4591
|
+
throw new Error(`Extractor already registered for agent type: ${extractor.agentType}`);
|
|
4592
|
+
}
|
|
4593
|
+
this.extractors.set(extractor.agentType, extractor);
|
|
4594
|
+
}
|
|
4595
|
+
/**
|
|
4596
|
+
* Get extractor for a specific agent type.
|
|
4597
|
+
*/
|
|
4598
|
+
get(agentType) {
|
|
4599
|
+
return this.extractors.get(agentType);
|
|
4600
|
+
}
|
|
4601
|
+
/**
|
|
4602
|
+
* Get all registered extractors.
|
|
4603
|
+
*/
|
|
4604
|
+
getAll() {
|
|
4605
|
+
return Array.from(this.extractors.values());
|
|
4606
|
+
}
|
|
4607
|
+
/**
|
|
4608
|
+
* Get extractors filtered by agent types.
|
|
4609
|
+
*/
|
|
4610
|
+
getByTypes(agentTypes) {
|
|
4611
|
+
return agentTypes.map((type) => this.extractors.get(type)).filter((e) => e !== void 0);
|
|
4612
|
+
}
|
|
4613
|
+
/**
|
|
4614
|
+
* Get extractors filtered by priority tier.
|
|
4615
|
+
*/
|
|
4616
|
+
getByPriority(priority) {
|
|
4617
|
+
return this.getAll().filter((e) => e.priority === priority);
|
|
4618
|
+
}
|
|
4619
|
+
/**
|
|
4620
|
+
* Get all extractors sorted by priority (P0 first, P3 last).
|
|
4621
|
+
*/
|
|
4622
|
+
getAllSortedByPriority() {
|
|
4623
|
+
const priorityOrder = {
|
|
4624
|
+
P0: 0,
|
|
4625
|
+
P1: 1,
|
|
4626
|
+
P2: 2,
|
|
4627
|
+
P3: 3
|
|
4628
|
+
};
|
|
4629
|
+
return this.getAll().sort((a, b) => {
|
|
4630
|
+
return priorityOrder[a.priority] - priorityOrder[b.priority];
|
|
4631
|
+
});
|
|
4632
|
+
}
|
|
4633
|
+
/**
|
|
4634
|
+
* Clear all registered extractors (for testing).
|
|
4635
|
+
*/
|
|
4636
|
+
clear() {
|
|
4637
|
+
this.extractors.clear();
|
|
4638
|
+
}
|
|
4639
|
+
};
|
|
4640
|
+
var registry = new ExtractorRegistry();
|
|
4641
|
+
|
|
4642
|
+
// ../../packages/discovery/src/extractors/base.ts
|
|
4643
|
+
var BaseAgentExtractor = class {
|
|
4644
|
+
/**
|
|
4645
|
+
* Resolve home directory (~) to absolute path.
|
|
4646
|
+
*/
|
|
4647
|
+
resolveHome() {
|
|
4648
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
4649
|
+
if (!home) {
|
|
4650
|
+
throw new Error("Could not resolve home directory (HOME/USERPROFILE not set)");
|
|
4651
|
+
}
|
|
4652
|
+
return home;
|
|
4653
|
+
}
|
|
4654
|
+
/**
|
|
4655
|
+
* Get platform-specific application data directory.
|
|
4656
|
+
*
|
|
4657
|
+
* Windows: %APPDATA% (Roaming)
|
|
4658
|
+
* macOS: ~/Library/Application Support
|
|
4659
|
+
* Linux: ~/.config (XDG_CONFIG_HOME or default)
|
|
4660
|
+
*/
|
|
4661
|
+
getAppDataDir() {
|
|
4662
|
+
const platform = process.platform;
|
|
4663
|
+
if (platform === "win32") {
|
|
4664
|
+
const appData = process.env.APPDATA;
|
|
4665
|
+
if (!appData) {
|
|
4666
|
+
throw new Error("Could not resolve APPDATA directory on Windows");
|
|
4667
|
+
}
|
|
4668
|
+
return appData;
|
|
4669
|
+
}
|
|
4670
|
+
if (platform === "darwin") {
|
|
4671
|
+
return `${this.resolveHome()}/Library/Application Support`;
|
|
4672
|
+
}
|
|
4673
|
+
return process.env.XDG_CONFIG_HOME || `${this.resolveHome()}/.config`;
|
|
4674
|
+
}
|
|
4675
|
+
};
|
|
4676
|
+
|
|
4677
|
+
// ../../packages/discovery/src/path-resolver.ts
|
|
4678
|
+
import { resolve as resolve2, join as join6, isAbsolute } from "node:path";
|
|
4679
|
+
import { existsSync as existsSync6, statSync as statSync3 } from "node:fs";
|
|
4680
|
+
function resolvePath(path, cwd) {
|
|
4681
|
+
if (path.startsWith("~/") || path === "~") {
|
|
4682
|
+
const home = process.env.HOME || process.env.USERPROFILE;
|
|
4683
|
+
if (!home) {
|
|
4684
|
+
throw new Error("Could not resolve home directory (HOME/USERPROFILE not set)");
|
|
4685
|
+
}
|
|
4686
|
+
return join6(home, path.slice(2));
|
|
4687
|
+
}
|
|
4688
|
+
if (isAbsolute(path)) {
|
|
4689
|
+
return path;
|
|
4690
|
+
}
|
|
4691
|
+
return resolve2(cwd, path);
|
|
4692
|
+
}
|
|
4693
|
+
function isRegularFile(path) {
|
|
4694
|
+
try {
|
|
4695
|
+
if (!existsSync6(path)) {
|
|
4696
|
+
return false;
|
|
4697
|
+
}
|
|
4698
|
+
const stats = statSync3(path);
|
|
4699
|
+
return stats.isFile();
|
|
4700
|
+
} catch {
|
|
4701
|
+
return false;
|
|
4702
|
+
}
|
|
4703
|
+
}
|
|
4704
|
+
function isReasonableSize(path) {
|
|
4705
|
+
try {
|
|
4706
|
+
const stats = statSync3(path);
|
|
4707
|
+
const maxSize = 10 * 1024 * 1024;
|
|
4708
|
+
return stats.size < maxSize;
|
|
4709
|
+
} catch {
|
|
4710
|
+
return false;
|
|
4711
|
+
}
|
|
4712
|
+
}
|
|
4713
|
+
function validateConfigPath(path) {
|
|
4714
|
+
return isRegularFile(path) && isReasonableSize(path);
|
|
4715
|
+
}
|
|
4716
|
+
|
|
4717
|
+
// ../../packages/discovery/src/extractors/cursor.ts
|
|
4718
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
4719
|
+
var CursorExtractor = class extends BaseAgentExtractor {
|
|
4720
|
+
agentType = "cursor";
|
|
4721
|
+
priority = "P0";
|
|
4722
|
+
discover(cwd) {
|
|
4723
|
+
const configs = [];
|
|
4724
|
+
const projectPath = resolvePath(".cursor/mcp.json", cwd);
|
|
4725
|
+
configs.push(this.createConfig(projectPath));
|
|
4726
|
+
try {
|
|
4727
|
+
const home = this.resolveHome();
|
|
4728
|
+
const userPath = resolvePath(".cursor/mcp.json", home);
|
|
4729
|
+
if (userPath !== projectPath) {
|
|
4730
|
+
configs.push(this.createConfig(userPath));
|
|
4731
|
+
}
|
|
4732
|
+
} catch {
|
|
4733
|
+
}
|
|
4734
|
+
return configs;
|
|
4735
|
+
}
|
|
4736
|
+
createConfig(configPath) {
|
|
4737
|
+
const exists = this.isValidConfig(configPath);
|
|
4738
|
+
return {
|
|
4739
|
+
agentType: this.agentType,
|
|
4740
|
+
configPath,
|
|
4741
|
+
exists,
|
|
4742
|
+
kind: "cursor-mcp-config",
|
|
4743
|
+
priority: this.priority
|
|
4744
|
+
};
|
|
4745
|
+
}
|
|
4746
|
+
/**
|
|
4747
|
+
* Check if path is a valid Cursor config.
|
|
4748
|
+
* Must exist, be regular file, reasonable size, and contain mcpServers key.
|
|
4749
|
+
*/
|
|
4750
|
+
isValidConfig(path) {
|
|
4751
|
+
if (!validateConfigPath(path)) {
|
|
4752
|
+
return false;
|
|
4753
|
+
}
|
|
4754
|
+
try {
|
|
4755
|
+
const content = readFileSync7(path, "utf8");
|
|
4756
|
+
const json = JSON.parse(content);
|
|
4757
|
+
return typeof json === "object" && json !== null && "mcpServers" in json;
|
|
4758
|
+
} catch {
|
|
4759
|
+
return false;
|
|
4760
|
+
}
|
|
4761
|
+
}
|
|
4762
|
+
};
|
|
4763
|
+
|
|
4764
|
+
// ../../packages/discovery/src/extractors/claude-code.ts
|
|
4765
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
4766
|
+
import { join as join7 } from "node:path";
|
|
4767
|
+
var ClaudeCodeExtractor = class extends BaseAgentExtractor {
|
|
4768
|
+
agentType = "claude-code";
|
|
4769
|
+
priority = "P0";
|
|
4770
|
+
discover(cwd) {
|
|
4771
|
+
const configs = [];
|
|
4772
|
+
const projectPath = resolvePath(".claude/settings.json", cwd);
|
|
4773
|
+
configs.push(this.createConfig(projectPath));
|
|
4774
|
+
try {
|
|
4775
|
+
const userPath = this.getUserSettingsPath();
|
|
4776
|
+
if (userPath !== projectPath) {
|
|
4777
|
+
configs.push(this.createConfig(userPath));
|
|
4778
|
+
}
|
|
4779
|
+
} catch {
|
|
4780
|
+
}
|
|
4781
|
+
return configs;
|
|
4782
|
+
}
|
|
4783
|
+
getUserSettingsPath() {
|
|
4784
|
+
const appDataDir = this.getAppDataDir();
|
|
4785
|
+
return join7(appDataDir, "Claude", "settings.json");
|
|
4786
|
+
}
|
|
4787
|
+
createConfig(configPath) {
|
|
4788
|
+
const exists = this.isValidConfig(configPath);
|
|
4789
|
+
return {
|
|
4790
|
+
agentType: this.agentType,
|
|
4791
|
+
configPath,
|
|
4792
|
+
exists,
|
|
4793
|
+
kind: "claude-settings",
|
|
4794
|
+
priority: this.priority
|
|
4795
|
+
};
|
|
4796
|
+
}
|
|
4797
|
+
/**
|
|
4798
|
+
* Check if path is a valid Claude Code config.
|
|
4799
|
+
* Must exist, be regular file, reasonable size, and contain mcpServers key.
|
|
4800
|
+
*/
|
|
4801
|
+
isValidConfig(path) {
|
|
4802
|
+
if (!validateConfigPath(path)) {
|
|
4803
|
+
return false;
|
|
4804
|
+
}
|
|
4805
|
+
try {
|
|
4806
|
+
const content = readFileSync8(path, "utf8");
|
|
4807
|
+
const json = JSON.parse(content);
|
|
4808
|
+
return typeof json === "object" && json !== null && "mcpServers" in json;
|
|
4809
|
+
} catch {
|
|
4810
|
+
return false;
|
|
4811
|
+
}
|
|
4812
|
+
}
|
|
4813
|
+
};
|
|
4814
|
+
|
|
4815
|
+
// ../../packages/discovery/src/extractors/claude-desktop.ts
|
|
4816
|
+
import { readFileSync as readFileSync9 } from "node:fs";
|
|
4817
|
+
import { join as join8 } from "node:path";
|
|
4818
|
+
var ClaudeDesktopExtractor = class extends BaseAgentExtractor {
|
|
4819
|
+
agentType = "claude-desktop";
|
|
4820
|
+
priority = "P0";
|
|
4821
|
+
discover(_cwd) {
|
|
4822
|
+
const configs = [];
|
|
4823
|
+
try {
|
|
4824
|
+
const userPath = this.getUserConfigPath();
|
|
4825
|
+
configs.push(this.createConfig(userPath));
|
|
4826
|
+
} catch {
|
|
4827
|
+
}
|
|
4828
|
+
return configs;
|
|
4829
|
+
}
|
|
4830
|
+
getUserConfigPath() {
|
|
4831
|
+
const appDataDir = this.getAppDataDir();
|
|
4832
|
+
return join8(appDataDir, "Claude", "claude_desktop_config.json");
|
|
4833
|
+
}
|
|
4834
|
+
createConfig(configPath) {
|
|
4835
|
+
const exists = this.isValidConfig(configPath);
|
|
4836
|
+
return {
|
|
4837
|
+
agentType: this.agentType,
|
|
4838
|
+
configPath,
|
|
4839
|
+
exists,
|
|
4840
|
+
kind: "claude-settings",
|
|
4841
|
+
priority: this.priority
|
|
4842
|
+
};
|
|
4843
|
+
}
|
|
4844
|
+
/**
|
|
4845
|
+
* Check if path is a valid Claude Desktop config.
|
|
4846
|
+
* Must exist, be regular file, reasonable size, and contain mcpServers key.
|
|
4847
|
+
*/
|
|
4848
|
+
isValidConfig(path) {
|
|
4849
|
+
if (!validateConfigPath(path)) {
|
|
4850
|
+
return false;
|
|
4851
|
+
}
|
|
4852
|
+
try {
|
|
4853
|
+
const content = readFileSync9(path, "utf8");
|
|
4854
|
+
const json = JSON.parse(content);
|
|
4855
|
+
return typeof json === "object" && json !== null && "mcpServers" in json;
|
|
4856
|
+
} catch {
|
|
4857
|
+
return false;
|
|
4858
|
+
}
|
|
4859
|
+
}
|
|
4860
|
+
};
|
|
4861
|
+
|
|
4862
|
+
// ../../packages/discovery/src/extractors/vscode.ts
|
|
4863
|
+
import { readFileSync as readFileSync10 } from "node:fs";
|
|
4864
|
+
import { join as join9 } from "node:path";
|
|
4865
|
+
var VSCodeExtractor = class extends BaseAgentExtractor {
|
|
4866
|
+
agentType = "vscode";
|
|
4867
|
+
priority = "P1";
|
|
4868
|
+
discover(cwd) {
|
|
4869
|
+
const configs = [];
|
|
4870
|
+
try {
|
|
4871
|
+
const userPath = this.getUserConfigPath();
|
|
4872
|
+
configs.push(this.createConfig(userPath));
|
|
4873
|
+
} catch {
|
|
4874
|
+
}
|
|
4875
|
+
return configs;
|
|
4876
|
+
}
|
|
4877
|
+
getUserConfigPath() {
|
|
4878
|
+
const appDataDir = this.getAppDataDir();
|
|
4879
|
+
return join9(appDataDir, "Code", "User", "mcp.json");
|
|
4880
|
+
}
|
|
4881
|
+
createConfig(configPath) {
|
|
4882
|
+
const exists = this.isValidConfig(configPath);
|
|
4883
|
+
return {
|
|
4884
|
+
agentType: this.agentType,
|
|
4885
|
+
configPath,
|
|
4886
|
+
exists,
|
|
4887
|
+
kind: "vscode-mcp-config",
|
|
4888
|
+
priority: this.priority
|
|
4889
|
+
};
|
|
4890
|
+
}
|
|
4891
|
+
/**
|
|
4892
|
+
* Check if path is a valid VS Code MCP config.
|
|
4893
|
+
* Must exist, be regular file, reasonable size, and contain mcpServers key.
|
|
4894
|
+
*/
|
|
4895
|
+
isValidConfig(path) {
|
|
4896
|
+
if (!validateConfigPath(path)) {
|
|
4897
|
+
return false;
|
|
4898
|
+
}
|
|
4899
|
+
try {
|
|
4900
|
+
const content = readFileSync10(path, "utf8");
|
|
4901
|
+
const json = JSON.parse(content);
|
|
4902
|
+
return typeof json === "object" && json !== null && "mcpServers" in json && json.mcpServers !== null;
|
|
4903
|
+
} catch {
|
|
4904
|
+
return false;
|
|
4905
|
+
}
|
|
4906
|
+
}
|
|
4907
|
+
};
|
|
4908
|
+
|
|
4909
|
+
// ../../packages/discovery/src/extractors/windsurf.ts
|
|
4910
|
+
import { readFileSync as readFileSync11 } from "node:fs";
|
|
4911
|
+
import { join as join10 } from "node:path";
|
|
4912
|
+
var WindsurfExtractor = class extends BaseAgentExtractor {
|
|
4913
|
+
agentType = "windsurf";
|
|
4914
|
+
priority = "P1";
|
|
4915
|
+
discover(cwd) {
|
|
4916
|
+
const configs = [];
|
|
4917
|
+
try {
|
|
4918
|
+
const userPath = this.getUserConfigPath();
|
|
4919
|
+
configs.push(this.createConfig(userPath));
|
|
4920
|
+
} catch {
|
|
4921
|
+
}
|
|
4922
|
+
return configs;
|
|
4923
|
+
}
|
|
4924
|
+
getUserConfigPath() {
|
|
4925
|
+
const appDataDir = this.getAppDataDir();
|
|
4926
|
+
return join10(appDataDir, "Windsurf", "mcp.json");
|
|
4927
|
+
}
|
|
4928
|
+
createConfig(configPath) {
|
|
4929
|
+
const exists = this.isValidConfig(configPath);
|
|
4930
|
+
return {
|
|
4931
|
+
agentType: this.agentType,
|
|
4932
|
+
configPath,
|
|
4933
|
+
exists,
|
|
4934
|
+
kind: "windsurf-mcp-config",
|
|
4935
|
+
priority: this.priority
|
|
4936
|
+
};
|
|
4937
|
+
}
|
|
4938
|
+
/**
|
|
4939
|
+
* Check if path is a valid Windsurf MCP config.
|
|
4940
|
+
* Must exist, be regular file, reasonable size, and contain mcpServers key.
|
|
4941
|
+
*/
|
|
4942
|
+
isValidConfig(path) {
|
|
4943
|
+
if (!validateConfigPath(path)) {
|
|
4944
|
+
return false;
|
|
4945
|
+
}
|
|
4946
|
+
try {
|
|
4947
|
+
const content = readFileSync11(path, "utf8");
|
|
4948
|
+
const json = JSON.parse(content);
|
|
4949
|
+
return typeof json === "object" && json !== null && "mcpServers" in json && json.mcpServers !== null;
|
|
4950
|
+
} catch {
|
|
4951
|
+
return false;
|
|
4952
|
+
}
|
|
4953
|
+
}
|
|
4954
|
+
};
|
|
4955
|
+
|
|
4956
|
+
// ../../packages/discovery/src/bootstrap.ts
|
|
4957
|
+
function bootstrapExtractors() {
|
|
4958
|
+
registry.register(new CursorExtractor());
|
|
4959
|
+
registry.register(new ClaudeCodeExtractor());
|
|
4960
|
+
registry.register(new ClaudeDesktopExtractor());
|
|
4961
|
+
registry.register(new VSCodeExtractor());
|
|
4962
|
+
registry.register(new WindsurfExtractor());
|
|
4963
|
+
}
|
|
4964
|
+
bootstrapExtractors();
|
|
4965
|
+
|
|
4966
|
+
// ../../packages/discovery/src/discovery-engine.ts
|
|
4967
|
+
function discoverConfigs(options) {
|
|
4968
|
+
const { cwd, agentTypes, includeMissing = false } = options;
|
|
4969
|
+
const extractors = agentTypes ? registry.getByTypes(agentTypes) : registry.getAllSortedByPriority();
|
|
4970
|
+
const results = extractors.map((extractor) => {
|
|
4971
|
+
try {
|
|
4972
|
+
return extractor.discover(cwd);
|
|
4973
|
+
} catch (error) {
|
|
4974
|
+
console.error(`[discovery] Extractor ${extractor.agentType} failed:`, error);
|
|
4975
|
+
return [];
|
|
4976
|
+
}
|
|
4977
|
+
});
|
|
4978
|
+
const allDiscovered = results.flat();
|
|
4979
|
+
const discovered = includeMissing ? allDiscovered : allDiscovered.filter((config) => config.exists);
|
|
4980
|
+
const searchedPaths = allDiscovered.map((config) => config.configPath);
|
|
4981
|
+
return {
|
|
4982
|
+
cwd,
|
|
4983
|
+
discovered,
|
|
4984
|
+
searchedPaths
|
|
4985
|
+
};
|
|
4986
|
+
}
|
|
4987
|
+
function discoverAgent(agentType, options) {
|
|
4988
|
+
const extractor = registry.get(agentType);
|
|
4989
|
+
if (!extractor) {
|
|
4990
|
+
throw new Error(`Unknown agent type: ${agentType}`);
|
|
4991
|
+
}
|
|
4992
|
+
try {
|
|
4993
|
+
return extractor.discover(options.cwd);
|
|
4994
|
+
} catch (error) {
|
|
4995
|
+
console.error(`[discovery] Failed to discover ${agentType}:`, error);
|
|
4996
|
+
return [];
|
|
4997
|
+
}
|
|
4998
|
+
}
|
|
4999
|
+
|
|
5000
|
+
// src/commands/scan.ts
|
|
3887
5001
|
function scanCommand(args, deps) {
|
|
3888
5002
|
if (flagBool(args.flags, "changed")) {
|
|
3889
5003
|
return scanChangedCommand(args, deps);
|
|
3890
5004
|
}
|
|
5005
|
+
if (flagBool(args.flags, "auto")) {
|
|
5006
|
+
return scanAutoCommand(args, deps);
|
|
5007
|
+
}
|
|
5008
|
+
const agentType = flagStr(args.flags, "agent");
|
|
5009
|
+
if (agentType) {
|
|
5010
|
+
return scanAgentCommand(agentType, args, deps);
|
|
5011
|
+
}
|
|
3891
5012
|
const policyPath = flagStr(args.flags, "policy");
|
|
3892
5013
|
let policy;
|
|
3893
5014
|
try {
|
|
@@ -3908,7 +5029,7 @@ function scanCommand(args, deps) {
|
|
|
3908
5029
|
}
|
|
3909
5030
|
function scanOneConfig(text, configPath, policy, args, deps, allowReceipt = true) {
|
|
3910
5031
|
const surfaceDir = flagStr(args.flags, "surface-dir");
|
|
3911
|
-
const surfaces = surfaceDir ? readDocumentSurfaces(
|
|
5032
|
+
const surfaces = surfaceDir ? readDocumentSurfaces(resolve3(deps.cwd, surfaceDir)) : void 0;
|
|
3912
5033
|
let summary;
|
|
3913
5034
|
try {
|
|
3914
5035
|
summary = scanConfigText(text, configPath, {
|
|
@@ -3930,7 +5051,7 @@ function scanOneConfig(text, configPath, policy, args, deps, allowReceipt = true
|
|
|
3930
5051
|
}
|
|
3931
5052
|
if (deps.writeCacheFile !== false) {
|
|
3932
5053
|
try {
|
|
3933
|
-
writeCache(summary,
|
|
5054
|
+
writeCache(summary, join11(deps.cwd, ".calllint", "last-scan.json"));
|
|
3934
5055
|
} catch {
|
|
3935
5056
|
}
|
|
3936
5057
|
}
|
|
@@ -3956,7 +5077,7 @@ function writeReceiptFile(summary, text, configPath, policy, args, deps) {
|
|
|
3956
5077
|
},
|
|
3957
5078
|
deps.generatedAt
|
|
3958
5079
|
);
|
|
3959
|
-
const outPath =
|
|
5080
|
+
const outPath = resolve3(
|
|
3960
5081
|
deps.cwd,
|
|
3961
5082
|
flagStr(args.flags, "receipt-out") ?? "calllint-receipt.json"
|
|
3962
5083
|
);
|
|
@@ -4004,7 +5125,7 @@ function scanChangedCommand(args, deps) {
|
|
|
4004
5125
|
};
|
|
4005
5126
|
}
|
|
4006
5127
|
const results = paths.map((p) => {
|
|
4007
|
-
const text =
|
|
5128
|
+
const text = readFileSync12(p, "utf8");
|
|
4008
5129
|
return scanOneConfig(text, p, policy, args, deps);
|
|
4009
5130
|
});
|
|
4010
5131
|
const exitCode = results.reduce((worst, r) => Math.max(worst, r.exitCode), EXIT.OK);
|
|
@@ -4017,28 +5138,117 @@ function scanChangedCommand(args, deps) {
|
|
|
4017
5138
|
const stderr = results.map((r) => r.stderr).filter(Boolean).join("\n");
|
|
4018
5139
|
return { stdout, exitCode, ...stderr ? { stderr } : {} };
|
|
4019
5140
|
}
|
|
4020
|
-
|
|
4021
|
-
|
|
4022
|
-
|
|
4023
|
-
const style = flagBool(args.flags, "no-emoji") ? NO_EMOJI_STYLE : DEFAULT_STYLE;
|
|
4024
|
-
const opts = { now: deps.now, generatedAt: deps.generatedAt };
|
|
4025
|
-
const input = resolveConfigInput(args, deps);
|
|
4026
|
-
if (isInputError(input)) {
|
|
4027
|
-
return { stdout: "", stderr: input.error, exitCode: input.exitCode };
|
|
4028
|
-
}
|
|
4029
|
-
let decisions;
|
|
5141
|
+
function scanAutoCommand(args, deps) {
|
|
5142
|
+
const policyPath = flagStr(args.flags, "policy");
|
|
5143
|
+
let policy;
|
|
4030
5144
|
try {
|
|
4031
|
-
|
|
4032
|
-
|
|
4033
|
-
|
|
4034
|
-
|
|
4035
|
-
|
|
4036
|
-
|
|
4037
|
-
|
|
4038
|
-
|
|
4039
|
-
|
|
4040
|
-
|
|
4041
|
-
|
|
5145
|
+
policy = loadPolicyOrDefault(policyPath);
|
|
5146
|
+
} catch (err2) {
|
|
5147
|
+
return {
|
|
5148
|
+
stdout: "",
|
|
5149
|
+
stderr: err2 instanceof Error ? err2.message : String(err2),
|
|
5150
|
+
exitCode: EXIT.ERROR
|
|
5151
|
+
};
|
|
5152
|
+
}
|
|
5153
|
+
const discovered = discoverConfigs({ cwd: deps.cwd });
|
|
5154
|
+
if (discovered.discovered.length === 0) {
|
|
5155
|
+
return {
|
|
5156
|
+
stdout: "",
|
|
5157
|
+
stderr: "No agent configs discovered. Try: calllint inventory\n",
|
|
5158
|
+
exitCode: EXIT.ERROR
|
|
5159
|
+
};
|
|
5160
|
+
}
|
|
5161
|
+
const results = [];
|
|
5162
|
+
for (const config of discovered.discovered) {
|
|
5163
|
+
let text;
|
|
5164
|
+
try {
|
|
5165
|
+
text = readFileSync12(config.configPath, "utf8");
|
|
5166
|
+
} catch (err2) {
|
|
5167
|
+
results.push({
|
|
5168
|
+
stdout: "",
|
|
5169
|
+
stderr: `Could not read ${config.configPath}: ${err2 instanceof Error ? err2.message : String(err2)}`,
|
|
5170
|
+
exitCode: EXIT.ERROR
|
|
5171
|
+
});
|
|
5172
|
+
continue;
|
|
5173
|
+
}
|
|
5174
|
+
const result = scanOneConfig(text, config.configPath, policy, args, deps, false);
|
|
5175
|
+
results.push(result);
|
|
5176
|
+
}
|
|
5177
|
+
const isJson = flagBool(args.flags, "json");
|
|
5178
|
+
const stdout = isJson ? JSON.stringify(results.map((r) => JSON.parse(r.stdout)), null, 2) + "\n" : results.map((r) => r.stdout).join("\n---\n\n");
|
|
5179
|
+
const exitCode = flagBool(args.flags, "ci") ? Math.max(...results.map((r) => r.exitCode)) : EXIT.OK;
|
|
5180
|
+
const stderr = results.map((r) => r.stderr).filter(Boolean).join("\n");
|
|
5181
|
+
return { stdout, exitCode, ...stderr ? { stderr } : {} };
|
|
5182
|
+
}
|
|
5183
|
+
function scanAgentCommand(agentType, args, deps) {
|
|
5184
|
+
const policyPath = flagStr(args.flags, "policy");
|
|
5185
|
+
let policy;
|
|
5186
|
+
try {
|
|
5187
|
+
policy = loadPolicyOrDefault(policyPath);
|
|
5188
|
+
} catch (err2) {
|
|
5189
|
+
return {
|
|
5190
|
+
stdout: "",
|
|
5191
|
+
stderr: err2 instanceof Error ? err2.message : String(err2),
|
|
5192
|
+
exitCode: EXIT.ERROR
|
|
5193
|
+
};
|
|
5194
|
+
}
|
|
5195
|
+
const discovered = discoverAgent(agentType, { cwd: deps.cwd });
|
|
5196
|
+
const existing = discovered.filter((c) => c.exists);
|
|
5197
|
+
if (existing.length === 0) {
|
|
5198
|
+
return {
|
|
5199
|
+
stdout: "",
|
|
5200
|
+
stderr: `No config found for agent '${agentType}'. Try: calllint inventory
|
|
5201
|
+
`,
|
|
5202
|
+
exitCode: EXIT.ERROR
|
|
5203
|
+
};
|
|
5204
|
+
}
|
|
5205
|
+
const results = [];
|
|
5206
|
+
for (const config of existing) {
|
|
5207
|
+
let text;
|
|
5208
|
+
try {
|
|
5209
|
+
text = readFileSync12(config.configPath, "utf8");
|
|
5210
|
+
} catch (err2) {
|
|
5211
|
+
results.push({
|
|
5212
|
+
stdout: "",
|
|
5213
|
+
stderr: `Could not read ${config.configPath}: ${err2 instanceof Error ? err2.message : String(err2)}`,
|
|
5214
|
+
exitCode: EXIT.ERROR
|
|
5215
|
+
});
|
|
5216
|
+
continue;
|
|
5217
|
+
}
|
|
5218
|
+
const result = scanOneConfig(text, config.configPath, policy, args, deps, true);
|
|
5219
|
+
results.push(result);
|
|
5220
|
+
}
|
|
5221
|
+
if (results.length === 1) {
|
|
5222
|
+
return results[0];
|
|
5223
|
+
}
|
|
5224
|
+
const isJson = flagBool(args.flags, "json");
|
|
5225
|
+
const stdout = isJson ? JSON.stringify(results.map((r) => JSON.parse(r.stdout)), null, 2) + "\n" : results.map((r) => r.stdout).join("\n---\n\n");
|
|
5226
|
+
const exitCode = flagBool(args.flags, "ci") ? Math.max(...results.map((r) => r.exitCode)) : EXIT.OK;
|
|
5227
|
+
const stderr = results.map((r) => r.stderr).filter(Boolean).join("\n");
|
|
5228
|
+
return { stdout, exitCode, ...stderr ? { stderr } : {} };
|
|
5229
|
+
}
|
|
5230
|
+
|
|
5231
|
+
// src/commands/check.ts
|
|
5232
|
+
function checkCommand(args, deps) {
|
|
5233
|
+
const style = flagBool(args.flags, "no-emoji") ? NO_EMOJI_STYLE : DEFAULT_STYLE;
|
|
5234
|
+
const opts = { now: deps.now, generatedAt: deps.generatedAt };
|
|
5235
|
+
const input = resolveConfigInput(args, deps);
|
|
5236
|
+
if (isInputError(input)) {
|
|
5237
|
+
return { stdout: "", stderr: input.error, exitCode: input.exitCode };
|
|
5238
|
+
}
|
|
5239
|
+
let decisions;
|
|
5240
|
+
try {
|
|
5241
|
+
const looksLikeJson = input.text.trim().startsWith("{");
|
|
5242
|
+
if (input.configPath === "<stdin>" && !looksLikeJson) {
|
|
5243
|
+
const { parsed } = parseSnippet(input.text);
|
|
5244
|
+
decisions = checkParsed(parsed, "stdin:snippet", "remote", opts);
|
|
5245
|
+
} else {
|
|
5246
|
+
const loaded = loadSurfaceText(input.text, input.configPath);
|
|
5247
|
+
decisions = checkParsed(loaded.parsed, input.configPath, loaded.origin, opts);
|
|
5248
|
+
}
|
|
5249
|
+
} catch (e) {
|
|
5250
|
+
if (e instanceof ConfigParseError) {
|
|
5251
|
+
return { stdout: "", stderr: `Parse error: ${e.message}`, exitCode: EXIT.ERROR };
|
|
4042
5252
|
}
|
|
4043
5253
|
if (e instanceof Error) {
|
|
4044
5254
|
return { stdout: "", stderr: e.message, exitCode: EXIT.UNKNOWN };
|
|
@@ -4106,7 +5316,7 @@ function worstExit2(decisions) {
|
|
|
4106
5316
|
|
|
4107
5317
|
// src/commands/genRule.ts
|
|
4108
5318
|
import { writeFileSync as writeFileSync4, mkdirSync as mkdirSync3 } from "node:fs";
|
|
4109
|
-
import { dirname as dirname3, join as
|
|
5319
|
+
import { dirname as dirname3, join as join12, resolve as resolve4 } from "node:path";
|
|
4110
5320
|
function isRuleHost(v) {
|
|
4111
5321
|
return v !== void 0 && RULE_HOSTS.includes(v);
|
|
4112
5322
|
}
|
|
@@ -4137,7 +5347,7 @@ Run \`calllint gen-rule\` to list hosts.`,
|
|
|
4137
5347
|
return { stdout: content, exitCode: EXIT.OK };
|
|
4138
5348
|
}
|
|
4139
5349
|
const rel = flagStr(args.flags, "out") ?? RULE_TARGETS[host].path;
|
|
4140
|
-
const abs =
|
|
5350
|
+
const abs = resolve4(deps.cwd, rel);
|
|
4141
5351
|
const write = deps.writeFile ?? defaultWrite;
|
|
4142
5352
|
try {
|
|
4143
5353
|
write(abs, content);
|
|
@@ -4203,7 +5413,7 @@ function diagnosticsCommand(args, deps) {
|
|
|
4203
5413
|
}
|
|
4204
5414
|
|
|
4205
5415
|
// src/commands/explain.ts
|
|
4206
|
-
import { join as
|
|
5416
|
+
import { join as join13 } from "node:path";
|
|
4207
5417
|
function explainCommand(args, deps) {
|
|
4208
5418
|
const serverName = args.positionals[0];
|
|
4209
5419
|
if (!serverName) {
|
|
@@ -4213,7 +5423,7 @@ function explainCommand(args, deps) {
|
|
|
4213
5423
|
exitCode: EXIT.USAGE
|
|
4214
5424
|
};
|
|
4215
5425
|
}
|
|
4216
|
-
const summary = readCache(
|
|
5426
|
+
const summary = readCache(join13(deps.cwd, ".calllint", "last-scan.json"));
|
|
4217
5427
|
if (!summary) {
|
|
4218
5428
|
return {
|
|
4219
5429
|
stdout: "",
|
|
@@ -4235,13 +5445,13 @@ function explainCommand(args, deps) {
|
|
|
4235
5445
|
}
|
|
4236
5446
|
|
|
4237
5447
|
// src/commands/policy.ts
|
|
4238
|
-
import { existsSync as
|
|
4239
|
-
import { join as
|
|
5448
|
+
import { existsSync as existsSync9, writeFileSync as writeFileSync5 } from "node:fs";
|
|
5449
|
+
import { join as join14 } from "node:path";
|
|
4240
5450
|
function policyCommand(args, deps) {
|
|
4241
5451
|
const sub = args.positionals[0];
|
|
4242
5452
|
if (sub === "init") {
|
|
4243
|
-
const path =
|
|
4244
|
-
if (
|
|
5453
|
+
const path = join14(deps.cwd, "calllint.policy.json");
|
|
5454
|
+
if (existsSync9(path) && !flagBool(args.flags, "force")) {
|
|
4245
5455
|
return {
|
|
4246
5456
|
stdout: "",
|
|
4247
5457
|
stderr: `${path} already exists. Use --force to overwrite.`,
|
|
@@ -4271,7 +5481,7 @@ function policyCommand(args, deps) {
|
|
|
4271
5481
|
}
|
|
4272
5482
|
|
|
4273
5483
|
// src/commands/verify.ts
|
|
4274
|
-
import { join as
|
|
5484
|
+
import { join as join15 } from "node:path";
|
|
4275
5485
|
function loadPolicy(args) {
|
|
4276
5486
|
try {
|
|
4277
5487
|
return loadPolicyOrDefault(flagStr(args.flags, "policy"));
|
|
@@ -4280,7 +5490,7 @@ function loadPolicy(args) {
|
|
|
4280
5490
|
}
|
|
4281
5491
|
}
|
|
4282
5492
|
function baselinePathFor(args, cwd) {
|
|
4283
|
-
return flagStr(args.flags, "baseline") ??
|
|
5493
|
+
return flagStr(args.flags, "baseline") ?? join15(cwd, ".calllint", "baseline.json");
|
|
4284
5494
|
}
|
|
4285
5495
|
function baselineCommand(args, deps) {
|
|
4286
5496
|
const policy = loadPolicy(args);
|
|
@@ -4406,8 +5616,8 @@ function approveCommand(args, deps) {
|
|
|
4406
5616
|
}
|
|
4407
5617
|
|
|
4408
5618
|
// src/commands/receipt.ts
|
|
4409
|
-
import { readFileSync as
|
|
4410
|
-
import { resolve as
|
|
5619
|
+
import { readFileSync as readFileSync13, writeFileSync as writeFileSync6 } from "node:fs";
|
|
5620
|
+
import { resolve as resolve5 } from "node:path";
|
|
4411
5621
|
|
|
4412
5622
|
// ../../packages/signature/src/ed25519.ts
|
|
4413
5623
|
import { createHash as createHash2 } from "node:crypto";
|
|
@@ -5016,7 +6226,7 @@ function receiptVerify(args, deps) {
|
|
|
5016
6226
|
}
|
|
5017
6227
|
let raw;
|
|
5018
6228
|
try {
|
|
5019
|
-
raw =
|
|
6229
|
+
raw = readFileSync13(resolve5(deps.cwd, file), "utf8");
|
|
5020
6230
|
} catch (e) {
|
|
5021
6231
|
return {
|
|
5022
6232
|
stdout: "",
|
|
@@ -5069,6 +6279,43 @@ function receiptVerify(args, deps) {
|
|
|
5069
6279
|
exitCode: EXIT.OK
|
|
5070
6280
|
};
|
|
5071
6281
|
}
|
|
6282
|
+
if (flagBool(args.flags, "verbose")) {
|
|
6283
|
+
const stdout2 = [
|
|
6284
|
+
"\u2713 CallLint receipt: valid",
|
|
6285
|
+
"",
|
|
6286
|
+
"Receipt Information:",
|
|
6287
|
+
` ID: ${receipt.receipt_id}`,
|
|
6288
|
+
` Schema: ${receipt.schema_version}`,
|
|
6289
|
+
` Created: ${receipt.created_at}`,
|
|
6290
|
+
"",
|
|
6291
|
+
"Tool:",
|
|
6292
|
+
` Name: ${receipt.tool.name}`,
|
|
6293
|
+
` Version: ${receipt.tool.version}`,
|
|
6294
|
+
"",
|
|
6295
|
+
"Subject:",
|
|
6296
|
+
` Type: ${receipt.subject.type}`,
|
|
6297
|
+
` Target: ${receipt.subject.target}`,
|
|
6298
|
+
"",
|
|
6299
|
+
`Verdict: ${receipt.verdict}`,
|
|
6300
|
+
"",
|
|
6301
|
+
"Hashes:",
|
|
6302
|
+
` Input: ${receipt.hashes.input_hash}`,
|
|
6303
|
+
` Policy: ${receipt.hashes.policy_hash}`,
|
|
6304
|
+
` Report: ${receipt.hashes.report_hash}`,
|
|
6305
|
+
` Ruleset: ${receipt.hashes.ruleset_hash}`,
|
|
6306
|
+
"",
|
|
6307
|
+
"Risk Summary:",
|
|
6308
|
+
` BLOCK: ${receipt.risk_counts.block ?? 0}`,
|
|
6309
|
+
` REVIEW: ${receipt.risk_counts.review ?? 0}`,
|
|
6310
|
+
` UNKNOWN: ${receipt.risk_counts.unknown ?? 0}`,
|
|
6311
|
+
` SAFE: ${receipt.risk_counts.safe ?? 0}`,
|
|
6312
|
+
"",
|
|
6313
|
+
`Network: ${receipt.trust_boundaries.network_used ? "Used" : "Not used"}`,
|
|
6314
|
+
"",
|
|
6315
|
+
"Signature: unsigned local receipt"
|
|
6316
|
+
].join("\n");
|
|
6317
|
+
return { stdout: stdout2, exitCode: EXIT.OK };
|
|
6318
|
+
}
|
|
5072
6319
|
const stdout = [
|
|
5073
6320
|
"\u2713 CallLint receipt: valid",
|
|
5074
6321
|
` Receipt ID: ${receipt.receipt_id}`,
|
|
@@ -5098,7 +6345,7 @@ function verifySignature(receipt, args, deps) {
|
|
|
5098
6345
|
}
|
|
5099
6346
|
let publicKey;
|
|
5100
6347
|
try {
|
|
5101
|
-
const keyJson = JSON.parse(
|
|
6348
|
+
const keyJson = JSON.parse(readFileSync13(resolve5(deps.cwd, publicKeyFlag), "utf8"));
|
|
5102
6349
|
publicKey = keyJson.public_key;
|
|
5103
6350
|
if (typeof publicKey !== "string") {
|
|
5104
6351
|
throw new Error("key file has no string 'public_key' field");
|
|
@@ -5143,6 +6390,47 @@ function verifySignature(receipt, args, deps) {
|
|
|
5143
6390
|
];
|
|
5144
6391
|
return { stdout: "", stderr: lines.join("\n"), exitCode: 1 };
|
|
5145
6392
|
}
|
|
6393
|
+
if (flagBool(args.flags, "verbose")) {
|
|
6394
|
+
const stdout2 = [
|
|
6395
|
+
"\u2713 CallLint receipt: valid",
|
|
6396
|
+
"",
|
|
6397
|
+
"Receipt Information:",
|
|
6398
|
+
` ID: ${receipt.receipt_id}`,
|
|
6399
|
+
` Schema: ${receipt.schema_version}`,
|
|
6400
|
+
` Created: ${receipt.created_at}`,
|
|
6401
|
+
"",
|
|
6402
|
+
"Tool:",
|
|
6403
|
+
` Name: ${receipt.tool.name}`,
|
|
6404
|
+
` Version: ${receipt.tool.version}`,
|
|
6405
|
+
"",
|
|
6406
|
+
"Subject:",
|
|
6407
|
+
` Type: ${receipt.subject.type}`,
|
|
6408
|
+
` Target: ${receipt.subject.target}`,
|
|
6409
|
+
"",
|
|
6410
|
+
`Verdict: ${receipt.verdict}`,
|
|
6411
|
+
"",
|
|
6412
|
+
"Hashes:",
|
|
6413
|
+
` Input: ${receipt.hashes.input_hash}`,
|
|
6414
|
+
` Policy: ${receipt.hashes.policy_hash}`,
|
|
6415
|
+
` Report: ${receipt.hashes.report_hash}`,
|
|
6416
|
+
` Ruleset: ${receipt.hashes.ruleset_hash}`,
|
|
6417
|
+
"",
|
|
6418
|
+
"Risk Summary:",
|
|
6419
|
+
` BLOCK: ${receipt.risk_counts.block ?? 0}`,
|
|
6420
|
+
` REVIEW: ${receipt.risk_counts.review ?? 0}`,
|
|
6421
|
+
` UNKNOWN: ${receipt.risk_counts.unknown ?? 0}`,
|
|
6422
|
+
` SAFE: ${receipt.risk_counts.safe ?? 0}`,
|
|
6423
|
+
"",
|
|
6424
|
+
`Network: ${receipt.trust_boundaries.network_used ? "Used" : "Not used"}`,
|
|
6425
|
+
"",
|
|
6426
|
+
"Signature:",
|
|
6427
|
+
` Status: valid`,
|
|
6428
|
+
` Key ID: ${sig.key_id}`,
|
|
6429
|
+
` Algorithm: ${sig.algorithm}`,
|
|
6430
|
+
` Signed at: ${sig.signed_at}`
|
|
6431
|
+
].join("\n");
|
|
6432
|
+
return { stdout: stdout2, exitCode: EXIT.OK };
|
|
6433
|
+
}
|
|
5146
6434
|
const stdout = [
|
|
5147
6435
|
"\u2713 CallLint receipt: valid",
|
|
5148
6436
|
` Receipt ID: ${receipt.receipt_id}`,
|
|
@@ -5171,7 +6459,7 @@ function receiptSign(args, deps) {
|
|
|
5171
6459
|
}
|
|
5172
6460
|
let unsignedReceipt;
|
|
5173
6461
|
try {
|
|
5174
|
-
unsignedReceipt = JSON.parse(
|
|
6462
|
+
unsignedReceipt = JSON.parse(readFileSync13(resolve5(deps.cwd, receiptFile), "utf8"));
|
|
5175
6463
|
} catch (e) {
|
|
5176
6464
|
return {
|
|
5177
6465
|
stdout: "",
|
|
@@ -5188,7 +6476,7 @@ function receiptSign(args, deps) {
|
|
|
5188
6476
|
}
|
|
5189
6477
|
let keypair;
|
|
5190
6478
|
try {
|
|
5191
|
-
const keyJson = JSON.parse(
|
|
6479
|
+
const keyJson = JSON.parse(readFileSync13(resolve5(deps.cwd, keyFile), "utf8"));
|
|
5192
6480
|
keypair = importKeypair(keyJson);
|
|
5193
6481
|
} catch (e) {
|
|
5194
6482
|
return {
|
|
@@ -5200,7 +6488,7 @@ function receiptSign(args, deps) {
|
|
|
5200
6488
|
try {
|
|
5201
6489
|
const signature = signReceipt(unsignedReceipt, keypair);
|
|
5202
6490
|
const signedReceipt = { ...unsignedReceipt, signature };
|
|
5203
|
-
writeFileSync6(
|
|
6491
|
+
writeFileSync6(resolve5(deps.cwd, outFile), JSON.stringify(signedReceipt, null, 2) + "\n", "utf8");
|
|
5204
6492
|
const stdout = [
|
|
5205
6493
|
"\u2713 Receipt signed successfully",
|
|
5206
6494
|
` Key ID: ${signature.key_id}`,
|
|
@@ -5235,7 +6523,7 @@ function receiptKeygen(args, deps) {
|
|
|
5235
6523
|
try {
|
|
5236
6524
|
const keypair = generateKeypair(keyId);
|
|
5237
6525
|
const exported = exportKeypair(keypair);
|
|
5238
|
-
writeFileSync6(
|
|
6526
|
+
writeFileSync6(resolve5(deps.cwd, outFile), JSON.stringify(exported, null, 2) + "\n", "utf8");
|
|
5239
6527
|
const stdout = [
|
|
5240
6528
|
"\u2713 Keypair generated successfully",
|
|
5241
6529
|
` Key ID: ${keyId}`,
|
|
@@ -5255,8 +6543,8 @@ function receiptKeygen(args, deps) {
|
|
|
5255
6543
|
}
|
|
5256
6544
|
|
|
5257
6545
|
// src/commands/action.ts
|
|
5258
|
-
import { readFileSync as
|
|
5259
|
-
import { resolve as
|
|
6546
|
+
import { readFileSync as readFileSync14, writeFileSync as writeFileSync7 } from "node:fs";
|
|
6547
|
+
import { resolve as resolve6 } from "node:path";
|
|
5260
6548
|
|
|
5261
6549
|
// ../../packages/action-analyzer/src/types.ts
|
|
5262
6550
|
var KIND_RISK_PROFILES = {
|
|
@@ -5730,8 +7018,8 @@ function actionInspect(args, deps) {
|
|
|
5730
7018
|
};
|
|
5731
7019
|
}
|
|
5732
7020
|
try {
|
|
5733
|
-
const absolutePath =
|
|
5734
|
-
const content =
|
|
7021
|
+
const absolutePath = resolve6(deps.cwd, actionFile);
|
|
7022
|
+
const content = readFileSync14(absolutePath, "utf-8");
|
|
5735
7023
|
const descriptor = JSON.parse(content);
|
|
5736
7024
|
if (descriptor.schema_version !== "calllint.action.v0") {
|
|
5737
7025
|
return {
|
|
@@ -5742,7 +7030,7 @@ Expected: calllint.action.v0`,
|
|
|
5742
7030
|
};
|
|
5743
7031
|
}
|
|
5744
7032
|
const policyPath = args.flags["policy"];
|
|
5745
|
-
const policy = loadPolicyOrDefault(policyPath ?
|
|
7033
|
+
const policy = loadPolicyOrDefault(policyPath ? resolve6(deps.cwd, policyPath) : void 0);
|
|
5746
7034
|
const findings = analyzeAction(descriptor);
|
|
5747
7035
|
const verdict = computeActionVerdict(findings, policy);
|
|
5748
7036
|
const actionReport = {
|
|
@@ -5869,7 +7157,7 @@ function writeActionReceipt(descriptor, rawContent, actionReport, policy, args,
|
|
|
5869
7157
|
},
|
|
5870
7158
|
deps.generatedAt || (/* @__PURE__ */ new Date()).toISOString()
|
|
5871
7159
|
);
|
|
5872
|
-
const outPath =
|
|
7160
|
+
const outPath = resolve6(
|
|
5873
7161
|
deps.cwd,
|
|
5874
7162
|
args.flags["receipt-out"] ?? "calllint-action-receipt.json"
|
|
5875
7163
|
);
|
|
@@ -5882,8 +7170,8 @@ function writeActionReceipt(descriptor, rawContent, actionReport, policy, args,
|
|
|
5882
7170
|
}
|
|
5883
7171
|
|
|
5884
7172
|
// src/commands/inbox.ts
|
|
5885
|
-
import { readFileSync as
|
|
5886
|
-
import { resolve as
|
|
7173
|
+
import { readFileSync as readFileSync15, writeFileSync as writeFileSync8 } from "node:fs";
|
|
7174
|
+
import { resolve as resolve7 } from "node:path";
|
|
5887
7175
|
function inboxCommand(args, deps) {
|
|
5888
7176
|
const subcommand = args.positionals[0];
|
|
5889
7177
|
if (!subcommand || subcommand === "help") {
|
|
@@ -5913,8 +7201,8 @@ function inboxInspect(args, deps) {
|
|
|
5913
7201
|
};
|
|
5914
7202
|
}
|
|
5915
7203
|
try {
|
|
5916
|
-
const absolutePath =
|
|
5917
|
-
const content =
|
|
7204
|
+
const absolutePath = resolve7(deps.cwd, eventFile);
|
|
7205
|
+
const content = readFileSync15(absolutePath, "utf-8");
|
|
5918
7206
|
const event = JSON.parse(content);
|
|
5919
7207
|
if (event.schema_version !== "calllint.agent-inbox-event.v0") {
|
|
5920
7208
|
return {
|
|
@@ -5932,7 +7220,7 @@ Expected: calllint.agent-inbox-event.v0`,
|
|
|
5932
7220
|
};
|
|
5933
7221
|
}
|
|
5934
7222
|
const policyPath = args.flags["policy"];
|
|
5935
|
-
const policy = loadPolicyOrDefault(policyPath ?
|
|
7223
|
+
const policy = loadPolicyOrDefault(policyPath ? resolve7(deps.cwd, policyPath) : void 0);
|
|
5936
7224
|
const actionCandidate = event.action_candidate;
|
|
5937
7225
|
let verdict;
|
|
5938
7226
|
let findings;
|
|
@@ -6080,7 +7368,7 @@ function writeInboxReceipt(event, rawContent, verdict, findings, policy, args, d
|
|
|
6080
7368
|
deps.generatedAt || (/* @__PURE__ */ new Date()).toISOString()
|
|
6081
7369
|
);
|
|
6082
7370
|
const receiptOutPath = args.flags["receipt-out"] || "calllint-inbox-receipt.json";
|
|
6083
|
-
const absoluteReceiptPath =
|
|
7371
|
+
const absoluteReceiptPath = resolve7(deps.cwd, receiptOutPath);
|
|
6084
7372
|
writeFileSync8(absoluteReceiptPath, JSON.stringify(receipt, null, 2) + "\n", "utf-8");
|
|
6085
7373
|
return null;
|
|
6086
7374
|
} catch (error) {
|
|
@@ -6135,141 +7423,1992 @@ SEE ALSO
|
|
|
6135
7423
|
`;
|
|
6136
7424
|
}
|
|
6137
7425
|
|
|
6138
|
-
// src/
|
|
6139
|
-
function
|
|
6140
|
-
|
|
6141
|
-
|
|
6142
|
-
|
|
6143
|
-
return helpCommand();
|
|
6144
|
-
}
|
|
6145
|
-
switch (cmd) {
|
|
6146
|
-
case "check":
|
|
6147
|
-
return checkCommand(args, {
|
|
6148
|
-
cwd: deps.cwd,
|
|
6149
|
-
readStdin: deps.readStdin,
|
|
6150
|
-
now: deps.now,
|
|
6151
|
-
generatedAt: deps.generatedAt
|
|
6152
|
-
});
|
|
6153
|
-
case "scan-all":
|
|
6154
|
-
return scanAllCommand(args, {
|
|
6155
|
-
cwd: deps.cwd,
|
|
6156
|
-
now: deps.now,
|
|
6157
|
-
generatedAt: deps.generatedAt
|
|
6158
|
-
});
|
|
6159
|
-
case "scan":
|
|
6160
|
-
return scanCommand(args, {
|
|
6161
|
-
cwd: deps.cwd,
|
|
6162
|
-
readStdin: deps.readStdin,
|
|
6163
|
-
now: deps.now,
|
|
6164
|
-
generatedAt: deps.generatedAt,
|
|
6165
|
-
writeCacheFile: deps.writeCacheFile,
|
|
6166
|
-
online: deps.online,
|
|
6167
|
-
getChangedFilesDiff: deps.getChangedFilesDiff,
|
|
6168
|
-
toolVersion: deps.toolVersion
|
|
6169
|
-
});
|
|
6170
|
-
case "diagnostics":
|
|
6171
|
-
return diagnosticsCommand(args, {
|
|
6172
|
-
cwd: deps.cwd,
|
|
6173
|
-
readStdin: deps.readStdin,
|
|
6174
|
-
now: deps.now,
|
|
6175
|
-
generatedAt: deps.generatedAt,
|
|
6176
|
-
online: deps.online
|
|
6177
|
-
});
|
|
6178
|
-
case "baseline":
|
|
6179
|
-
return baselineCommand(args, {
|
|
6180
|
-
cwd: deps.cwd,
|
|
6181
|
-
readStdin: deps.readStdin,
|
|
6182
|
-
generatedAt: deps.generatedAt,
|
|
6183
|
-
writeBaselineFile: deps.writeCacheFile,
|
|
6184
|
-
online: deps.online
|
|
6185
|
-
});
|
|
6186
|
-
case "verify":
|
|
6187
|
-
return verifyCommand(args, {
|
|
6188
|
-
cwd: deps.cwd,
|
|
6189
|
-
readStdin: deps.readStdin,
|
|
6190
|
-
now: deps.now,
|
|
6191
|
-
generatedAt: deps.generatedAt,
|
|
6192
|
-
writeBaselineFile: deps.writeCacheFile,
|
|
6193
|
-
online: deps.online
|
|
6194
|
-
});
|
|
6195
|
-
case "approve":
|
|
6196
|
-
return approveCommand(args, {
|
|
6197
|
-
cwd: deps.cwd,
|
|
6198
|
-
now: deps.now,
|
|
6199
|
-
generatedAt: deps.generatedAt,
|
|
6200
|
-
writeFile: deps.writeCacheFile
|
|
6201
|
-
});
|
|
6202
|
-
case "explain":
|
|
6203
|
-
return explainCommand(args, { cwd: deps.cwd });
|
|
6204
|
-
case "receipt":
|
|
6205
|
-
return receiptCommand(args, { cwd: deps.cwd });
|
|
6206
|
-
case "action":
|
|
6207
|
-
return actionCommand(args, { cwd: deps.cwd, toolVersion: deps.toolVersion, generatedAt: deps.generatedAt });
|
|
6208
|
-
case "inbox":
|
|
6209
|
-
return inboxCommand(args, { cwd: deps.cwd, toolVersion: deps.toolVersion, generatedAt: deps.generatedAt });
|
|
6210
|
-
case "gen-rule":
|
|
6211
|
-
return genRuleCommand(args, { cwd: deps.cwd });
|
|
6212
|
-
case "policy":
|
|
6213
|
-
return policyCommand(args, { cwd: deps.cwd });
|
|
6214
|
-
default:
|
|
7426
|
+
// src/commands/inventory.ts
|
|
7427
|
+
function inventoryCommand(args, deps) {
|
|
7428
|
+
try {
|
|
7429
|
+
const result = discoverConfigs({ cwd: deps.cwd });
|
|
7430
|
+
if (args.flags.json) {
|
|
6215
7431
|
return {
|
|
6216
|
-
|
|
6217
|
-
|
|
6218
|
-
|
|
6219
|
-
exitCode: 2
|
|
7432
|
+
exitCode: 0,
|
|
7433
|
+
stdout: JSON.stringify(result, null, 2) + "\n",
|
|
7434
|
+
stderr: ""
|
|
6220
7435
|
};
|
|
7436
|
+
}
|
|
7437
|
+
const lines = [];
|
|
7438
|
+
if (result.discovered.length === 0) {
|
|
7439
|
+
lines.push("No agent configs discovered.");
|
|
7440
|
+
lines.push("");
|
|
7441
|
+
lines.push("Searched agents: Cursor, Claude Code, Claude Desktop");
|
|
7442
|
+
lines.push("To scan a specific config: calllint scan --config <path>");
|
|
7443
|
+
} 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;
|
|
6221
7499
|
}
|
|
6222
7500
|
}
|
|
6223
7501
|
|
|
6224
|
-
//
|
|
6225
|
-
|
|
6226
|
-
|
|
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;
|
|
6227
7516
|
}
|
|
6228
|
-
function
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
|
|
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";
|
|
6234
7537
|
}
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
6238
|
-
}
|
|
6239
|
-
var INSTALL_SCRIPT_KEYS = ["preinstall", "install", "postinstall"];
|
|
6240
|
-
async function fetchNpmFacts(packageSpec, fetchJson) {
|
|
6241
|
-
const { name, version } = parseSpec(packageSpec);
|
|
6242
|
-
const url = `https://registry.npmjs.org/${name.replace(/\//g, "%2f")}`;
|
|
6243
|
-
let doc;
|
|
6244
|
-
try {
|
|
6245
|
-
doc = await fetchJson(url);
|
|
6246
|
-
} catch {
|
|
6247
|
-
return { name, versionExists: false, installScripts: [] };
|
|
7538
|
+
if (raw.partial === true) {
|
|
7539
|
+
degradedReasons.push("SkillSpector reported a partial scan");
|
|
7540
|
+
completenessHint = completenessHint ?? "partial";
|
|
6248
7541
|
}
|
|
6249
|
-
if (
|
|
6250
|
-
|
|
6251
|
-
|
|
6252
|
-
const versions = isRecord3(doc.versions) ? doc.versions : {};
|
|
6253
|
-
const isFloating = !version || version === "latest" || /[\^~><*]/.test(version);
|
|
6254
|
-
const resolvedVersion = isFloating ? latestVersion : version;
|
|
6255
|
-
const versionDoc = resolvedVersion && isRecord3(versions[resolvedVersion]) ? versions[resolvedVersion] : void 0;
|
|
6256
|
-
if (!versionDoc) {
|
|
6257
|
-
return { name, versionExists: false, installScripts: [], latestVersion, resolvedVersion };
|
|
7542
|
+
if (raw.degraded === true) {
|
|
7543
|
+
degradedReasons.push("SkillSpector reported a degraded scan");
|
|
7544
|
+
completenessHint = "degraded";
|
|
6258
7545
|
}
|
|
6259
|
-
const
|
|
6260
|
-
const installScripts = INSTALL_SCRIPT_KEYS.filter(
|
|
6261
|
-
(k) => typeof scripts[k] === "string" && scripts[k].length > 0
|
|
6262
|
-
);
|
|
6263
|
-
const deprecated = typeof versionDoc.deprecated === "string" ? versionDoc.deprecated : void 0;
|
|
6264
|
-
const description = typeof versionDoc.description === "string" ? versionDoc.description : void 0;
|
|
6265
|
-
const readme = typeof doc.readme === "string" ? doc.readme : void 0;
|
|
7546
|
+
const coverage = (arrayOf(raw.categories) ?? arrayOf(raw.coverage) ?? []).map((c) => String(c));
|
|
6266
7547
|
return {
|
|
6267
|
-
|
|
6268
|
-
|
|
6269
|
-
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
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
|
+
]);
|
|
7675
|
+
}
|
|
7676
|
+
} catch (err2) {
|
|
7677
|
+
return failClosed(provider, rawReportDigest, [
|
|
7678
|
+
`adapter error: ${err2.message}`
|
|
7679
|
+
]);
|
|
7680
|
+
}
|
|
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
|
+
}
|
|
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);
|
|
7707
|
+
}
|
|
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
|
+
}
|
|
7727
|
+
|
|
7728
|
+
// src/commands/evidence.ts
|
|
7729
|
+
function evidenceCommand(args, deps) {
|
|
7730
|
+
const subcommand = args.positionals[0];
|
|
7731
|
+
if (!subcommand || subcommand === "help") {
|
|
7732
|
+
return { stdout: evidenceHelp(), stderr: "", exitCode: 0 };
|
|
7733
|
+
}
|
|
7734
|
+
if (subcommand === "import") {
|
|
7735
|
+
return evidenceImport(args, deps);
|
|
7736
|
+
}
|
|
7737
|
+
return {
|
|
7738
|
+
stdout: "",
|
|
7739
|
+
stderr: `Unknown evidence subcommand: ${subcommand}
|
|
7740
|
+
Run \`calllint evidence help\`.`,
|
|
7741
|
+
exitCode: 2
|
|
7742
|
+
};
|
|
7743
|
+
}
|
|
7744
|
+
function evidenceImport(args, deps) {
|
|
7745
|
+
const file = args.positionals[1];
|
|
7746
|
+
if (!file) {
|
|
7747
|
+
return {
|
|
7748
|
+
stdout: "",
|
|
7749
|
+
stderr: "Error: Missing report file\nUsage: calllint evidence import <report.json|.sarif> [--provider <p>] [--format json|sarif]",
|
|
7750
|
+
exitCode: 2
|
|
7751
|
+
};
|
|
7752
|
+
}
|
|
7753
|
+
let rawText;
|
|
7754
|
+
try {
|
|
7755
|
+
rawText = readFileSync16(resolve8(deps.cwd, file), "utf-8");
|
|
7756
|
+
} catch (error) {
|
|
7757
|
+
const err2 = error;
|
|
7758
|
+
const stderr = err2.code === "ENOENT" ? `Error: File not found: ${file}` : `Error: ${err2.message}`;
|
|
7759
|
+
return { stdout: "", stderr, exitCode: 2 };
|
|
7760
|
+
}
|
|
7761
|
+
const provider = args.flags["provider"];
|
|
7762
|
+
const formatFlag = args.flags["format"];
|
|
7763
|
+
const format = formatFlag === "sarif" ? "sarif" : formatFlag === "json" ? "json" : void 0;
|
|
7764
|
+
const envelope = importEvidence(rawText, { provider, format });
|
|
7765
|
+
const exitCode = envelope.completeness === "complete" ? 0 : envelope.completeness === "partial" ? 10 : 20;
|
|
7766
|
+
if (args.flags["json"]) {
|
|
7767
|
+
return { stdout: JSON.stringify(envelope, null, 2), stderr: "", exitCode };
|
|
7768
|
+
}
|
|
7769
|
+
const badge2 = envelope.completeness === "complete" ? "\u2713" : envelope.completeness === "partial" ? "~" : "\u25C7";
|
|
7770
|
+
let stdout = `
|
|
7771
|
+
CallLint evidence import
|
|
7772
|
+
`;
|
|
7773
|
+
stdout += `provider: ${envelope.provider} (${envelope.providerVersion})
|
|
7774
|
+
`;
|
|
7775
|
+
stdout += `scan mode: ${envelope.scanMode}
|
|
7776
|
+
`;
|
|
7777
|
+
stdout += `completeness: ${badge2} ${envelope.completeness}
|
|
7778
|
+
`;
|
|
7779
|
+
stdout += `findings: ${envelope.findings.length} (provider-native, not re-scored)
|
|
7780
|
+
`;
|
|
7781
|
+
stdout += `raw digest: ${envelope.rawReportDigest}
|
|
7782
|
+
`;
|
|
7783
|
+
if (envelope.degradedReasons.length > 0) {
|
|
7784
|
+
stdout += `
|
|
7785
|
+
degraded/failed reasons:
|
|
7786
|
+
`;
|
|
7787
|
+
for (const r of envelope.degradedReasons) stdout += ` \u2022 ${r}
|
|
7788
|
+
`;
|
|
7789
|
+
}
|
|
7790
|
+
stdout += `
|
|
7791
|
+
Note: external evidence is recorded, not converted into a CallLint verdict.
|
|
7792
|
+
`;
|
|
7793
|
+
stdout += `A degraded or failed external scan is never treated as a pass.
|
|
7794
|
+
`;
|
|
7795
|
+
return { stdout, stderr: "", exitCode };
|
|
7796
|
+
}
|
|
7797
|
+
function evidenceHelp() {
|
|
7798
|
+
return `
|
|
7799
|
+
calllint evidence \u2014 Import third-party scanner evidence (no re-scoring)
|
|
7800
|
+
|
|
7801
|
+
USAGE
|
|
7802
|
+
calllint evidence import <report.json|.sarif> [options]
|
|
7803
|
+
|
|
7804
|
+
DESCRIPTION
|
|
7805
|
+
Parse a third-party scanner report (e.g. NVIDIA SkillSpector) into a normalized
|
|
7806
|
+
envelope (calllint.evidence-provider.v0). CallLint records the external evidence
|
|
7807
|
+
with provenance \u2014 provider, pinned version, scan mode, coverage, completeness,
|
|
7808
|
+
raw-report digest \u2014 WITHOUT re-scoring or renaming the provider's own findings.
|
|
7809
|
+
|
|
7810
|
+
Aggregate, don't impersonate: an external SAFE never upgrades a CallLint verdict,
|
|
7811
|
+
and a degraded / failed / malformed scan fails closed (never reads as a pass).
|
|
7812
|
+
|
|
7813
|
+
OPTIONS
|
|
7814
|
+
--provider <name> Force the provider adapter (default: auto-detect). e.g. skillspector
|
|
7815
|
+
--format json|sarif Force the input format (default: auto-detect)
|
|
7816
|
+
--json Emit the raw envelope as JSON
|
|
7817
|
+
|
|
7818
|
+
EXIT CODES
|
|
7819
|
+
0 evidence complete
|
|
7820
|
+
10 evidence partial (review the gaps)
|
|
7821
|
+
20 evidence degraded/failed/malformed (fail-closed \u2014 not a pass)
|
|
7822
|
+
2 usage error / file not found
|
|
7823
|
+
|
|
7824
|
+
EXAMPLES
|
|
7825
|
+
calllint evidence import skillspector-report.json
|
|
7826
|
+
calllint evidence import skillspector.sarif --format sarif --json
|
|
7827
|
+
|
|
7828
|
+
SEE ALSO
|
|
7829
|
+
ADR 0034 \u2014 Evidence Provider Envelope
|
|
7830
|
+
docs/new7-packet-a-evidence.md \u2014 file-level execution plan
|
|
7831
|
+
`;
|
|
7832
|
+
}
|
|
7833
|
+
|
|
7834
|
+
// src/commands/trust.ts
|
|
7835
|
+
import { existsSync as existsSync11, mkdirSync as mkdirSync5, readdirSync as readdirSync2, readFileSync as readFileSync18, statSync as statSync4, writeFileSync as writeFileSync10 } from "node:fs";
|
|
7836
|
+
import { basename as basename4, join as join16, resolve as resolvePath4 } from "node:path";
|
|
7837
|
+
import { homedir, userInfo } from "node:os";
|
|
7838
|
+
|
|
7839
|
+
// ../../packages/install-planner/src/buildPlan.ts
|
|
7840
|
+
function ptr(segment) {
|
|
7841
|
+
return segment.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
7842
|
+
}
|
|
7843
|
+
function buildServerOps(ctx) {
|
|
7844
|
+
const forward = [];
|
|
7845
|
+
const inverse = [];
|
|
7846
|
+
const cfg = ctx.currentConfig ?? null;
|
|
7847
|
+
const hasContainer = !!cfg && typeof cfg === "object" && !!cfg.mcpServers;
|
|
7848
|
+
if (!hasContainer) {
|
|
7849
|
+
forward.push({ op: "add", path: "/mcpServers", value: {} });
|
|
7850
|
+
inverse.unshift({ op: "remove", path: "/mcpServers" });
|
|
7851
|
+
}
|
|
7852
|
+
for (const s of ctx.servers) {
|
|
7853
|
+
const path = `/mcpServers/${ptr(s.name)}`;
|
|
7854
|
+
const prior = hasContainer && cfg?.mcpServers ? cfg.mcpServers[s.name] : void 0;
|
|
7855
|
+
forward.push({ op: "add", path, value: s.entry });
|
|
7856
|
+
if (prior === void 0) {
|
|
7857
|
+
if (hasContainer) inverse.unshift({ op: "remove", path });
|
|
7858
|
+
} else {
|
|
7859
|
+
inverse.unshift({ op: "replace", path, value: prior });
|
|
7860
|
+
}
|
|
7861
|
+
}
|
|
7862
|
+
const op = {
|
|
7863
|
+
type: "json-patch",
|
|
7864
|
+
target: ctx.configPath,
|
|
7865
|
+
preconditionDigest: ctx.configDigest,
|
|
7866
|
+
patch: forward
|
|
7867
|
+
};
|
|
7868
|
+
const rb = {
|
|
7869
|
+
type: "json-patch",
|
|
7870
|
+
target: ctx.configPath,
|
|
7871
|
+
preconditionDigest: ctx.configDigest,
|
|
7872
|
+
patch: inverse
|
|
7873
|
+
};
|
|
7874
|
+
return { operations: [op], rollback: inverse.length > 0 ? [rb] : [] };
|
|
7875
|
+
}
|
|
7876
|
+
function buildInstallPlan(ctx, upstream) {
|
|
7877
|
+
const { operations, rollback } = buildServerOps(ctx);
|
|
7878
|
+
const idempotencyKey = hashJson({
|
|
7879
|
+
host: ctx.host,
|
|
7880
|
+
operations
|
|
7881
|
+
});
|
|
7882
|
+
const planId = hashJson({
|
|
7883
|
+
artifactDigest: upstream.artifactDigest,
|
|
7884
|
+
authorityDigest: upstream.authority.digest,
|
|
7885
|
+
decisionDigest: upstream.decision.digest,
|
|
7886
|
+
host: ctx.host,
|
|
7887
|
+
operations
|
|
7888
|
+
}).slice("sha256:".length, "sha256:".length + 16);
|
|
7889
|
+
const sealed = {
|
|
7890
|
+
schema: "calllint.install-plan.v1",
|
|
7891
|
+
planId,
|
|
7892
|
+
artifactDigest: upstream.artifactDigest,
|
|
7893
|
+
authorityDigest: upstream.authority.digest,
|
|
7894
|
+
decisionDigest: upstream.decision.digest,
|
|
7895
|
+
policyDigest: upstream.decision.policyDigest,
|
|
7896
|
+
host: ctx.host,
|
|
7897
|
+
tier: ctx.tier,
|
|
7898
|
+
operations,
|
|
7899
|
+
rollback,
|
|
7900
|
+
backup: { path: ctx.backupPath },
|
|
7901
|
+
idempotencyKey,
|
|
7902
|
+
expiresAt: ctx.expiresAt
|
|
7903
|
+
};
|
|
7904
|
+
return { ...sealed, planDigest: hashJson(sealed) };
|
|
7905
|
+
}
|
|
7906
|
+
function verifyPlanDigest(plan) {
|
|
7907
|
+
const { planDigest, ...rest } = plan;
|
|
7908
|
+
return planDigest === hashJson(rest);
|
|
7909
|
+
}
|
|
7910
|
+
|
|
7911
|
+
// ../../packages/install-planner/src/validate.ts
|
|
7912
|
+
function validatePlan(plan) {
|
|
7913
|
+
const errors = [];
|
|
7914
|
+
if (plan.schema !== "calllint.install-plan.v1") {
|
|
7915
|
+
errors.push(`unexpected schema: ${String(plan.schema)}`);
|
|
7916
|
+
}
|
|
7917
|
+
if (!verifyPlanDigest(plan)) {
|
|
7918
|
+
errors.push("planDigest does not match plan contents (tampered or stale)");
|
|
7919
|
+
}
|
|
7920
|
+
if (plan.operations.length === 0) {
|
|
7921
|
+
errors.push("plan has no operations (nothing to install)");
|
|
7922
|
+
}
|
|
7923
|
+
for (const op of [...plan.operations, ...plan.rollback]) {
|
|
7924
|
+
if (op.type !== "json-patch") {
|
|
7925
|
+
errors.push(`operation is not json-patch: ${String(op.type)}`);
|
|
7926
|
+
}
|
|
7927
|
+
if (!op.preconditionDigest) {
|
|
7928
|
+
errors.push(`operation on ${op.target} has no preconditionDigest`);
|
|
7929
|
+
}
|
|
7930
|
+
for (const p of op.patch) {
|
|
7931
|
+
if (!p.path.startsWith("/")) {
|
|
7932
|
+
errors.push(`patch path must be an absolute JSON-Pointer: ${p.path}`);
|
|
7933
|
+
}
|
|
7934
|
+
}
|
|
7935
|
+
}
|
|
7936
|
+
return { ok: errors.length === 0, errors };
|
|
7937
|
+
}
|
|
7938
|
+
|
|
7939
|
+
// ../../packages/install-planner/src/jsonPatch.ts
|
|
7940
|
+
var JsonPatchError = class extends Error {
|
|
7941
|
+
constructor(message) {
|
|
7942
|
+
super(message);
|
|
7943
|
+
this.name = "JsonPatchError";
|
|
7944
|
+
}
|
|
7945
|
+
};
|
|
7946
|
+
function clone(v) {
|
|
7947
|
+
return v === void 0 ? v : JSON.parse(JSON.stringify(v));
|
|
7948
|
+
}
|
|
7949
|
+
function parsePointer(pointer) {
|
|
7950
|
+
if (pointer === "") return [];
|
|
7951
|
+
if (!pointer.startsWith("/")) throw new JsonPatchError(`invalid JSON-Pointer: ${pointer}`);
|
|
7952
|
+
return pointer.slice(1).split("/").map((t) => t.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
7953
|
+
}
|
|
7954
|
+
function isContainer(v) {
|
|
7955
|
+
return v !== null && typeof v === "object";
|
|
7956
|
+
}
|
|
7957
|
+
function resolveParent(doc, tokens) {
|
|
7958
|
+
let node = doc;
|
|
7959
|
+
for (let i = 0; i < tokens.length - 1; i++) {
|
|
7960
|
+
if (!isContainer(node)) throw new JsonPatchError(`path traverses a non-container at "${tokens[i]}"`);
|
|
7961
|
+
node = Array.isArray(node) ? node[Number(tokens[i])] : node[tokens[i]];
|
|
7962
|
+
}
|
|
7963
|
+
if (!isContainer(node)) throw new JsonPatchError("path parent is not a container");
|
|
7964
|
+
return { parent: node, key: tokens[tokens.length - 1] };
|
|
7965
|
+
}
|
|
7966
|
+
function getAt(doc, tokens) {
|
|
7967
|
+
let node = doc;
|
|
7968
|
+
for (const t of tokens) {
|
|
7969
|
+
if (!isContainer(node)) throw new JsonPatchError(`cannot read "${t}" of a non-container`);
|
|
7970
|
+
node = Array.isArray(node) ? node[t === "-" ? node.length : Number(t)] : node[t];
|
|
7971
|
+
}
|
|
7972
|
+
return node;
|
|
7973
|
+
}
|
|
7974
|
+
function addAt(parent, key, value) {
|
|
7975
|
+
if (Array.isArray(parent)) {
|
|
7976
|
+
const idx = key === "-" ? parent.length : Number(key);
|
|
7977
|
+
if (Number.isNaN(idx) || idx < 0 || idx > parent.length) throw new JsonPatchError(`array index out of range: ${key}`);
|
|
7978
|
+
parent.splice(idx, 0, value);
|
|
7979
|
+
} else {
|
|
7980
|
+
parent[key] = value;
|
|
7981
|
+
}
|
|
7982
|
+
}
|
|
7983
|
+
function removeAt(parent, key) {
|
|
7984
|
+
if (Array.isArray(parent)) {
|
|
7985
|
+
const idx = Number(key);
|
|
7986
|
+
if (Number.isNaN(idx) || idx < 0 || idx >= parent.length) throw new JsonPatchError(`array index out of range: ${key}`);
|
|
7987
|
+
parent.splice(idx, 1);
|
|
7988
|
+
} else {
|
|
7989
|
+
if (!(key in parent)) throw new JsonPatchError(`cannot remove missing key: ${key}`);
|
|
7990
|
+
delete parent[key];
|
|
7991
|
+
}
|
|
7992
|
+
}
|
|
7993
|
+
function applyJsonPatch(doc, patch) {
|
|
7994
|
+
let result = clone(doc);
|
|
7995
|
+
for (const op of patch) {
|
|
7996
|
+
const tokens = parsePointer(op.path);
|
|
7997
|
+
if (op.op === "test") {
|
|
7998
|
+
const actual = getAt(result, tokens);
|
|
7999
|
+
if (JSON.stringify(actual) !== JSON.stringify(op.value)) {
|
|
8000
|
+
throw new JsonPatchError(`test failed at ${op.path}`);
|
|
8001
|
+
}
|
|
8002
|
+
continue;
|
|
8003
|
+
}
|
|
8004
|
+
if (tokens.length === 0) {
|
|
8005
|
+
if (op.op === "add" || op.op === "replace") {
|
|
8006
|
+
result = clone(op.value);
|
|
8007
|
+
continue;
|
|
8008
|
+
}
|
|
8009
|
+
throw new JsonPatchError(`op "${op.op}" not allowed on document root`);
|
|
8010
|
+
}
|
|
8011
|
+
switch (op.op) {
|
|
8012
|
+
case "add": {
|
|
8013
|
+
const { parent, key } = resolveParent(result, tokens);
|
|
8014
|
+
addAt(parent, key, clone(op.value));
|
|
8015
|
+
break;
|
|
8016
|
+
}
|
|
8017
|
+
case "replace": {
|
|
8018
|
+
const { parent, key } = resolveParent(result, tokens);
|
|
8019
|
+
if (Array.isArray(parent)) {
|
|
8020
|
+
const idx = Number(key);
|
|
8021
|
+
if (Number.isNaN(idx) || idx < 0 || idx >= parent.length) throw new JsonPatchError(`replace index out of range: ${key}`);
|
|
8022
|
+
parent[idx] = clone(op.value);
|
|
8023
|
+
} else {
|
|
8024
|
+
if (!(key in parent)) throw new JsonPatchError(`cannot replace missing key: ${key}`);
|
|
8025
|
+
parent[key] = clone(op.value);
|
|
8026
|
+
}
|
|
8027
|
+
break;
|
|
8028
|
+
}
|
|
8029
|
+
case "remove": {
|
|
8030
|
+
const { parent, key } = resolveParent(result, tokens);
|
|
8031
|
+
removeAt(parent, key);
|
|
8032
|
+
break;
|
|
8033
|
+
}
|
|
8034
|
+
case "move":
|
|
8035
|
+
case "copy": {
|
|
8036
|
+
if (op.from === void 0) throw new JsonPatchError(`"${op.op}" requires "from"`);
|
|
8037
|
+
const fromTokens = parsePointer(op.from);
|
|
8038
|
+
const moved = clone(getAt(result, fromTokens));
|
|
8039
|
+
if (moved === void 0) throw new JsonPatchError(`"${op.op}" source missing: ${op.from}`);
|
|
8040
|
+
if (op.op === "move") {
|
|
8041
|
+
const src = resolveParent(result, fromTokens);
|
|
8042
|
+
removeAt(src.parent, src.key);
|
|
8043
|
+
}
|
|
8044
|
+
const { parent, key } = resolveParent(result, tokens);
|
|
8045
|
+
addAt(parent, key, moved);
|
|
8046
|
+
break;
|
|
8047
|
+
}
|
|
8048
|
+
default:
|
|
8049
|
+
throw new JsonPatchError(`unsupported op: ${op.op}`);
|
|
8050
|
+
}
|
|
8051
|
+
}
|
|
8052
|
+
return result;
|
|
8053
|
+
}
|
|
8054
|
+
|
|
8055
|
+
// ../../packages/install-planner/src/applyEngine.ts
|
|
8056
|
+
function digestBytes(bytes) {
|
|
8057
|
+
return hashJson(bytes);
|
|
8058
|
+
}
|
|
8059
|
+
function detectIndent(bytes) {
|
|
8060
|
+
const m = bytes.match(/\n([ \t]+)"/);
|
|
8061
|
+
if (!m) return 2;
|
|
8062
|
+
return m[1].includes(" ") ? " " : m[1].length;
|
|
8063
|
+
}
|
|
8064
|
+
function serializeLike(original, value) {
|
|
8065
|
+
const indent = original === null ? 2 : detectIndent(original);
|
|
8066
|
+
const body = JSON.stringify(value, null, indent);
|
|
8067
|
+
const trailing = original === null || original.endsWith("\n") ? "\n" : "";
|
|
8068
|
+
return body + trailing;
|
|
8069
|
+
}
|
|
8070
|
+
function sameConfig(a, b) {
|
|
8071
|
+
return stableStringify(a) === stableStringify(b);
|
|
8072
|
+
}
|
|
8073
|
+
function mk(plan, opts, state, outcome, before, extra, notes) {
|
|
8074
|
+
return {
|
|
8075
|
+
schema: APPLY_RESULT_SCHEMA,
|
|
8076
|
+
state,
|
|
8077
|
+
outcome,
|
|
8078
|
+
planId: plan.planId,
|
|
8079
|
+
planDigest: plan.planDigest,
|
|
8080
|
+
host: plan.host,
|
|
8081
|
+
configPath: opts.configPath,
|
|
8082
|
+
configDigestBefore: before,
|
|
8083
|
+
configDigestAfter: null,
|
|
8084
|
+
backupPath: null,
|
|
8085
|
+
rolledBack: false,
|
|
8086
|
+
notes,
|
|
8087
|
+
appliedAt: opts.now,
|
|
8088
|
+
...extra
|
|
8089
|
+
};
|
|
8090
|
+
}
|
|
8091
|
+
function applyPlan(opts) {
|
|
8092
|
+
const { plan, fs } = opts;
|
|
8093
|
+
const notes = [];
|
|
8094
|
+
const v = validatePlan(plan);
|
|
8095
|
+
if (!v.ok) {
|
|
8096
|
+
return mk(plan, opts, "PLAN_STALE", "stale", "absent", {}, [...notes, ...v.errors.map((e) => `invalid plan: ${e}`)]);
|
|
8097
|
+
}
|
|
8098
|
+
if (!verifyPlanDigest(plan)) {
|
|
8099
|
+
return mk(plan, opts, "PLAN_STALE", "stale", "absent", {}, [...notes, "plan digest does not seal its contents \u2014 tampered"]);
|
|
8100
|
+
}
|
|
8101
|
+
if (opts.approvalDigest !== plan.planDigest) {
|
|
8102
|
+
return mk(plan, opts, "PLAN_STALE", "stale", "absent", {}, [
|
|
8103
|
+
...notes,
|
|
8104
|
+
`approval digest does not match plan digest (approved ${opts.approvalDigest.slice(0, 16)}\u2026, plan ${plan.planDigest.slice(0, 16)}\u2026)`
|
|
8105
|
+
]);
|
|
8106
|
+
}
|
|
8107
|
+
if (opts.now > plan.expiresAt) {
|
|
8108
|
+
return mk(plan, opts, "PLAN_STALE", "stale", "absent", {}, [...notes, `plan expired at ${plan.expiresAt} (now ${opts.now})`]);
|
|
8109
|
+
}
|
|
8110
|
+
if (plan.tier !== "A") {
|
|
8111
|
+
return mk(plan, opts, "PLAN_STALE", "stale", "absent", {}, [...notes, `host "${plan.host}" is tier ${plan.tier} \u2014 not approved for apply`]);
|
|
8112
|
+
}
|
|
8113
|
+
const op = plan.operations[0];
|
|
8114
|
+
if (!op) {
|
|
8115
|
+
return mk(plan, opts, "VERIFIED", "already_applied", "absent", { configDigestAfter: null }, [...notes, "plan has no operations \u2014 nothing to apply"]);
|
|
8116
|
+
}
|
|
8117
|
+
const exists = fs.exists(opts.configPath);
|
|
8118
|
+
const currentBytes = exists ? fs.readFile(opts.configPath) : null;
|
|
8119
|
+
const before = currentBytes === null ? "absent" : digestBytes(currentBytes);
|
|
8120
|
+
const currentConfig = currentBytes === null ? null : safeParse(currentBytes);
|
|
8121
|
+
if (currentBytes !== null && currentConfig === PARSE_FAILED) {
|
|
8122
|
+
return mk(plan, opts, "APPLY_CONFLICT", "conflict", before, {}, [...notes, "current config is not valid JSON \u2014 refusing to overwrite"]);
|
|
8123
|
+
}
|
|
8124
|
+
let next;
|
|
8125
|
+
try {
|
|
8126
|
+
next = applyJsonPatch(currentConfig ?? {}, op.patch);
|
|
8127
|
+
} catch (e) {
|
|
8128
|
+
const msg = e instanceof JsonPatchError ? e.message : String(e);
|
|
8129
|
+
return mk(plan, opts, "APPLY_CONFLICT", "conflict", before, {}, [...notes, `patch does not apply to current config: ${msg}`]);
|
|
8130
|
+
}
|
|
8131
|
+
if (currentConfig !== null && sameConfig(next, currentConfig)) {
|
|
8132
|
+
notes.push("plan already in effect \u2014 no change needed (idempotent)");
|
|
8133
|
+
return mk(plan, opts, "VERIFIED", "already_applied", before, { configDigestAfter: before === "absent" ? null : before }, notes);
|
|
8134
|
+
}
|
|
8135
|
+
if (op.preconditionDigest !== before) {
|
|
8136
|
+
return mk(plan, opts, "APPLY_CONFLICT", "conflict", before, {}, [
|
|
8137
|
+
...notes,
|
|
8138
|
+
`target config changed since planning (precondition ${op.preconditionDigest.slice(0, 16)}\u2026, now ${before === "absent" ? "absent" : before.slice(0, 16) + "\u2026"}) \u2192 not auto-merging`
|
|
8139
|
+
]);
|
|
8140
|
+
}
|
|
8141
|
+
fs.ensureDir(opts.lockPath);
|
|
8142
|
+
if (!fs.acquireLock(opts.lockPath)) {
|
|
8143
|
+
return mk(plan, opts, "APPLY_CONFLICT", "conflict", before, {}, [...notes, "another apply holds the config lock \u2014 try again"]);
|
|
8144
|
+
}
|
|
8145
|
+
try {
|
|
8146
|
+
return writeVerifyRollback(opts, op.patch, currentBytes, before, next, notes);
|
|
8147
|
+
} finally {
|
|
8148
|
+
fs.remove(opts.lockPath);
|
|
8149
|
+
}
|
|
8150
|
+
}
|
|
8151
|
+
var PARSE_FAILED = Symbol("parse-failed");
|
|
8152
|
+
function safeParse(bytes) {
|
|
8153
|
+
try {
|
|
8154
|
+
return JSON.parse(bytes);
|
|
8155
|
+
} catch {
|
|
8156
|
+
return PARSE_FAILED;
|
|
8157
|
+
}
|
|
8158
|
+
}
|
|
8159
|
+
function writeVerifyRollback(opts, patch, currentBytes, before, next, notes) {
|
|
8160
|
+
const { plan, fs } = opts;
|
|
8161
|
+
const nextBytes = serializeLike(currentBytes, next);
|
|
8162
|
+
const after = digestBytes(nextBytes);
|
|
8163
|
+
let backupPath = null;
|
|
8164
|
+
if (currentBytes !== null) {
|
|
8165
|
+
fs.ensureDir(opts.backupPath);
|
|
8166
|
+
fs.writeFile(opts.backupPath, currentBytes);
|
|
8167
|
+
fs.fsync(opts.backupPath);
|
|
8168
|
+
backupPath = opts.backupPath;
|
|
8169
|
+
notes.push(`backed up original config \u2192 ${opts.backupPath}`);
|
|
8170
|
+
}
|
|
8171
|
+
const tmp = opts.configPath + ".calllint-tmp";
|
|
8172
|
+
fs.ensureDir(opts.configPath);
|
|
8173
|
+
fs.writeFile(tmp, nextBytes);
|
|
8174
|
+
fs.fsync(tmp);
|
|
8175
|
+
fs.rename(tmp, opts.configPath);
|
|
8176
|
+
notes.push("config written atomically (temp \u2192 fsync \u2192 rename)");
|
|
8177
|
+
const verifyOk = verify2(fs, opts.configPath, patch, next);
|
|
8178
|
+
if (verifyOk) {
|
|
8179
|
+
notes.push("post-apply verify OK \u2014 resulting config re-parses and matches the plan");
|
|
8180
|
+
return mk(plan, opts, "VERIFIED", "applied", before, { configDigestAfter: after, backupPath }, notes);
|
|
8181
|
+
}
|
|
8182
|
+
notes.push("post-apply verify FAILED \u2014 attempting rollback to the original config");
|
|
8183
|
+
if (currentBytes === null) {
|
|
8184
|
+
fs.remove(opts.configPath);
|
|
8185
|
+
const restored = !fs.exists(opts.configPath);
|
|
8186
|
+
return mk(plan, opts, restored ? "VERIFICATION_FAILED" : "ROLLBACK_REQUIRED", restored ? "rolled_back" : "rollback_failed", before, { configDigestAfter: null, backupPath, rolledBack: restored }, [
|
|
8187
|
+
...notes,
|
|
8188
|
+
restored ? "rollback OK \u2014 created file removed, original absence restored" : "rollback FAILED \u2014 file still present; manual intervention required"
|
|
8189
|
+
]);
|
|
8190
|
+
}
|
|
8191
|
+
const rtmp = opts.configPath + ".calllint-rollback-tmp";
|
|
8192
|
+
fs.writeFile(rtmp, currentBytes);
|
|
8193
|
+
fs.fsync(rtmp);
|
|
8194
|
+
fs.rename(rtmp, opts.configPath);
|
|
8195
|
+
const restoredDigest = digestBytes(fs.readFile(opts.configPath));
|
|
8196
|
+
const rolledBack = restoredDigest === before;
|
|
8197
|
+
return mk(plan, opts, rolledBack ? "VERIFICATION_FAILED" : "ROLLBACK_REQUIRED", rolledBack ? "rolled_back" : "rollback_failed", before, { configDigestAfter: null, backupPath, rolledBack }, [
|
|
8198
|
+
...notes,
|
|
8199
|
+
rolledBack ? "rollback OK \u2014 original config restored (digest matches)" : "rollback FAILED \u2014 restored digest does not match original; manual intervention required"
|
|
8200
|
+
]);
|
|
8201
|
+
}
|
|
8202
|
+
function verify2(fs, path, patch, expected) {
|
|
8203
|
+
if (!fs.exists(path)) return false;
|
|
8204
|
+
const parsed = safeParse(fs.readFile(path));
|
|
8205
|
+
if (parsed === PARSE_FAILED) return false;
|
|
8206
|
+
if (stableStringify(parsed) !== stableStringify(expected)) return false;
|
|
8207
|
+
return true;
|
|
8208
|
+
}
|
|
8209
|
+
|
|
8210
|
+
// ../../packages/install-planner/src/nodeFsPort.ts
|
|
8211
|
+
import {
|
|
8212
|
+
existsSync as existsSync10,
|
|
8213
|
+
readFileSync as readFileSync17,
|
|
8214
|
+
writeFileSync as writeFileSync9,
|
|
8215
|
+
renameSync,
|
|
8216
|
+
rmSync,
|
|
8217
|
+
mkdirSync as mkdirSync4,
|
|
8218
|
+
openSync,
|
|
8219
|
+
closeSync,
|
|
8220
|
+
fsyncSync
|
|
8221
|
+
} from "node:fs";
|
|
8222
|
+
import { dirname as dirname4 } from "node:path";
|
|
8223
|
+
function nodeFsPort() {
|
|
8224
|
+
return {
|
|
8225
|
+
exists: (p) => existsSync10(p),
|
|
8226
|
+
readFile: (p) => readFileSync17(p, "utf8"),
|
|
8227
|
+
writeFile: (p, data) => writeFileSync9(p, data, "utf8"),
|
|
8228
|
+
fsync: (p) => {
|
|
8229
|
+
try {
|
|
8230
|
+
const fd = openSync(p, "r+");
|
|
8231
|
+
try {
|
|
8232
|
+
fsyncSync(fd);
|
|
8233
|
+
} finally {
|
|
8234
|
+
closeSync(fd);
|
|
8235
|
+
}
|
|
8236
|
+
} catch {
|
|
8237
|
+
}
|
|
8238
|
+
},
|
|
8239
|
+
rename: (from, to) => renameSync(from, to),
|
|
8240
|
+
remove: (p) => rmSync(p, { force: true }),
|
|
8241
|
+
ensureDir: (p) => mkdirSync4(dirname4(p), { recursive: true }),
|
|
8242
|
+
acquireLock: (p) => {
|
|
8243
|
+
try {
|
|
8244
|
+
const fd = openSync(p, "wx");
|
|
8245
|
+
closeSync(fd);
|
|
8246
|
+
return true;
|
|
8247
|
+
} catch {
|
|
8248
|
+
return false;
|
|
8249
|
+
}
|
|
8250
|
+
}
|
|
8251
|
+
};
|
|
8252
|
+
}
|
|
8253
|
+
|
|
8254
|
+
// ../../packages/install-planner/src/pathSafety.ts
|
|
8255
|
+
import { isAbsolute as isAbsolute2, resolve as resolve9 } from "node:path";
|
|
8256
|
+
var PathSafetyError = class extends Error {
|
|
8257
|
+
constructor(message) {
|
|
8258
|
+
super(message);
|
|
8259
|
+
this.name = "PathSafetyError";
|
|
8260
|
+
}
|
|
8261
|
+
};
|
|
8262
|
+
function expandHome(p, home) {
|
|
8263
|
+
if (p.includes("\0")) throw new PathSafetyError("path contains a NUL byte");
|
|
8264
|
+
let expanded = p;
|
|
8265
|
+
if (p === "~") expanded = home;
|
|
8266
|
+
else if (p.startsWith("~/") || p.startsWith("~\\")) expanded = home + p.slice(1);
|
|
8267
|
+
else if (p.startsWith("~")) throw new PathSafetyError(`refusing to expand "~" username form: ${p}`);
|
|
8268
|
+
return expanded;
|
|
8269
|
+
}
|
|
8270
|
+
function safeConfigPath(rawPath, opts) {
|
|
8271
|
+
const expanded = expandHome(rawPath, opts.home);
|
|
8272
|
+
const abs = isAbsolute2(expanded) ? expanded : resolve9(opts.cwd, expanded);
|
|
8273
|
+
if (abs.includes("\0")) throw new PathSafetyError("resolved path contains a NUL byte");
|
|
8274
|
+
if (!isAbsolute2(abs)) throw new PathSafetyError(`could not resolve to an absolute path: ${rawPath}`);
|
|
8275
|
+
return abs;
|
|
8276
|
+
}
|
|
8277
|
+
|
|
8278
|
+
// ../../packages/install-planner/src/decisionReceipt.ts
|
|
8279
|
+
function resultFor(outcome) {
|
|
8280
|
+
if (outcome === "applied" || outcome === "already_applied") return "applied";
|
|
8281
|
+
if (outcome === "rolled_back") return "rolled-back";
|
|
8282
|
+
return "prepared-only";
|
|
8283
|
+
}
|
|
8284
|
+
function deriveReceiptId(installPlanDigest, approvedAt) {
|
|
8285
|
+
const h2 = sha256(`${installPlanDigest}|${approvedAt}`).slice(7);
|
|
8286
|
+
const b64 = Buffer.from(h2.slice(0, 32), "hex").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
8287
|
+
return `clrec_${b64}`;
|
|
8288
|
+
}
|
|
8289
|
+
function sortedUnique(xs) {
|
|
8290
|
+
return [...new Set(xs)].sort();
|
|
8291
|
+
}
|
|
8292
|
+
function buildDecisionReceipt(result, plan, ctx) {
|
|
8293
|
+
return {
|
|
8294
|
+
schema: "calllint.receipt.v1",
|
|
8295
|
+
receiptId: deriveReceiptId(plan.planDigest, ctx.approvedAt),
|
|
8296
|
+
artifactDigest: plan.artifactDigest ?? null,
|
|
8297
|
+
evidenceDigests: sortedUnique(ctx.evidenceDigests ?? []),
|
|
8298
|
+
authorityDigest: plan.authorityDigest,
|
|
8299
|
+
policyDigest: plan.policyDigest,
|
|
8300
|
+
decisionDigest: plan.decisionDigest,
|
|
8301
|
+
installPlanDigest: plan.planDigest,
|
|
8302
|
+
approval: {
|
|
8303
|
+
type: "local-human",
|
|
8304
|
+
approvedAt: ctx.approvedAt,
|
|
8305
|
+
approver: ctx.approver,
|
|
8306
|
+
approvedDigest: plan.planDigest
|
|
8307
|
+
},
|
|
8308
|
+
result: resultFor(result.outcome),
|
|
8309
|
+
host: plan.host,
|
|
8310
|
+
configPath: result.configPath,
|
|
8311
|
+
configDigestBefore: result.configDigestBefore,
|
|
8312
|
+
configDigestAfter: result.configDigestAfter,
|
|
8313
|
+
policyVersion: ctx.policyVersion ?? null,
|
|
8314
|
+
scannerVersion: ctx.scannerVersion,
|
|
8315
|
+
exceptionReason: ctx.exceptionReason ?? null,
|
|
8316
|
+
expiration: plan.expiresAt,
|
|
8317
|
+
supersedes: ctx.supersedes ?? null,
|
|
8318
|
+
revocation: null,
|
|
8319
|
+
signature: null
|
|
8320
|
+
};
|
|
8321
|
+
}
|
|
8322
|
+
function signDecisionReceipt(receipt, keypair) {
|
|
8323
|
+
const { signature: _drop, ...body } = receipt;
|
|
8324
|
+
const sig = signReceipt(body, keypair);
|
|
8325
|
+
return { ...receipt, signature: { ...sig, algorithm: "ed25519" } };
|
|
8326
|
+
}
|
|
8327
|
+
|
|
8328
|
+
// ../../packages/install-planner/src/verifyDecisionReceipt.ts
|
|
8329
|
+
var SHA256 = /^sha256:[0-9a-f]{64}$/;
|
|
8330
|
+
var RECEIPT_ID = /^clrec_[A-Za-z0-9_-]+$/;
|
|
8331
|
+
var RESULTS = /* @__PURE__ */ new Set(["applied", "rolled-back", "prepared-only"]);
|
|
8332
|
+
function isObj(v) {
|
|
8333
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
8334
|
+
}
|
|
8335
|
+
function isSha(v) {
|
|
8336
|
+
return typeof v === "string" && SHA256.test(v);
|
|
8337
|
+
}
|
|
8338
|
+
function isShaOrNull(v) {
|
|
8339
|
+
return v === null || isSha(v);
|
|
8340
|
+
}
|
|
8341
|
+
function verifyDecisionReceipt(input, opts) {
|
|
8342
|
+
const errors = [];
|
|
8343
|
+
const push = (m) => errors.push(m);
|
|
8344
|
+
const fail = (msg) => ({
|
|
8345
|
+
valid: false,
|
|
8346
|
+
errors: [msg],
|
|
8347
|
+
signed: false,
|
|
8348
|
+
expired: false,
|
|
8349
|
+
tampered: false
|
|
8350
|
+
});
|
|
8351
|
+
if (!isObj(input)) return fail("receipt is not a JSON object");
|
|
8352
|
+
const r = input;
|
|
8353
|
+
if (r.schema !== "calllint.receipt.v1") push('schema must be "calllint.receipt.v1"');
|
|
8354
|
+
if (typeof r.receiptId !== "string" || !RECEIPT_ID.test(r.receiptId)) push("receiptId must match /^clrec_/");
|
|
8355
|
+
if (!isShaOrNull(r.artifactDigest)) push("artifactDigest must be sha256 or null");
|
|
8356
|
+
if (!Array.isArray(r.evidenceDigests) || !r.evidenceDigests.every(isSha)) push("evidenceDigests must be an array of sha256");
|
|
8357
|
+
for (const k of ["authorityDigest", "policyDigest", "decisionDigest", "installPlanDigest"]) {
|
|
8358
|
+
if (!isSha(r[k])) push(`${k} must be sha256`);
|
|
8359
|
+
}
|
|
8360
|
+
if (typeof r.result !== "string" || !RESULTS.has(r.result)) push("result must be applied|rolled-back|prepared-only");
|
|
8361
|
+
if (typeof r.host !== "string" || r.host.length === 0) push("host must be a non-empty string");
|
|
8362
|
+
if (typeof r.configPath !== "string" || r.configPath.length === 0) push("configPath must be a non-empty string");
|
|
8363
|
+
if (!(r.configDigestBefore === "absent" || isSha(r.configDigestBefore))) push('configDigestBefore must be sha256 or "absent"');
|
|
8364
|
+
if (!isShaOrNull(r.configDigestAfter)) push("configDigestAfter must be sha256 or null");
|
|
8365
|
+
if (typeof r.scannerVersion !== "string" || r.scannerVersion.length === 0) push("scannerVersion must be a non-empty string");
|
|
8366
|
+
if (typeof r.expiration !== "string" || Number.isNaN(Date.parse(r.expiration))) push("expiration must be ISO-8601");
|
|
8367
|
+
if (!isObj(r.approval)) {
|
|
8368
|
+
push("approval must be an object");
|
|
8369
|
+
} else {
|
|
8370
|
+
const a = r.approval;
|
|
8371
|
+
if (a.type !== "local-human") push('approval.type must be "local-human"');
|
|
8372
|
+
if (typeof a.approvedAt !== "string" || Number.isNaN(Date.parse(a.approvedAt))) push("approval.approvedAt must be ISO-8601");
|
|
8373
|
+
if (!(a.approver === null || typeof a.approver === "string")) push("approval.approver must be string|null");
|
|
8374
|
+
if (!isSha(a.approvedDigest)) push("approval.approvedDigest must be sha256");
|
|
8375
|
+
else if (isSha(r.installPlanDigest) && a.approvedDigest !== r.installPlanDigest) {
|
|
8376
|
+
push("approval.approvedDigest must equal installPlanDigest (approval binding broken)");
|
|
8377
|
+
}
|
|
8378
|
+
}
|
|
8379
|
+
let signed = false;
|
|
8380
|
+
let tampered = false;
|
|
8381
|
+
if (r.signature != null) {
|
|
8382
|
+
signed = true;
|
|
8383
|
+
const sig = r.signature;
|
|
8384
|
+
if (sig.algorithm !== "ed25519" || typeof sig.key_id !== "string" || typeof sig.value !== "string") {
|
|
8385
|
+
push("signature, when present, must have ed25519 algorithm + string key_id + value");
|
|
8386
|
+
} else if (opts.publicKey !== void 0) {
|
|
8387
|
+
const res = verifyReceipt2(r, opts.publicKey);
|
|
8388
|
+
if (!res.valid) {
|
|
8389
|
+
tampered = true;
|
|
8390
|
+
push(`signature verification failed: ${res.error ?? "invalid signature"}`);
|
|
8391
|
+
}
|
|
8392
|
+
}
|
|
8393
|
+
}
|
|
8394
|
+
const expired = typeof r.expiration === "string" && !Number.isNaN(Date.parse(r.expiration)) ? Date.parse(opts.now) > Date.parse(r.expiration) : false;
|
|
8395
|
+
return { valid: errors.length === 0, errors, signed, expired, tampered };
|
|
8396
|
+
}
|
|
8397
|
+
|
|
8398
|
+
// ../../packages/install-planner/src/adapters/claudeCode.ts
|
|
8399
|
+
var CLAUDE_CODE_HOST_ID = "claude-code";
|
|
8400
|
+
var CLAUDE_CODE_TIER = "A";
|
|
8401
|
+
var claudeCodeAdapter = {
|
|
8402
|
+
id: CLAUDE_CODE_HOST_ID,
|
|
8403
|
+
tier: CLAUDE_CODE_TIER,
|
|
8404
|
+
createPlan(ctx, upstream) {
|
|
8405
|
+
return buildInstallPlan({ ...ctx, host: CLAUDE_CODE_HOST_ID, tier: CLAUDE_CODE_TIER }, upstream);
|
|
8406
|
+
},
|
|
8407
|
+
validatePlan(plan) {
|
|
8408
|
+
return validatePlan(plan);
|
|
8409
|
+
},
|
|
8410
|
+
applyPlan(plan, ctx) {
|
|
8411
|
+
return applyPlan({
|
|
8412
|
+
plan,
|
|
8413
|
+
approvalDigest: ctx.approvalDigest,
|
|
8414
|
+
configPath: ctx.configPath,
|
|
8415
|
+
backupPath: ctx.backupPath,
|
|
8416
|
+
lockPath: ctx.lockPath,
|
|
8417
|
+
fs: ctx.fs,
|
|
8418
|
+
now: ctx.now
|
|
8419
|
+
});
|
|
8420
|
+
}
|
|
8421
|
+
};
|
|
8422
|
+
function claudeCodeServerEntry(server) {
|
|
8423
|
+
const entry = {};
|
|
8424
|
+
if (server.url) {
|
|
8425
|
+
entry["url"] = server.url;
|
|
8426
|
+
} else if (server.command) {
|
|
8427
|
+
entry["command"] = server.command;
|
|
8428
|
+
entry["args"] = server.args ?? [];
|
|
8429
|
+
}
|
|
8430
|
+
if (server.envKeys && server.envKeys.length > 0) {
|
|
8431
|
+
const env = {};
|
|
8432
|
+
for (const k of [...server.envKeys].sort()) env[k] = "";
|
|
8433
|
+
entry["env"] = env;
|
|
8434
|
+
}
|
|
8435
|
+
return entry;
|
|
8436
|
+
}
|
|
8437
|
+
|
|
8438
|
+
// ../../packages/install-planner/src/index.ts
|
|
8439
|
+
var HOST_ADAPTERS = {
|
|
8440
|
+
[claudeCodeAdapter.id]: claudeCodeAdapter
|
|
8441
|
+
};
|
|
8442
|
+
function getHostAdapter(host) {
|
|
8443
|
+
return HOST_ADAPTERS[host] ?? null;
|
|
8444
|
+
}
|
|
8445
|
+
|
|
8446
|
+
// src/commands/trust.ts
|
|
8447
|
+
function trustCommand(args, deps) {
|
|
8448
|
+
const subcommand = args.positionals[0];
|
|
8449
|
+
if (!subcommand || subcommand === "help") {
|
|
8450
|
+
return { stdout: trustHelp(), stderr: "", exitCode: EXIT.OK };
|
|
8451
|
+
}
|
|
8452
|
+
if (subcommand === "prepare") return trustPrepare(args, deps);
|
|
8453
|
+
if (subcommand === "show") return trustShow(args, deps);
|
|
8454
|
+
if (subcommand === "explain") return trustExplain(args, deps);
|
|
8455
|
+
if (subcommand === "apply") return trustApply(args, deps);
|
|
8456
|
+
if (subcommand === "verify") return trustVerify(args, deps);
|
|
8457
|
+
return {
|
|
8458
|
+
stdout: "",
|
|
8459
|
+
stderr: `Unknown trust subcommand: ${subcommand}
|
|
8460
|
+
Run \`calllint trust help\`.`,
|
|
8461
|
+
exitCode: EXIT.USAGE
|
|
8462
|
+
};
|
|
8463
|
+
}
|
|
8464
|
+
var MAX_FILE_BYTES2 = 5 * 1024 * 1024;
|
|
8465
|
+
var MAX_DIR_ENTRIES = 2e3;
|
|
8466
|
+
var MAX_DIR_DEPTH = 8;
|
|
8467
|
+
var SKIP_DIRS2 = /* @__PURE__ */ new Set([
|
|
8468
|
+
"node_modules",
|
|
8469
|
+
".git",
|
|
8470
|
+
"dist",
|
|
8471
|
+
"build",
|
|
8472
|
+
".next",
|
|
8473
|
+
"coverage",
|
|
8474
|
+
".turbo"
|
|
8475
|
+
]);
|
|
8476
|
+
function loadArtifactInput(target, deps) {
|
|
8477
|
+
const spec = parseTargetSpec(target);
|
|
8478
|
+
if (spec.kind === "npm") {
|
|
8479
|
+
return {
|
|
8480
|
+
sourceType: "npm",
|
|
8481
|
+
source: target,
|
|
8482
|
+
requestedRef: spec.packageSpec?.includes("@") ? spec.packageSpec.slice(spec.packageSpec.lastIndexOf("@") + 1) : null,
|
|
8483
|
+
resolutionReasons: [
|
|
8484
|
+
"npm target requires network to pin an exact published version (offline default)"
|
|
8485
|
+
],
|
|
8486
|
+
resolvedAt: deps.generatedAt
|
|
8487
|
+
};
|
|
8488
|
+
}
|
|
8489
|
+
if (spec.kind === "github") {
|
|
8490
|
+
return {
|
|
8491
|
+
sourceType: "git",
|
|
8492
|
+
source: target,
|
|
8493
|
+
requestedRef: spec.ref ?? null,
|
|
8494
|
+
resolutionReasons: [
|
|
8495
|
+
"git target requires network to pin an immutable commit (offline default)"
|
|
8496
|
+
],
|
|
8497
|
+
resolvedAt: deps.generatedAt
|
|
8498
|
+
};
|
|
8499
|
+
}
|
|
8500
|
+
const abs = resolvePath4(deps.cwd, target);
|
|
8501
|
+
if (!existsSync11(abs)) {
|
|
8502
|
+
return { error: `Target not found: ${target}`, exitCode: EXIT.USAGE };
|
|
8503
|
+
}
|
|
8504
|
+
let stat;
|
|
8505
|
+
try {
|
|
8506
|
+
stat = statSync4(abs);
|
|
8507
|
+
} catch (err2) {
|
|
8508
|
+
return { error: `Cannot stat target: ${err2.message}`, exitCode: EXIT.USAGE };
|
|
8509
|
+
}
|
|
8510
|
+
if (stat.isDirectory()) {
|
|
8511
|
+
const entries = readDirEntries(abs);
|
|
8512
|
+
return {
|
|
8513
|
+
sourceType: "dir",
|
|
8514
|
+
source: target,
|
|
8515
|
+
requestedRef: null,
|
|
8516
|
+
entries,
|
|
8517
|
+
resolutionReasons: entries.length === 0 ? ["directory has no readable files to digest"] : [],
|
|
8518
|
+
resolvedAt: deps.generatedAt
|
|
8519
|
+
};
|
|
8520
|
+
}
|
|
8521
|
+
const name = basename4(abs).toLowerCase();
|
|
8522
|
+
const sourceType = name.endsWith(".json") && (name.includes("mcp") || name.includes("settings")) ? "mcp-config" : "file";
|
|
8523
|
+
let content;
|
|
8524
|
+
try {
|
|
8525
|
+
content = readFileSync18(abs, "utf8").slice(0, MAX_FILE_BYTES2);
|
|
8526
|
+
} catch (err2) {
|
|
8527
|
+
return { error: `Cannot read target: ${err2.message}`, exitCode: EXIT.USAGE };
|
|
8528
|
+
}
|
|
8529
|
+
return {
|
|
8530
|
+
sourceType,
|
|
8531
|
+
source: target,
|
|
8532
|
+
requestedRef: null,
|
|
8533
|
+
content,
|
|
8534
|
+
resolvedAt: deps.generatedAt
|
|
8535
|
+
};
|
|
8536
|
+
}
|
|
8537
|
+
function readDirEntries(absDir) {
|
|
8538
|
+
const entries = [];
|
|
8539
|
+
function walk(dir, depth) {
|
|
8540
|
+
if (depth > MAX_DIR_DEPTH || entries.length >= MAX_DIR_ENTRIES) return;
|
|
8541
|
+
let dirents;
|
|
8542
|
+
try {
|
|
8543
|
+
dirents = readdirSync2(dir, { withFileTypes: true, encoding: "utf8" });
|
|
8544
|
+
} catch {
|
|
8545
|
+
return;
|
|
8546
|
+
}
|
|
8547
|
+
dirents.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
8548
|
+
for (const e of dirents) {
|
|
8549
|
+
if (entries.length >= MAX_DIR_ENTRIES) return;
|
|
8550
|
+
if (e.isSymbolicLink()) continue;
|
|
8551
|
+
const full = join16(dir, e.name);
|
|
8552
|
+
if (e.isDirectory()) {
|
|
8553
|
+
if (SKIP_DIRS2.has(e.name)) continue;
|
|
8554
|
+
walk(full, depth + 1);
|
|
8555
|
+
} else if (e.isFile()) {
|
|
8556
|
+
let text;
|
|
8557
|
+
try {
|
|
8558
|
+
text = readFileSync18(full, "utf8").slice(0, MAX_FILE_BYTES2);
|
|
8559
|
+
} catch {
|
|
8560
|
+
continue;
|
|
8561
|
+
}
|
|
8562
|
+
const rel = full.slice(absDir.length).replace(/^[/\\]/, "").replace(/\\/g, "/");
|
|
8563
|
+
entries.push({ path: rel, content: text });
|
|
8564
|
+
}
|
|
8565
|
+
}
|
|
8566
|
+
}
|
|
8567
|
+
walk(absDir, 0);
|
|
8568
|
+
return entries;
|
|
8569
|
+
}
|
|
8570
|
+
var INSTRUCTION_SURFACES = [
|
|
8571
|
+
{ match: (r) => r === "SKILL.md", kind: "skill" },
|
|
8572
|
+
{ match: (r) => r === "AGENTS.md" || r === "CLAUDE.md", kind: "agents" },
|
|
8573
|
+
{ match: (r) => r === "README.md", kind: "readme" },
|
|
8574
|
+
{ match: (r) => r.startsWith(".cursor/rules/") && r.endsWith(".md"), kind: "agents" }
|
|
8575
|
+
];
|
|
8576
|
+
function surfaceKindFor(rel) {
|
|
8577
|
+
for (const s of INSTRUCTION_SURFACES) if (s.match(rel)) return s.kind;
|
|
8578
|
+
return null;
|
|
8579
|
+
}
|
|
8580
|
+
function buildAuthorityForTarget(input, artifactDigest) {
|
|
8581
|
+
const servers = [];
|
|
8582
|
+
const surfaces = [];
|
|
8583
|
+
if (input.sourceType === "mcp-config" && typeof input.content === "string") {
|
|
8584
|
+
try {
|
|
8585
|
+
servers.push(...parseConfigText(input.content, input.source).servers);
|
|
8586
|
+
} catch {
|
|
8587
|
+
}
|
|
8588
|
+
}
|
|
8589
|
+
if (input.sourceType === "dir" && input.entries) {
|
|
8590
|
+
for (const e of input.entries) {
|
|
8591
|
+
const kind = surfaceKindFor(e.path);
|
|
8592
|
+
if (kind) {
|
|
8593
|
+
surfaces.push({
|
|
8594
|
+
path: e.path,
|
|
8595
|
+
kind,
|
|
8596
|
+
text: e.content,
|
|
8597
|
+
truncated: e.content.length >= MAX_FILE_BYTES2
|
|
8598
|
+
});
|
|
8599
|
+
}
|
|
8600
|
+
}
|
|
8601
|
+
} else if (input.sourceType === "file" && typeof input.content === "string") {
|
|
8602
|
+
const rel = basename4(input.source);
|
|
8603
|
+
const kind = surfaceKindFor(rel) ?? (rel.toLowerCase().endsWith(".md") ? "readme" : null);
|
|
8604
|
+
if (kind) {
|
|
8605
|
+
surfaces.push({
|
|
8606
|
+
path: rel,
|
|
8607
|
+
kind,
|
|
8608
|
+
text: input.content,
|
|
8609
|
+
truncated: input.content.length >= MAX_FILE_BYTES2
|
|
8610
|
+
});
|
|
8611
|
+
}
|
|
8612
|
+
}
|
|
8613
|
+
return buildAuthorityManifest({ artifactDigest, servers, surfaces });
|
|
8614
|
+
}
|
|
8615
|
+
function defaultHostConfigPath(host) {
|
|
8616
|
+
if (host === CLAUDE_CODE_HOST_ID) return join16(homedir(), ".claude.json");
|
|
8617
|
+
return null;
|
|
8618
|
+
}
|
|
8619
|
+
function plannedServersFor(input, host) {
|
|
8620
|
+
if (input.sourceType !== "mcp-config" || typeof input.content !== "string") return [];
|
|
8621
|
+
let servers;
|
|
8622
|
+
try {
|
|
8623
|
+
servers = parseConfigText(input.content, input.source).servers;
|
|
8624
|
+
} catch {
|
|
8625
|
+
return [];
|
|
8626
|
+
}
|
|
8627
|
+
return servers.map((s) => ({
|
|
8628
|
+
name: s.name,
|
|
8629
|
+
entry: claudeCodeServerEntry({ command: s.command, args: s.args, url: s.url, envKeys: s.envKeys })
|
|
8630
|
+
}));
|
|
8631
|
+
}
|
|
8632
|
+
function buildPlanForHost(host, input, artifactDigest, authority, decision, deps, configPathOverride) {
|
|
8633
|
+
const adapter = getHostAdapter(host);
|
|
8634
|
+
if (!adapter) {
|
|
8635
|
+
return { error: `Unknown host "${host}". Known hosts: ${CLAUDE_CODE_HOST_ID}`, exitCode: EXIT.USAGE };
|
|
8636
|
+
}
|
|
8637
|
+
const servers = plannedServersFor(input, host);
|
|
8638
|
+
if (servers.length === 0) return null;
|
|
8639
|
+
const configPath = configPathOverride ? resolvePath4(deps.cwd, configPathOverride) : defaultHostConfigPath(host);
|
|
8640
|
+
if (!configPath) {
|
|
8641
|
+
return { error: `No default config path known for host "${host}"; pass --host-config <path>`, exitCode: EXIT.USAGE };
|
|
8642
|
+
}
|
|
8643
|
+
let currentConfig = null;
|
|
8644
|
+
let configDigest = "absent";
|
|
8645
|
+
if (existsSync11(configPath)) {
|
|
8646
|
+
try {
|
|
8647
|
+
const bytes = readFileSync18(configPath, "utf8");
|
|
8648
|
+
configDigest = hashJson(bytes);
|
|
8649
|
+
currentConfig = JSON.parse(bytes);
|
|
8650
|
+
} catch {
|
|
8651
|
+
return { error: `Host config exists but is not readable/valid JSON: ${configPath}`, exitCode: EXIT.ERROR };
|
|
8652
|
+
}
|
|
8653
|
+
}
|
|
8654
|
+
const backupPath = `${configPath}.calllint-backup`;
|
|
8655
|
+
const ctx = {
|
|
8656
|
+
host,
|
|
8657
|
+
tier: adapter.tier,
|
|
8658
|
+
configPath,
|
|
8659
|
+
configDigest,
|
|
8660
|
+
currentConfig,
|
|
8661
|
+
servers,
|
|
8662
|
+
backupPath,
|
|
8663
|
+
expiresAt: planExpiry(deps.generatedAt)
|
|
8664
|
+
};
|
|
8665
|
+
const plan = adapter.createPlan(ctx, { artifactDigest, authority, decision });
|
|
8666
|
+
const check = adapter.validatePlan(plan);
|
|
8667
|
+
if (!check.ok) {
|
|
8668
|
+
return { error: `Generated plan failed validation: ${check.errors.join("; ")}`, exitCode: EXIT.ERROR };
|
|
8669
|
+
}
|
|
8670
|
+
return plan;
|
|
8671
|
+
}
|
|
8672
|
+
function planExpiry(generatedAt) {
|
|
8673
|
+
const t = Date.parse(generatedAt);
|
|
8674
|
+
if (Number.isNaN(t)) return generatedAt;
|
|
8675
|
+
return new Date(t + 60 * 60 * 1e3).toISOString();
|
|
8676
|
+
}
|
|
8677
|
+
function writePlanFile(plan, deps) {
|
|
8678
|
+
const dir = join16(deps.cwd, ".calllint", "plans");
|
|
8679
|
+
mkdirSync5(dir, { recursive: true });
|
|
8680
|
+
const file = join16(dir, `${plan.planId}.json`);
|
|
8681
|
+
writeFileSync10(file, JSON.stringify(plan, null, 2) + "\n", "utf8");
|
|
8682
|
+
return file;
|
|
8683
|
+
}
|
|
8684
|
+
function loadEvidence(file, deps, format, provider) {
|
|
8685
|
+
let rawText;
|
|
8686
|
+
try {
|
|
8687
|
+
rawText = readFileSync18(resolvePath4(deps.cwd, file), "utf8");
|
|
8688
|
+
} catch (err2) {
|
|
8689
|
+
const e = err2;
|
|
8690
|
+
return {
|
|
8691
|
+
error: e.code === "ENOENT" ? `Evidence file not found: ${file}` : e.message,
|
|
8692
|
+
exitCode: EXIT.USAGE
|
|
8693
|
+
};
|
|
8694
|
+
}
|
|
8695
|
+
return importEvidence(rawText, { provider, format });
|
|
8696
|
+
}
|
|
8697
|
+
function trustPrepare(args, deps) {
|
|
8698
|
+
const target = args.positionals[1];
|
|
8699
|
+
if (!target) {
|
|
8700
|
+
return {
|
|
8701
|
+
stdout: "",
|
|
8702
|
+
stderr: "Error: Missing target\nUsage: calllint trust prepare <git-url|dir|SKILL.md|mcp.json> [--evidence <file>] [--json]",
|
|
8703
|
+
exitCode: EXIT.USAGE
|
|
8704
|
+
};
|
|
8705
|
+
}
|
|
8706
|
+
const input = loadArtifactInput(target, deps);
|
|
8707
|
+
if ("error" in input) {
|
|
8708
|
+
return { stdout: "", stderr: `Error: ${input.error}`, exitCode: input.exitCode };
|
|
8709
|
+
}
|
|
8710
|
+
if (flagBool(args.flags, "with-skillspector")) {
|
|
8711
|
+
return {
|
|
8712
|
+
stdout: "",
|
|
8713
|
+
stderr: "Error: --with-skillspector (pinned runner) is not wired yet.\nRun SkillSpector yourself, then attach its report:\n calllint trust prepare " + target + " --evidence skillspector-report.json",
|
|
8714
|
+
exitCode: EXIT.USAGE
|
|
8715
|
+
};
|
|
8716
|
+
}
|
|
8717
|
+
const evidenceFile = typeof args.flags["evidence"] === "string" ? args.flags["evidence"] : void 0;
|
|
8718
|
+
const formatFlag = args.flags["format"];
|
|
8719
|
+
const format = formatFlag === "sarif" ? "sarif" : formatFlag === "json" ? "json" : void 0;
|
|
8720
|
+
const providerFlag = typeof args.flags["provider"] === "string" ? args.flags["provider"] : void 0;
|
|
8721
|
+
let evidence;
|
|
8722
|
+
if (evidenceFile) {
|
|
8723
|
+
const imported = loadEvidence(evidenceFile, deps, format, providerFlag);
|
|
8724
|
+
if ("error" in imported) {
|
|
8725
|
+
return { stdout: "", stderr: `Error: ${imported.error}`, exitCode: imported.exitCode };
|
|
8726
|
+
}
|
|
8727
|
+
evidence = [imported];
|
|
8728
|
+
}
|
|
8729
|
+
const artifact = resolveArtifactIdentity(input);
|
|
8730
|
+
const authority = buildAuthorityForTarget(input, artifact.digest ?? null);
|
|
8731
|
+
const policyPath = flagStr(args.flags, "policy");
|
|
8732
|
+
const policy = loadPolicyOrDefault(policyPath ? resolvePath4(deps.cwd, policyPath) : void 0);
|
|
8733
|
+
const decision = artifact.resolution === "resolved" ? decideOverAuthority({ authority, evidence, policy }) : void 0;
|
|
8734
|
+
let plan;
|
|
8735
|
+
const hostFlag = flagStr(args.flags, "host");
|
|
8736
|
+
if (hostFlag && decision && decision.verdict !== "UNKNOWN") {
|
|
8737
|
+
const hostConfig = flagStr(args.flags, "host-config");
|
|
8738
|
+
const built = buildPlanForHost(hostFlag, input, artifact.digest ?? null, authority, decision, deps, hostConfig);
|
|
8739
|
+
if (built && "error" in built) {
|
|
8740
|
+
return { stdout: "", stderr: `Error: ${built.error}`, exitCode: built.exitCode };
|
|
8741
|
+
}
|
|
8742
|
+
plan = built ?? void 0;
|
|
8743
|
+
}
|
|
8744
|
+
const preparation = prepare({
|
|
8745
|
+
artifact,
|
|
8746
|
+
evidence,
|
|
8747
|
+
authority,
|
|
8748
|
+
decision,
|
|
8749
|
+
plan,
|
|
8750
|
+
preparedAt: deps.generatedAt
|
|
8751
|
+
});
|
|
8752
|
+
const exitCode = prepareExitCode(preparation);
|
|
8753
|
+
let planNote = "";
|
|
8754
|
+
if (preparation.plan && flagBool(args.flags, "write-plan")) {
|
|
8755
|
+
const file = writePlanFile(preparation.plan, deps);
|
|
8756
|
+
planNote = `
|
|
8757
|
+
plan written: ${file}
|
|
8758
|
+
`;
|
|
8759
|
+
}
|
|
8760
|
+
if (flagBool(args.flags, "json")) {
|
|
8761
|
+
return { stdout: JSON.stringify(preparation, null, 2), stderr: "", exitCode };
|
|
8762
|
+
}
|
|
8763
|
+
return { stdout: renderPreparation(preparation) + planNote, stderr: "", exitCode };
|
|
8764
|
+
}
|
|
8765
|
+
function loadPreparation(args, deps) {
|
|
8766
|
+
const file = args.positionals[1];
|
|
8767
|
+
if (!file) {
|
|
8768
|
+
return { error: "Missing preparation file", exitCode: EXIT.USAGE };
|
|
8769
|
+
}
|
|
8770
|
+
let raw;
|
|
8771
|
+
try {
|
|
8772
|
+
raw = readFileSync18(join16(deps.cwd, file), "utf8");
|
|
8773
|
+
} catch (err2) {
|
|
8774
|
+
const e = err2;
|
|
8775
|
+
return {
|
|
8776
|
+
error: e.code === "ENOENT" ? `File not found: ${file}` : e.message,
|
|
8777
|
+
exitCode: EXIT.USAGE
|
|
8778
|
+
};
|
|
8779
|
+
}
|
|
8780
|
+
let parsed;
|
|
8781
|
+
try {
|
|
8782
|
+
parsed = JSON.parse(raw);
|
|
8783
|
+
} catch {
|
|
8784
|
+
return { error: `Not valid JSON: ${file}`, exitCode: EXIT.ERROR };
|
|
8785
|
+
}
|
|
8786
|
+
if (!parsed || typeof parsed !== "object" || parsed.schema !== "calllint.trust-preparation.v0") {
|
|
8787
|
+
return { error: `Not a calllint.trust-preparation.v0 document: ${file}`, exitCode: EXIT.ERROR };
|
|
8788
|
+
}
|
|
8789
|
+
return parsed;
|
|
8790
|
+
}
|
|
8791
|
+
function trustShow(args, deps) {
|
|
8792
|
+
const prep = loadPreparation(args, deps);
|
|
8793
|
+
if ("error" in prep) return { stdout: "", stderr: `Error: ${prep.error}`, exitCode: prep.exitCode };
|
|
8794
|
+
if (flagBool(args.flags, "json")) {
|
|
8795
|
+
return { stdout: JSON.stringify(prep, null, 2), stderr: "", exitCode: EXIT.OK };
|
|
8796
|
+
}
|
|
8797
|
+
return { stdout: renderPreparation(prep), stderr: "", exitCode: EXIT.OK };
|
|
8798
|
+
}
|
|
8799
|
+
function trustExplain(args, deps) {
|
|
8800
|
+
const prep = loadPreparation(args, deps);
|
|
8801
|
+
if ("error" in prep) return { stdout: "", stderr: `Error: ${prep.error}`, exitCode: prep.exitCode };
|
|
8802
|
+
return { stdout: renderExplanation(prep), stderr: "", exitCode: EXIT.OK };
|
|
8803
|
+
}
|
|
8804
|
+
function receiptId(planDigest, now) {
|
|
8805
|
+
return "clrec_" + hashJson({ planDigest, now }).slice("sha256:".length, "sha256:".length + 16);
|
|
8806
|
+
}
|
|
8807
|
+
function applyExitCode(r) {
|
|
8808
|
+
if (r.outcome === "applied") return EXIT.OK;
|
|
8809
|
+
if (r.outcome === "already_applied") return EXIT.REVIEW;
|
|
8810
|
+
return EXIT.UNKNOWN;
|
|
8811
|
+
}
|
|
8812
|
+
function trustApply(args, deps) {
|
|
8813
|
+
const planFile = flagStr(args.flags, "plan");
|
|
8814
|
+
const approve = flagStr(args.flags, "approve");
|
|
8815
|
+
if (!planFile) {
|
|
8816
|
+
return usageErr("Missing --plan <file>\nUsage: calllint trust apply --plan <plan.json> --approve <plan-digest>");
|
|
8817
|
+
}
|
|
8818
|
+
if (!approve) {
|
|
8819
|
+
return usageErr("Missing --approve <plan-digest>\nApproval must name the exact plan digest you reviewed (see `trust prepare`).");
|
|
8820
|
+
}
|
|
8821
|
+
let plan;
|
|
8822
|
+
try {
|
|
8823
|
+
plan = JSON.parse(readFileSync18(resolvePath4(deps.cwd, planFile), "utf8"));
|
|
8824
|
+
} catch (err2) {
|
|
8825
|
+
const e = err2;
|
|
8826
|
+
return { stdout: "", stderr: e.code === "ENOENT" ? `Plan file not found: ${planFile}` : `Plan file is not valid JSON: ${e.message}`, exitCode: EXIT.ERROR };
|
|
8827
|
+
}
|
|
8828
|
+
if (plan?.schema !== "calllint.install-plan.v1") {
|
|
8829
|
+
return { stdout: "", stderr: `Not a calllint.install-plan.v1 document: ${planFile}`, exitCode: EXIT.ERROR };
|
|
8830
|
+
}
|
|
8831
|
+
const adapter = getHostAdapter(plan.host);
|
|
8832
|
+
if (!adapter) return usageErr(`Unknown host "${plan.host}". Known hosts: ${CLAUDE_CODE_HOST_ID}`);
|
|
8833
|
+
if (!adapter.applyPlan) {
|
|
8834
|
+
return usageErr(`Host "${plan.host}" is tier ${adapter.tier} \u2014 plan-only; copy the patch or use a Tier-A host to apply.`);
|
|
8835
|
+
}
|
|
8836
|
+
const rawTarget = plan.operations[0]?.target;
|
|
8837
|
+
if (!rawTarget) return usageErr("Plan has no operations to apply.");
|
|
8838
|
+
let configPath;
|
|
8839
|
+
try {
|
|
8840
|
+
configPath = safeConfigPath(rawTarget, { cwd: deps.cwd, home: homedir() });
|
|
8841
|
+
} catch (err2) {
|
|
8842
|
+
if (err2 instanceof PathSafetyError) return { stdout: "", stderr: `Unsafe target path in plan: ${err2.message}`, exitCode: EXIT.ERROR };
|
|
8843
|
+
throw err2;
|
|
8844
|
+
}
|
|
8845
|
+
const rid = receiptId(plan.planDigest, deps.generatedAt);
|
|
8846
|
+
const backupPath = `${configPath}.calllint-backup-${rid}`;
|
|
8847
|
+
const lockPath = join16(deps.cwd, ".calllint", "locks", `${hashJson(configPath).slice("sha256:".length, "sha256:".length + 16)}.lock`);
|
|
8848
|
+
const result = adapter.applyPlan(plan, {
|
|
8849
|
+
approvalDigest: approve,
|
|
8850
|
+
configPath,
|
|
8851
|
+
backupPath,
|
|
8852
|
+
lockPath,
|
|
8853
|
+
fs: nodeFsPort(),
|
|
8854
|
+
now: deps.generatedAt
|
|
8855
|
+
});
|
|
8856
|
+
let receiptNote = "";
|
|
8857
|
+
const receiptOut = flagStr(args.flags, "receipt");
|
|
8858
|
+
if (receiptOut) {
|
|
8859
|
+
const err2 = emitReceipt(result, plan, args, deps, receiptOut);
|
|
8860
|
+
if (err2) return err2;
|
|
8861
|
+
receiptNote = `
|
|
8862
|
+
receipt: ${resolvePath4(deps.cwd, receiptOut)}
|
|
8863
|
+
`;
|
|
8864
|
+
}
|
|
8865
|
+
if (flagBool(args.flags, "json")) {
|
|
8866
|
+
return { stdout: JSON.stringify(result, null, 2), stderr: "", exitCode: applyExitCode(result) };
|
|
8867
|
+
}
|
|
8868
|
+
return { stdout: renderApplyResult(result) + receiptNote, stderr: "", exitCode: applyExitCode(result) };
|
|
8869
|
+
}
|
|
8870
|
+
function emitReceipt(result, plan, args, deps, outFile) {
|
|
8871
|
+
const approver = flagStr(args.flags, "approver") ?? (userInfo().username || null);
|
|
8872
|
+
let receipt = buildDecisionReceipt(result, plan, {
|
|
8873
|
+
approvedAt: deps.generatedAt,
|
|
8874
|
+
approver,
|
|
8875
|
+
scannerVersion: deps.toolVersion ?? "0.0.0-dev",
|
|
8876
|
+
policyVersion: null
|
|
8877
|
+
});
|
|
8878
|
+
const keyFile = flagStr(args.flags, "key");
|
|
8879
|
+
if (flagBool(args.flags, "sign") || keyFile) {
|
|
8880
|
+
if (!keyFile) return usageErr("--sign requires --key <keyfile> (a local ed25519 keypair from `receipt keygen`)");
|
|
8881
|
+
try {
|
|
8882
|
+
const kp = importKeypair(JSON.parse(readFileSync18(resolvePath4(deps.cwd, keyFile), "utf8")));
|
|
8883
|
+
receipt = signDecisionReceipt(receipt, kp);
|
|
8884
|
+
} catch (err2) {
|
|
8885
|
+
return { stdout: "", stderr: `Cannot load signing key: ${err2.message}`, exitCode: EXIT.ERROR };
|
|
8886
|
+
}
|
|
8887
|
+
}
|
|
8888
|
+
try {
|
|
8889
|
+
writeFileSync10(resolvePath4(deps.cwd, outFile), JSON.stringify(receipt, null, 2) + "\n");
|
|
8890
|
+
} catch (err2) {
|
|
8891
|
+
return { stdout: "", stderr: `Cannot write receipt: ${err2.message}`, exitCode: EXIT.ERROR };
|
|
8892
|
+
}
|
|
8893
|
+
return null;
|
|
8894
|
+
}
|
|
8895
|
+
function trustVerify(args, deps) {
|
|
8896
|
+
const file = args.positionals[1];
|
|
8897
|
+
if (!file) return usageErr("Usage: calllint trust verify <receipt.json> [--public-key <keyfile>]");
|
|
8898
|
+
let parsed;
|
|
8899
|
+
try {
|
|
8900
|
+
parsed = JSON.parse(readFileSync18(resolvePath4(deps.cwd, file), "utf8"));
|
|
8901
|
+
} catch (err2) {
|
|
8902
|
+
const e = err2;
|
|
8903
|
+
return { stdout: "", stderr: e.code === "ENOENT" ? `Receipt file not found: ${file}` : `Receipt is not valid JSON: ${e.message}`, exitCode: EXIT.ERROR };
|
|
8904
|
+
}
|
|
8905
|
+
let publicKey;
|
|
8906
|
+
const pkFile = flagStr(args.flags, "public-key");
|
|
8907
|
+
if (pkFile) {
|
|
8908
|
+
try {
|
|
8909
|
+
const j = JSON.parse(readFileSync18(resolvePath4(deps.cwd, pkFile), "utf8"));
|
|
8910
|
+
publicKey = j.public_key ?? j.publicKey;
|
|
8911
|
+
if (typeof publicKey !== "string") throw new Error("no public_key field");
|
|
8912
|
+
} catch (err2) {
|
|
8913
|
+
return { stdout: "", stderr: `Cannot load public key: ${err2.message}`, exitCode: EXIT.ERROR };
|
|
8914
|
+
}
|
|
8915
|
+
}
|
|
8916
|
+
const res = verifyDecisionReceipt(parsed, { now: deps.generatedAt, publicKey });
|
|
8917
|
+
if (flagBool(args.flags, "json")) {
|
|
8918
|
+
return { stdout: JSON.stringify(res, null, 2), stderr: "", exitCode: res.valid ? EXIT.OK : 1 };
|
|
8919
|
+
}
|
|
8920
|
+
let out = `
|
|
8921
|
+
CallLint trust verify
|
|
8922
|
+
`;
|
|
8923
|
+
out += `receipt: ${resolvePath4(deps.cwd, file)}
|
|
8924
|
+
`;
|
|
8925
|
+
out += `structure: ${res.valid ? "\u2705 valid" : "\u26D4 invalid"}
|
|
8926
|
+
`;
|
|
8927
|
+
out += `signed: ${res.signed ? publicKey ? res.tampered ? "\u26D4 signature INVALID" : "\u2705 signature verified" : "\u25C7 present (no --public-key; not checked)" : "\u25C7 unsigned"}
|
|
8928
|
+
`;
|
|
8929
|
+
out += `expiry: ${res.expired ? "\u26A0 EXPIRED" : "\u2705 within validity window"}
|
|
8930
|
+
`;
|
|
8931
|
+
if (res.errors.length > 0) {
|
|
8932
|
+
out += `
|
|
8933
|
+
errors:
|
|
8934
|
+
`;
|
|
8935
|
+
for (const e of res.errors) out += ` \u2022 ${e}
|
|
8936
|
+
`;
|
|
8937
|
+
}
|
|
8938
|
+
return { stdout: out, stderr: "", exitCode: res.valid ? EXIT.OK : 1 };
|
|
8939
|
+
}
|
|
8940
|
+
function usageErr(msg) {
|
|
8941
|
+
return { stdout: "", stderr: `Error: ${msg}`, exitCode: EXIT.USAGE };
|
|
8942
|
+
}
|
|
8943
|
+
var STATE_BADGE = {
|
|
8944
|
+
PLAN_READY: "\u25C7 prepared (read-only)",
|
|
8945
|
+
AUTHORITY_NORMALIZED: "\u25C7 authority normalized (read-only)",
|
|
8946
|
+
DECIDED: "\u25C7 decided (read-only)",
|
|
8947
|
+
FETCH_REJECTED: "\u26A0 not a verified target",
|
|
8948
|
+
RESOLUTION_FAILED: "\u26D4 could not resolve",
|
|
8949
|
+
EVIDENCE_PARTIAL: "\u26A0 evidence partial",
|
|
8950
|
+
EVIDENCE_FAILED: "\u26D4 evidence failed",
|
|
8951
|
+
POLICY_UNKNOWN: "\u25C7 policy unknown (insufficient evidence)",
|
|
8952
|
+
VERIFIED: "\u2705 applied + verified",
|
|
8953
|
+
APPLY_CONFLICT: "\u26D4 config conflict",
|
|
8954
|
+
PLAN_STALE: "\u26D4 plan stale",
|
|
8955
|
+
VERIFICATION_FAILED: "\u21A9 verify failed \u2014 rolled back",
|
|
8956
|
+
ROLLBACK_REQUIRED: "\u{1F6A8} rollback required"
|
|
8957
|
+
};
|
|
8958
|
+
var VERDICT_SYMBOL = {
|
|
8959
|
+
SAFE: "\u{1F6E1} SAFE",
|
|
8960
|
+
REVIEW: "\u26A0 REVIEW",
|
|
8961
|
+
BLOCK: "\u26D4 BLOCK",
|
|
8962
|
+
UNKNOWN: "\u25C7 UNKNOWN"
|
|
8963
|
+
};
|
|
8964
|
+
function renderPreparation(p) {
|
|
8965
|
+
const a = p.artifact;
|
|
8966
|
+
let out = `
|
|
8967
|
+
CallLint trust prepare (read-only)
|
|
8968
|
+
`;
|
|
8969
|
+
out += `target: ${a.source}
|
|
8970
|
+
`;
|
|
8971
|
+
out += `source type: ${a.sourceType}
|
|
8972
|
+
`;
|
|
8973
|
+
out += `requested: ${a.requestedRef ?? "(none)"}
|
|
8974
|
+
`;
|
|
8975
|
+
out += `resolved ref: ${a.resolvedRef ?? "(unresolved)"}
|
|
8976
|
+
`;
|
|
8977
|
+
out += `digest: ${a.digest ?? "(none)"}
|
|
8978
|
+
`;
|
|
8979
|
+
out += `resolution: ${a.resolution}
|
|
8980
|
+
`;
|
|
8981
|
+
out += `state: ${STATE_BADGE[p.state] ?? p.state}
|
|
8982
|
+
`;
|
|
8983
|
+
if (p.evidence === null) {
|
|
8984
|
+
out += `evidence: (none attached \u2014 pass --evidence <file>)
|
|
8985
|
+
`;
|
|
8986
|
+
} else {
|
|
8987
|
+
out += `evidence: ${p.evidence.length} provider(s)
|
|
8988
|
+
`;
|
|
8989
|
+
for (const e of p.evidence) {
|
|
8990
|
+
out += ` \u2022 ${e.provider} (${e.providerVersion}) \u2014 ${e.scanMode}, ${e.completeness}, ${e.findings.length} finding(s), not re-scored
|
|
8991
|
+
`;
|
|
8992
|
+
}
|
|
8993
|
+
}
|
|
8994
|
+
if (p.authority === null) {
|
|
8995
|
+
out += `authority: (not normalized \u2014 G3)
|
|
8996
|
+
`;
|
|
8997
|
+
} else {
|
|
8998
|
+
const caps = p.authority.capabilities;
|
|
8999
|
+
out += `authority: ${caps.length} capabilit${caps.length === 1 ? "y" : "ies"}`;
|
|
9000
|
+
out += p.authority.completeness === "partial" ? " (partial)\n" : "\n";
|
|
9001
|
+
for (const c of caps) {
|
|
9002
|
+
const dst = c.destination ? ` \u2192 ${c.destination}` : "";
|
|
9003
|
+
const appr = c.approvalRequirement === "none" ? "" : ` [${c.approvalRequirement}]`;
|
|
9004
|
+
out += ` \u2022 ${c.action} ${c.resource}${dst}${appr} (${c.evidenceSource})
|
|
9005
|
+
`;
|
|
9006
|
+
}
|
|
9007
|
+
if (p.authority.approval.required.length > 0) {
|
|
9008
|
+
out += ` approvals required: ${p.authority.approval.required.join(", ")}
|
|
9009
|
+
`;
|
|
9010
|
+
}
|
|
9011
|
+
}
|
|
9012
|
+
if (p.decision === null) {
|
|
9013
|
+
out += `decision: (not decided \u2014 G4)
|
|
9014
|
+
`;
|
|
9015
|
+
} else {
|
|
9016
|
+
const d = p.decision;
|
|
9017
|
+
out += `decision: ${VERDICT_SYMBOL[d.verdict] ?? d.verdict}`;
|
|
9018
|
+
out += d.completeness === "partial" ? " (over partial evidence)\n" : "\n";
|
|
9019
|
+
for (const r of d.reasons) {
|
|
9020
|
+
out += ` \u2022 ${r.code} \u2192 ${r.contributes} (${r.evidenceSource})
|
|
9021
|
+
`;
|
|
9022
|
+
}
|
|
9023
|
+
if (d.requiredApprovals.length > 0) {
|
|
9024
|
+
out += ` approvals required: ${d.requiredApprovals.join(", ")}
|
|
9025
|
+
`;
|
|
9026
|
+
}
|
|
9027
|
+
}
|
|
9028
|
+
if (p.plan === null) {
|
|
9029
|
+
out += `plan: (none \u2014 pass --host <id> for an install plan)
|
|
9030
|
+
`;
|
|
9031
|
+
} else {
|
|
9032
|
+
const pl = p.plan;
|
|
9033
|
+
out += `plan: host "${pl.host}" (tier ${pl.tier}) \u2014 ${pl.operations.length} op(s), ${pl.rollback.length} rollback op(s)
|
|
9034
|
+
`;
|
|
9035
|
+
out += ` plan id: ${pl.planId}
|
|
9036
|
+
`;
|
|
9037
|
+
out += ` plan digest:${pl.planDigest}
|
|
9038
|
+
`;
|
|
9039
|
+
out += ` expires: ${pl.expiresAt}
|
|
9040
|
+
`;
|
|
9041
|
+
out += ` NOT applied \u2014 review, then: calllint trust apply --plan <file> --approve ${pl.planDigest}
|
|
9042
|
+
`;
|
|
9043
|
+
}
|
|
9044
|
+
if (p.notes.length > 0) {
|
|
9045
|
+
out += `
|
|
9046
|
+
notes:
|
|
9047
|
+
`;
|
|
9048
|
+
for (const n of p.notes) out += ` \u2022 ${n}
|
|
9049
|
+
`;
|
|
9050
|
+
}
|
|
9051
|
+
out += `
|
|
9052
|
+
This is the READ-ONLY half of the Trust Gateway. It touched no live config
|
|
9053
|
+
`;
|
|
9054
|
+
out += `and never executed the target. An unresolved target is never a pass.
|
|
9055
|
+
`;
|
|
9056
|
+
return out;
|
|
9057
|
+
}
|
|
9058
|
+
function renderExplanation(p) {
|
|
9059
|
+
let out = `
|
|
9060
|
+
CallLint trust explain
|
|
9061
|
+
`;
|
|
9062
|
+
out += `state: ${p.state}
|
|
9063
|
+
|
|
9064
|
+
`;
|
|
9065
|
+
switch (p.state) {
|
|
9066
|
+
case "AUTHORITY_NORMALIZED": {
|
|
9067
|
+
const caps = p.authority?.capabilities.length ?? 0;
|
|
9068
|
+
out += `The artifact resolved to an immutable, digested identity
|
|
9069
|
+
`;
|
|
9070
|
+
out += `(${p.artifact.resolvedRef}) and its authority was normalized into a
|
|
9071
|
+
`;
|
|
9072
|
+
out += `manifest of ${caps} capabilit${caps === 1 ? "y" : "ies"}, each pinned to the evidence byte
|
|
9073
|
+
`;
|
|
9074
|
+
out += `that granted it. This is an inventory, not a verdict \u2014 the deterministic
|
|
9075
|
+
`;
|
|
9076
|
+
out += `decision (G4) will bind this manifest's digest. UNKNOWN is never SAFE.
|
|
9077
|
+
`;
|
|
9078
|
+
break;
|
|
9079
|
+
}
|
|
9080
|
+
case "DECIDED": {
|
|
9081
|
+
const d = p.decision;
|
|
9082
|
+
out += `The artifact resolved and its authority was normalized, then the
|
|
9083
|
+
`;
|
|
9084
|
+
out += `deterministic policy decided ${d?.verdict ?? "?"} over the manifest \u2014
|
|
9085
|
+
`;
|
|
9086
|
+
out += `binding the artifact, authority, and policy digests. The verdict comes
|
|
9087
|
+
`;
|
|
9088
|
+
out += `from normalized authority + policy, never from a scanner: external
|
|
9089
|
+
`;
|
|
9090
|
+
out += `evidence can add reasons or lower confidence, but never sets it alone.
|
|
9091
|
+
`;
|
|
9092
|
+
break;
|
|
9093
|
+
}
|
|
9094
|
+
case "POLICY_UNKNOWN":
|
|
9095
|
+
out += `The policy could not reach a confident verdict \u2014 authority or evidence
|
|
9096
|
+
`;
|
|
9097
|
+
out += `was incomplete, so the decision is UNKNOWN. Insufficient evidence is
|
|
9098
|
+
`;
|
|
9099
|
+
out += `fail-closed: UNKNOWN outranks REVIEW and never reads as a pass.
|
|
9100
|
+
`;
|
|
9101
|
+
break;
|
|
9102
|
+
case "PLAN_READY":
|
|
9103
|
+
out += `The artifact resolved to an immutable, digested identity
|
|
9104
|
+
`;
|
|
9105
|
+
out += `(${p.artifact.resolvedRef}). Downstream evidence, authority, and a
|
|
9106
|
+
`;
|
|
9107
|
+
out += `deterministic decision will bind this digest as Phase G lands.
|
|
9108
|
+
`;
|
|
9109
|
+
break;
|
|
9110
|
+
case "FETCH_REJECTED":
|
|
9111
|
+
out += `The artifact could not be fully pinned \u2014 either an immutable ref or
|
|
9112
|
+
`;
|
|
9113
|
+
out += `the bytes to digest were missing. It is NOT a verified target and the
|
|
9114
|
+
`;
|
|
9115
|
+
out += `gateway will not advance to a plan.
|
|
9116
|
+
`;
|
|
9117
|
+
break;
|
|
9118
|
+
case "RESOLUTION_FAILED":
|
|
9119
|
+
out += `The target could not be resolved to an immutable, digested identity.
|
|
9120
|
+
`;
|
|
9121
|
+
out += `Nothing can be evaluated. UNKNOWN is never SAFE.
|
|
9122
|
+
`;
|
|
9123
|
+
break;
|
|
9124
|
+
default:
|
|
9125
|
+
out += `The read-only preparation stopped in a state that does not read as a
|
|
9126
|
+
`;
|
|
9127
|
+
out += `pass. See the notes for why.
|
|
9128
|
+
`;
|
|
9129
|
+
}
|
|
9130
|
+
if (p.notes.length > 0) {
|
|
9131
|
+
out += `
|
|
9132
|
+
why:
|
|
9133
|
+
`;
|
|
9134
|
+
for (const n of p.notes) out += ` \u2022 ${n}
|
|
9135
|
+
`;
|
|
9136
|
+
}
|
|
9137
|
+
return out;
|
|
9138
|
+
}
|
|
9139
|
+
var APPLY_BADGE = {
|
|
9140
|
+
applied: "\u2705 applied + verified",
|
|
9141
|
+
already_applied: "\u25C7 already applied (no change)",
|
|
9142
|
+
stale: "\u26D4 plan stale \u2014 not applied",
|
|
9143
|
+
conflict: "\u26D4 config conflict \u2014 not applied",
|
|
9144
|
+
rolled_back: "\u21A9 verify failed \u2014 rolled back to original",
|
|
9145
|
+
rollback_failed: "\u{1F6A8} rollback FAILED \u2014 manual intervention required"
|
|
9146
|
+
};
|
|
9147
|
+
function renderApplyResult(r) {
|
|
9148
|
+
let out = `
|
|
9149
|
+
CallLint trust apply
|
|
9150
|
+
`;
|
|
9151
|
+
out += `host: ${r.host}
|
|
9152
|
+
`;
|
|
9153
|
+
out += `config: ${r.configPath}
|
|
9154
|
+
`;
|
|
9155
|
+
out += `plan id: ${r.planId}
|
|
9156
|
+
`;
|
|
9157
|
+
out += `plan digest: ${r.planDigest}
|
|
9158
|
+
`;
|
|
9159
|
+
out += `state: ${r.state}
|
|
9160
|
+
`;
|
|
9161
|
+
out += `outcome: ${APPLY_BADGE[r.outcome] ?? r.outcome}
|
|
9162
|
+
`;
|
|
9163
|
+
out += `before: ${r.configDigestBefore}
|
|
9164
|
+
`;
|
|
9165
|
+
out += `after: ${r.configDigestAfter ?? "(unchanged / not written)"}
|
|
9166
|
+
`;
|
|
9167
|
+
if (r.backupPath) out += `backup: ${r.backupPath}
|
|
9168
|
+
`;
|
|
9169
|
+
if (r.notes.length > 0) {
|
|
9170
|
+
out += `
|
|
9171
|
+
notes:
|
|
9172
|
+
`;
|
|
9173
|
+
for (const n of r.notes) out += ` \u2022 ${n}
|
|
9174
|
+
`;
|
|
9175
|
+
}
|
|
9176
|
+
if (r.outcome === "applied") {
|
|
9177
|
+
out += `
|
|
9178
|
+
The config was written atomically and re-verified. To undo, restore the
|
|
9179
|
+
`;
|
|
9180
|
+
out += `backup above (the plan also carries typed rollback operations).
|
|
9181
|
+
`;
|
|
9182
|
+
} else if (r.outcome === "rollback_failed") {
|
|
9183
|
+
out += `
|
|
9184
|
+
The write could not be verified AND the automatic rollback failed. Your
|
|
9185
|
+
`;
|
|
9186
|
+
out += `original config is preserved at the backup path \u2014 restore it by hand.
|
|
9187
|
+
`;
|
|
9188
|
+
}
|
|
9189
|
+
return out;
|
|
9190
|
+
}
|
|
9191
|
+
function trustHelp() {
|
|
9192
|
+
return `
|
|
9193
|
+
calllint trust \u2014 Automated Trust Gateway (prepare \u2192 approve \u2192 apply \u2192 verify)
|
|
9194
|
+
|
|
9195
|
+
USAGE
|
|
9196
|
+
calllint trust prepare <target> [--evidence <file>] [--host <id>] [--json]
|
|
9197
|
+
calllint trust show <preparation.json> Human summary of a preparation
|
|
9198
|
+
calllint trust explain <preparation.json> Why this state / these notes
|
|
9199
|
+
calllint trust apply --plan <p> --approve <plan-digest> Apply an approved plan
|
|
9200
|
+
calllint trust verify <receipt.json> [--public-key <k>] Verify a decision receipt
|
|
9201
|
+
|
|
9202
|
+
DESCRIPTION
|
|
9203
|
+
\`trust prepare\` resolves a target (a directory, SKILL.md, mcp.json, or a
|
|
9204
|
+
remote git/npm ref) to an immutable, digest-pinned Artifact Identity
|
|
9205
|
+
(calllint.artifact.v1) and produces a READ-ONLY preview. It touches no live
|
|
9206
|
+
config and NEVER executes the target \u2014 it only reads bytes to digest them.
|
|
9207
|
+
|
|
9208
|
+
Attach a third-party scanner report with --evidence <file>: it is imported
|
|
9209
|
+
provenance-preserved and NEVER re-scored. Degraded or failed evidence can only
|
|
9210
|
+
tighten the preparation (fail-closed) \u2014 a degraded external scan never reads
|
|
9211
|
+
as a pass. An external SAFE never upgrades a CallLint verdict.
|
|
9212
|
+
|
|
9213
|
+
Remote git/npm targets need network to pin an immutable ref; offline they
|
|
9214
|
+
degrade explicitly (never a silent pass). Authority, Decision, and Install
|
|
9215
|
+
Plan slots are populated by later Phase-G steps.
|
|
9216
|
+
|
|
9217
|
+
OPTIONS (prepare)
|
|
9218
|
+
--evidence <file> Attach a third-party scanner report (JSON or SARIF)
|
|
9219
|
+
--format json|sarif Force the evidence format (default: auto-detect)
|
|
9220
|
+
--provider <name> Force the evidence provider adapter (default: auto-detect)
|
|
9221
|
+
--policy <file> Use a policy file for the decision (default: built-in defaults)
|
|
9222
|
+
--host <id> Build an install plan for a host (G5: claude-code). Reads
|
|
9223
|
+
the host config READ-ONLY; never applies. Plan is emitted
|
|
9224
|
+
only for a non-blocking decision (SAFE/REVIEW).
|
|
9225
|
+
--host-config <path> Override the host config path (default: ~/.claude.json)
|
|
9226
|
+
--write-plan Persist the plan to .calllint/plans/<plan-id>.json
|
|
9227
|
+
--no-llm Default posture: no LLM in the verdict path (accepted, no-op)
|
|
9228
|
+
--json Emit the raw calllint.trust-preparation.v0 document
|
|
9229
|
+
|
|
9230
|
+
OPTIONS (apply)
|
|
9231
|
+
--plan <file> The install plan to apply (from prepare --write-plan)
|
|
9232
|
+
--approve <digest> The exact plan digest you reviewed \u2014 binds the approval.
|
|
9233
|
+
A mismatch, a tampered plan, or an expired plan is refused
|
|
9234
|
+
(PLAN_STALE) before any write. Apply re-checks the target's
|
|
9235
|
+
precondition digest (drift \u2192 APPLY_CONFLICT), writes
|
|
9236
|
+
atomically with a backup, re-verifies, and rolls back on
|
|
9237
|
+
failure. Re-applying the same plan is a no-op.
|
|
9238
|
+
--receipt <file> After apply, write a calllint.receipt.v1 decision receipt
|
|
9239
|
+
(durable proof of the six-digest chain + approval + result)
|
|
9240
|
+
--sign --key <file> Sign the receipt with a local ed25519 keypair (\`receipt keygen\`)
|
|
9241
|
+
--approver <name> Attribution for the receipt (default: OS user)
|
|
9242
|
+
--json Emit the raw calllint.apply-result.v1 document
|
|
9243
|
+
|
|
9244
|
+
OPTIONS (verify)
|
|
9245
|
+
--public-key <file> Public key JSON to check the receipt's ed25519 signature.
|
|
9246
|
+
Without it, a signature is shape-checked but not verified.
|
|
9247
|
+
verify is READ-ONLY: it never re-judges, re-scans, or writes.
|
|
9248
|
+
--json Emit the raw verification result
|
|
9249
|
+
|
|
9250
|
+
TARGETS
|
|
9251
|
+
./path/to/dir a local directory (tree-digested, read-only)
|
|
9252
|
+
./SKILL.md a single file
|
|
9253
|
+
./.cursor/mcp.json an MCP config
|
|
9254
|
+
npm:<pkg>[@version] an npm package (needs network to pin \u2014 offline: unresolved)
|
|
9255
|
+
github:<owner/repo>[@ref] a git repo (needs network to pin \u2014 offline: unresolved)
|
|
9256
|
+
|
|
9257
|
+
EXIT CODES
|
|
9258
|
+
0 decision SAFE (or resolved read-only preview with no blocking authority)
|
|
9259
|
+
10 decision REVIEW / target not fully pinned / evidence partial
|
|
9260
|
+
20 decision BLOCK or UNKNOWN / unresolved / fail-closed (never a pass)
|
|
9261
|
+
2 usage error / target not found
|
|
9262
|
+
3 malformed preparation document
|
|
9263
|
+
|
|
9264
|
+
SEE ALSO
|
|
9265
|
+
ADR 0035 \u2014 Automated Trust Gateway & Authority Manifest
|
|
9266
|
+
ADR 0036 \u2014 Install Plan & Approval Binding
|
|
9267
|
+
ADR 0037 \u2014 Host Adapter Safety Contract
|
|
9268
|
+
`;
|
|
9269
|
+
}
|
|
9270
|
+
|
|
9271
|
+
// src/run.ts
|
|
9272
|
+
function run(argv, deps) {
|
|
9273
|
+
const args = parseArgs(argv);
|
|
9274
|
+
const cmd = args.command;
|
|
9275
|
+
if (!cmd || cmd === "help" || cmd === "--help" || cmd === "-h") {
|
|
9276
|
+
return helpCommand();
|
|
9277
|
+
}
|
|
9278
|
+
switch (cmd) {
|
|
9279
|
+
case "check":
|
|
9280
|
+
return checkCommand(args, {
|
|
9281
|
+
cwd: deps.cwd,
|
|
9282
|
+
readStdin: deps.readStdin,
|
|
9283
|
+
now: deps.now,
|
|
9284
|
+
generatedAt: deps.generatedAt
|
|
9285
|
+
});
|
|
9286
|
+
case "scan-all":
|
|
9287
|
+
return scanAllCommand(args, {
|
|
9288
|
+
cwd: deps.cwd,
|
|
9289
|
+
now: deps.now,
|
|
9290
|
+
generatedAt: deps.generatedAt
|
|
9291
|
+
});
|
|
9292
|
+
case "scan":
|
|
9293
|
+
return scanCommand(args, {
|
|
9294
|
+
cwd: deps.cwd,
|
|
9295
|
+
readStdin: deps.readStdin,
|
|
9296
|
+
now: deps.now,
|
|
9297
|
+
generatedAt: deps.generatedAt,
|
|
9298
|
+
writeCacheFile: deps.writeCacheFile,
|
|
9299
|
+
online: deps.online,
|
|
9300
|
+
getChangedFilesDiff: deps.getChangedFilesDiff,
|
|
9301
|
+
toolVersion: deps.toolVersion
|
|
9302
|
+
});
|
|
9303
|
+
case "diagnostics":
|
|
9304
|
+
return diagnosticsCommand(args, {
|
|
9305
|
+
cwd: deps.cwd,
|
|
9306
|
+
readStdin: deps.readStdin,
|
|
9307
|
+
now: deps.now,
|
|
9308
|
+
generatedAt: deps.generatedAt,
|
|
9309
|
+
online: deps.online
|
|
9310
|
+
});
|
|
9311
|
+
case "baseline":
|
|
9312
|
+
return baselineCommand(args, {
|
|
9313
|
+
cwd: deps.cwd,
|
|
9314
|
+
readStdin: deps.readStdin,
|
|
9315
|
+
generatedAt: deps.generatedAt,
|
|
9316
|
+
writeBaselineFile: deps.writeCacheFile,
|
|
9317
|
+
online: deps.online
|
|
9318
|
+
});
|
|
9319
|
+
case "verify":
|
|
9320
|
+
return verifyCommand(args, {
|
|
9321
|
+
cwd: deps.cwd,
|
|
9322
|
+
readStdin: deps.readStdin,
|
|
9323
|
+
now: deps.now,
|
|
9324
|
+
generatedAt: deps.generatedAt,
|
|
9325
|
+
writeBaselineFile: deps.writeCacheFile,
|
|
9326
|
+
online: deps.online
|
|
9327
|
+
});
|
|
9328
|
+
case "approve":
|
|
9329
|
+
return approveCommand(args, {
|
|
9330
|
+
cwd: deps.cwd,
|
|
9331
|
+
now: deps.now,
|
|
9332
|
+
generatedAt: deps.generatedAt,
|
|
9333
|
+
writeFile: deps.writeCacheFile
|
|
9334
|
+
});
|
|
9335
|
+
case "explain":
|
|
9336
|
+
return explainCommand(args, { cwd: deps.cwd });
|
|
9337
|
+
case "receipt":
|
|
9338
|
+
return receiptCommand(args, { cwd: deps.cwd });
|
|
9339
|
+
case "action":
|
|
9340
|
+
return actionCommand(args, { cwd: deps.cwd, toolVersion: deps.toolVersion, generatedAt: deps.generatedAt });
|
|
9341
|
+
case "inbox":
|
|
9342
|
+
return inboxCommand(args, { cwd: deps.cwd, toolVersion: deps.toolVersion, generatedAt: deps.generatedAt });
|
|
9343
|
+
case "inventory":
|
|
9344
|
+
return inventoryCommand(args, { cwd: deps.cwd });
|
|
9345
|
+
case "evidence":
|
|
9346
|
+
return evidenceCommand(args, { cwd: deps.cwd });
|
|
9347
|
+
case "trust":
|
|
9348
|
+
return trustCommand(args, { cwd: deps.cwd, generatedAt: deps.generatedAt, toolVersion: deps.toolVersion });
|
|
9349
|
+
case "gen-rule":
|
|
9350
|
+
return genRuleCommand(args, { cwd: deps.cwd });
|
|
9351
|
+
case "policy":
|
|
9352
|
+
return policyCommand(args, { cwd: deps.cwd });
|
|
9353
|
+
default:
|
|
9354
|
+
return {
|
|
9355
|
+
stdout: "",
|
|
9356
|
+
stderr: `Unknown command: ${cmd}
|
|
9357
|
+
Run \`calllint help\`.`,
|
|
9358
|
+
exitCode: 2
|
|
9359
|
+
};
|
|
9360
|
+
}
|
|
9361
|
+
}
|
|
9362
|
+
|
|
9363
|
+
// ../../packages/online/src/npm.ts
|
|
9364
|
+
function isRecord3(v) {
|
|
9365
|
+
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
9366
|
+
}
|
|
9367
|
+
function parseSpec(packageSpec) {
|
|
9368
|
+
if (packageSpec.startsWith("@")) {
|
|
9369
|
+
const slash = packageSpec.indexOf("/");
|
|
9370
|
+
const at2 = packageSpec.indexOf("@", slash);
|
|
9371
|
+
if (at2 === -1) return { name: packageSpec };
|
|
9372
|
+
return { name: packageSpec.slice(0, at2), version: packageSpec.slice(at2 + 1) || void 0 };
|
|
9373
|
+
}
|
|
9374
|
+
const at = packageSpec.indexOf("@");
|
|
9375
|
+
if (at <= 0) return { name: packageSpec };
|
|
9376
|
+
return { name: packageSpec.slice(0, at), version: packageSpec.slice(at + 1) || void 0 };
|
|
9377
|
+
}
|
|
9378
|
+
var INSTALL_SCRIPT_KEYS = ["preinstall", "install", "postinstall"];
|
|
9379
|
+
async function fetchNpmFacts(packageSpec, fetchJson) {
|
|
9380
|
+
const { name, version } = parseSpec(packageSpec);
|
|
9381
|
+
const url = `https://registry.npmjs.org/${name.replace(/\//g, "%2f")}`;
|
|
9382
|
+
let doc;
|
|
9383
|
+
try {
|
|
9384
|
+
doc = await fetchJson(url);
|
|
9385
|
+
} catch {
|
|
9386
|
+
return { name, versionExists: false, installScripts: [] };
|
|
9387
|
+
}
|
|
9388
|
+
if (!isRecord3(doc)) return { name, versionExists: false, installScripts: [] };
|
|
9389
|
+
const distTags = isRecord3(doc["dist-tags"]) ? doc["dist-tags"] : {};
|
|
9390
|
+
const latestVersion = typeof distTags.latest === "string" ? distTags.latest : void 0;
|
|
9391
|
+
const versions = isRecord3(doc.versions) ? doc.versions : {};
|
|
9392
|
+
const isFloating = !version || version === "latest" || /[\^~><*]/.test(version);
|
|
9393
|
+
const resolvedVersion = isFloating ? latestVersion : version;
|
|
9394
|
+
const versionDoc = resolvedVersion && isRecord3(versions[resolvedVersion]) ? versions[resolvedVersion] : void 0;
|
|
9395
|
+
if (!versionDoc) {
|
|
9396
|
+
return { name, versionExists: false, installScripts: [], latestVersion, resolvedVersion };
|
|
9397
|
+
}
|
|
9398
|
+
const scripts = isRecord3(versionDoc.scripts) ? versionDoc.scripts : {};
|
|
9399
|
+
const installScripts = INSTALL_SCRIPT_KEYS.filter(
|
|
9400
|
+
(k) => typeof scripts[k] === "string" && scripts[k].length > 0
|
|
9401
|
+
);
|
|
9402
|
+
const deprecated = typeof versionDoc.deprecated === "string" ? versionDoc.deprecated : void 0;
|
|
9403
|
+
const description = typeof versionDoc.description === "string" ? versionDoc.description : void 0;
|
|
9404
|
+
const readme = typeof doc.readme === "string" ? doc.readme : void 0;
|
|
9405
|
+
return {
|
|
9406
|
+
name,
|
|
9407
|
+
versionExists: true,
|
|
9408
|
+
installScripts,
|
|
9409
|
+
deprecated,
|
|
9410
|
+
latestVersion,
|
|
9411
|
+
resolvedVersion,
|
|
6273
9412
|
description,
|
|
6274
9413
|
readme
|
|
6275
9414
|
};
|
|
@@ -6480,13 +9619,13 @@ async function breathe(argv, deps = {}) {
|
|
|
6480
9619
|
}
|
|
6481
9620
|
|
|
6482
9621
|
// src/version.ts
|
|
6483
|
-
import { readFileSync as
|
|
9622
|
+
import { readFileSync as readFileSync19 } from "node:fs";
|
|
6484
9623
|
import { fileURLToPath } from "node:url";
|
|
6485
|
-
import { dirname as
|
|
9624
|
+
import { dirname as dirname6, join as join17 } from "node:path";
|
|
6486
9625
|
function resolveToolVersion() {
|
|
6487
9626
|
try {
|
|
6488
|
-
const pkgPath =
|
|
6489
|
-
const pkg = JSON.parse(
|
|
9627
|
+
const pkgPath = join17(dirname6(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
9628
|
+
const pkg = JSON.parse(readFileSync19(pkgPath, "utf8"));
|
|
6490
9629
|
return typeof pkg.version === "string" && pkg.version.length > 0 ? pkg.version : "unknown";
|
|
6491
9630
|
} catch {
|
|
6492
9631
|
return "unknown";
|
|
@@ -6496,7 +9635,7 @@ function resolveToolVersion() {
|
|
|
6496
9635
|
// src/index.ts
|
|
6497
9636
|
function readStdin() {
|
|
6498
9637
|
try {
|
|
6499
|
-
return
|
|
9638
|
+
return readFileSync20(0, "utf8");
|
|
6500
9639
|
} catch {
|
|
6501
9640
|
return "";
|
|
6502
9641
|
}
|