calllint 0.4.0 → 0.6.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 +1403 -51
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync8 } from "node:fs";
|
|
5
|
+
import { execFileSync } from "node:child_process";
|
|
5
6
|
|
|
6
7
|
// src/args.ts
|
|
7
8
|
var EXIT = {
|
|
@@ -56,25 +57,39 @@ USAGE
|
|
|
56
57
|
calllint <command> [options]
|
|
57
58
|
|
|
58
59
|
COMMANDS
|
|
59
|
-
|
|
60
|
+
check [target] Compact safety decision for an MCP config or install snippet
|
|
61
|
+
scan-all Scan every agent-tool surface in the repo (compact table)
|
|
62
|
+
explain <server> Explain the verdict for one server from the last scan
|
|
63
|
+
verify [target] Compare a fresh scan against the baseline (drift / rug-pull)
|
|
64
|
+
|
|
65
|
+
Advanced:
|
|
66
|
+
scan [target] Full ScanReport for an MCP config / npm:<pkg> / github:<repo>
|
|
60
67
|
diagnostics [target] Emit editor/agent-host diagnostics JSON (calllint.diagnostics.v0)
|
|
61
68
|
baseline [target] Record the approved risk surface as a baseline
|
|
62
|
-
|
|
63
|
-
|
|
69
|
+
approve Record the repo-wide capability surface as approved state (L4)
|
|
70
|
+
gen-rule --host <h> Emit the CallLint agent-safety rule for a host (CLAUDE.md, etc.)
|
|
64
71
|
policy init Write a default calllint.policy.json
|
|
65
72
|
policy explain Show the effective policy
|
|
66
73
|
help Show this help
|
|
67
74
|
|
|
75
|
+
CHECK OPTIONS
|
|
76
|
+
--stdin Read a config JSON or install snippet from stdin
|
|
77
|
+
--json Emit the compact decision JSON (calllint.decision.v0, <1 KB)
|
|
78
|
+
--explain Show the full evidence-backed report instead of the compact view
|
|
79
|
+
--no-emoji Plain-text symbols (good for CI logs)
|
|
80
|
+
|
|
68
81
|
TARGETS
|
|
69
82
|
<path> A config file (default: detect common locations)
|
|
70
83
|
npm:<pkg>[@ver] Synthesize a config for an npm package (offline)
|
|
71
84
|
github:<owner/repo>[@ref] A GitHub repo (requires --online)
|
|
72
85
|
|
|
73
86
|
SCAN OPTIONS
|
|
87
|
+
--changed Scan only the agent-tool configs changed in the git diff
|
|
74
88
|
--json Emit the ScanReport JSON (stable, emoji-free)
|
|
75
89
|
--compact One line per server
|
|
76
90
|
--no-emoji Plain-text symbols (good for CI logs)
|
|
77
91
|
--sarif Emit SARIF 2.1.0 (GitHub Code Scanning / CI)
|
|
92
|
+
--markdown Emit Markdown for PR comments / GitHub Step Summary
|
|
78
93
|
--html Emit a self-contained HTML report
|
|
79
94
|
--policy <file> Use a policy file (default: built-in defaults)
|
|
80
95
|
--stdin Read config JSON from stdin
|
|
@@ -83,18 +98,20 @@ SCAN OPTIONS
|
|
|
83
98
|
|
|
84
99
|
VERIFY OPTIONS
|
|
85
100
|
--baseline <file> Baseline path (default: .calllint/baseline.json)
|
|
86
|
-
--
|
|
101
|
+
--approved [file] Diff the repo-wide capability surface against approved state
|
|
102
|
+
(default: .calllint/approved.json) instead of the baseline
|
|
103
|
+
--ci Exit 40 if the surface drifted (from baseline or approved state)
|
|
87
104
|
--json Emit the drift report JSON
|
|
88
105
|
|
|
89
106
|
EXAMPLES
|
|
90
|
-
calllint
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
calllint
|
|
94
|
-
calllint
|
|
95
|
-
calllint scan
|
|
96
|
-
calllint baseline ./mcp.json
|
|
107
|
+
calllint check .cursor/mcp.json
|
|
108
|
+
calllint check npm:mcp-weather@1.0.0
|
|
109
|
+
echo "npx -y demo-mcp@1.2.3" | calllint check --stdin
|
|
110
|
+
calllint scan-all --no-emoji
|
|
111
|
+
calllint check ./mcp.json --json
|
|
112
|
+
calllint scan .cursor/mcp.json --markdown
|
|
97
113
|
calllint verify ./mcp.json --ci
|
|
114
|
+
calllint approve && calllint verify --approved --ci
|
|
98
115
|
calllint explain filesystem
|
|
99
116
|
`;
|
|
100
117
|
function helpCommand() {
|
|
@@ -102,7 +119,7 @@ function helpCommand() {
|
|
|
102
119
|
}
|
|
103
120
|
|
|
104
121
|
// src/commands/scan.ts
|
|
105
|
-
import { join as
|
|
122
|
+
import { join as join6, resolve as resolve2 } from "node:path";
|
|
106
123
|
|
|
107
124
|
// ../../packages/policy/src/defaultPolicy.ts
|
|
108
125
|
function defaultPolicy() {
|
|
@@ -168,6 +185,12 @@ function validateOverride(o, index, issues) {
|
|
|
168
185
|
message: "must be a valid ISO timestamp (overrides must expire)"
|
|
169
186
|
});
|
|
170
187
|
}
|
|
188
|
+
if (o.owner !== void 0 && (typeof o.owner !== "string" || !o.owner.trim())) {
|
|
189
|
+
issues.push({
|
|
190
|
+
path: `${base}.owner`,
|
|
191
|
+
message: "must be a non-empty string when present"
|
|
192
|
+
});
|
|
193
|
+
}
|
|
171
194
|
const allow = Array.isArray(o.allow) ? o.allow : [];
|
|
172
195
|
const allowsDangerous = allow.some(
|
|
173
196
|
(s) => typeof s === "string" && DANGEROUS_SYMBOLS.has(s)
|
|
@@ -255,7 +278,7 @@ function applyPolicy(rawVerdict, serverName, blockingFindings, policy, now) {
|
|
|
255
278
|
return {
|
|
256
279
|
verdict: "REVIEW",
|
|
257
280
|
changed: true,
|
|
258
|
-
note: `Policy decision: override for "${serverName}" (expires ${override.expiresAt}) \u2014 ${override.reason}`
|
|
281
|
+
note: `Policy decision: override for "${serverName}" (expires ${override.expiresAt}${override.owner ? `, owner: ${override.owner}` : ""}) \u2014 ${override.reason}`
|
|
259
282
|
};
|
|
260
283
|
}
|
|
261
284
|
function shouldFailCi(verdict, policy) {
|
|
@@ -354,6 +377,100 @@ function highestRiskClass(classes) {
|
|
|
354
377
|
return worst;
|
|
355
378
|
}
|
|
356
379
|
|
|
380
|
+
// ../../packages/types/src/reasonCodes.ts
|
|
381
|
+
var REASON_CODES = [
|
|
382
|
+
"UNPINNED_PACKAGE",
|
|
383
|
+
"UNKNOWN_REMOTE",
|
|
384
|
+
"SECRET_IN_WORKSPACE_CONFIG",
|
|
385
|
+
"BROAD_FILESYSTEM_ACCESS",
|
|
386
|
+
"SHELL_OR_DOCKER_EXECUTION",
|
|
387
|
+
"EXTERNAL_MUTATION_UNKNOWN",
|
|
388
|
+
"MONEY_OR_PAYMENT_CAPABILITY",
|
|
389
|
+
"MESSAGING_OR_EMAIL_SEND",
|
|
390
|
+
"PROMPT_METADATA_INSTRUCTION",
|
|
391
|
+
"OAUTH_SCOPE_UNKNOWN_OR_EXPANDED",
|
|
392
|
+
"TOOL_DESCRIPTOR_CHANGED",
|
|
393
|
+
"LONG_RUNNING_GATEWAY_RUNTIME"
|
|
394
|
+
];
|
|
395
|
+
var REASON_CODE_META = {
|
|
396
|
+
UNPINNED_PACKAGE: {
|
|
397
|
+
backedBy: ["supply.unpinned-package"],
|
|
398
|
+
status: "wired",
|
|
399
|
+
label: "Unpinned package"
|
|
400
|
+
},
|
|
401
|
+
UNKNOWN_REMOTE: {
|
|
402
|
+
backedBy: ["supply.unknown-remote"],
|
|
403
|
+
status: "wired",
|
|
404
|
+
label: "Unknown remote endpoint"
|
|
405
|
+
},
|
|
406
|
+
SECRET_IN_WORKSPACE_CONFIG: {
|
|
407
|
+
backedBy: ["secrets.env-key"],
|
|
408
|
+
status: "wired",
|
|
409
|
+
label: "Secret in workspace config"
|
|
410
|
+
},
|
|
411
|
+
BROAD_FILESYSTEM_ACCESS: {
|
|
412
|
+
backedBy: ["files.broad-path"],
|
|
413
|
+
status: "wired",
|
|
414
|
+
label: "Broad filesystem access"
|
|
415
|
+
},
|
|
416
|
+
SHELL_OR_DOCKER_EXECUTION: {
|
|
417
|
+
backedBy: ["exec.dangerous-command", "exec.unverified-local-source"],
|
|
418
|
+
status: "wired",
|
|
419
|
+
label: "Shell or Docker execution"
|
|
420
|
+
},
|
|
421
|
+
EXTERNAL_MUTATION_UNKNOWN: {
|
|
422
|
+
backedBy: ["action.external-mutation"],
|
|
423
|
+
status: "wired",
|
|
424
|
+
label: "Unknown external mutation"
|
|
425
|
+
},
|
|
426
|
+
MONEY_OR_PAYMENT_CAPABILITY: {
|
|
427
|
+
backedBy: ["action.financial", "action.financial-observed"],
|
|
428
|
+
status: "wired",
|
|
429
|
+
label: "Money or payment capability"
|
|
430
|
+
},
|
|
431
|
+
MESSAGING_OR_EMAIL_SEND: {
|
|
432
|
+
backedBy: ["action.messaging-send"],
|
|
433
|
+
status: "wired",
|
|
434
|
+
label: "Messaging or email send"
|
|
435
|
+
},
|
|
436
|
+
PROMPT_METADATA_INSTRUCTION: {
|
|
437
|
+
backedBy: ["prompt.hidden-instructions", "prompt.poisoning"],
|
|
438
|
+
status: "wired",
|
|
439
|
+
label: "Prompt-metadata instruction"
|
|
440
|
+
},
|
|
441
|
+
OAUTH_SCOPE_UNKNOWN_OR_EXPANDED: {
|
|
442
|
+
backedBy: ["auth.oauth-scope"],
|
|
443
|
+
status: "wired",
|
|
444
|
+
label: "OAuth scope unknown or expanded"
|
|
445
|
+
},
|
|
446
|
+
TOOL_DESCRIPTOR_CHANGED: {
|
|
447
|
+
// Backed by the drift signal (toolMetadataHash change), not a static detector.
|
|
448
|
+
// Wired via the verify/drift path in P1/P4, not findingsToReasonCodes.
|
|
449
|
+
backedBy: ["drift:toolMetadataHash"],
|
|
450
|
+
status: "wired",
|
|
451
|
+
label: "Tool descriptor changed"
|
|
452
|
+
},
|
|
453
|
+
LONG_RUNNING_GATEWAY_RUNTIME: {
|
|
454
|
+
backedBy: ["runtime.gateway"],
|
|
455
|
+
status: "wired",
|
|
456
|
+
label: "Long-running gateway runtime"
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
function reasonCodeForFinding(findingId) {
|
|
460
|
+
for (const code of REASON_CODES) {
|
|
461
|
+
if (REASON_CODE_META[code].backedBy.includes(findingId)) return code;
|
|
462
|
+
}
|
|
463
|
+
return void 0;
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ../../packages/types/src/decision.ts
|
|
467
|
+
var VERDICT_NEXT_ACTION = {
|
|
468
|
+
SAFE: "continue",
|
|
469
|
+
REVIEW: "ask_before_continue",
|
|
470
|
+
BLOCK: "stop",
|
|
471
|
+
UNKNOWN: "gather_more_evidence"
|
|
472
|
+
};
|
|
473
|
+
|
|
357
474
|
// ../../packages/resolver/src/npmSpec.ts
|
|
358
475
|
var PACKAGE_RUNNERS = /* @__PURE__ */ new Set(["npx", "pnpm", "yarn", "bunx", "uvx", "pipx"]);
|
|
359
476
|
var SHELL_COMMANDS = /* @__PURE__ */ new Set([
|
|
@@ -1207,6 +1324,177 @@ function detectHiddenInstructions(ctx) {
|
|
|
1207
1324
|
];
|
|
1208
1325
|
}
|
|
1209
1326
|
|
|
1327
|
+
// ../../packages/static-analyzer/src/detectors/messagingSend.ts
|
|
1328
|
+
var MESSAGING_PACKAGE_HINTS = [
|
|
1329
|
+
"slack",
|
|
1330
|
+
"discord",
|
|
1331
|
+
"telegram",
|
|
1332
|
+
"sendgrid",
|
|
1333
|
+
"twilio",
|
|
1334
|
+
"mailgun",
|
|
1335
|
+
"postmark",
|
|
1336
|
+
"gmail",
|
|
1337
|
+
"smtp",
|
|
1338
|
+
"nodemailer",
|
|
1339
|
+
"mailchimp",
|
|
1340
|
+
"ses-email",
|
|
1341
|
+
"whatsapp",
|
|
1342
|
+
"messagebird",
|
|
1343
|
+
"vonage",
|
|
1344
|
+
"nexmo"
|
|
1345
|
+
];
|
|
1346
|
+
var SEND_PATTERNS = [
|
|
1347
|
+
/\bsend[_-]?(e?mail|message|sms|text|dm|notification|push)\b/i,
|
|
1348
|
+
/\bpost[_-]?message\b/i,
|
|
1349
|
+
/\bsend[_-]?mail\b/i,
|
|
1350
|
+
/\b(e?mail|sms|message)[_-]?send\b/i,
|
|
1351
|
+
/\bdeliver[_-]?(message|email)\b/i
|
|
1352
|
+
];
|
|
1353
|
+
function packageHint(name) {
|
|
1354
|
+
if (!name) return void 0;
|
|
1355
|
+
const lower = name.toLowerCase();
|
|
1356
|
+
return MESSAGING_PACKAGE_HINTS.find((h) => lower.includes(h));
|
|
1357
|
+
}
|
|
1358
|
+
function textMatchesSend(text) {
|
|
1359
|
+
if (!text) return false;
|
|
1360
|
+
return SEND_PATTERNS.some((re) => re.test(text));
|
|
1361
|
+
}
|
|
1362
|
+
function detectMessagingSend(ctx) {
|
|
1363
|
+
const { server, binding } = ctx;
|
|
1364
|
+
const evidence = [];
|
|
1365
|
+
let observed = false;
|
|
1366
|
+
for (const tool of server.providedTools) {
|
|
1367
|
+
if (textMatchesSend(tool.name) || textMatchesSend(tool.description)) {
|
|
1368
|
+
observed = true;
|
|
1369
|
+
evidence.push({ type: "tool-metadata", key: "tool", value: tool.name ?? "(unnamed)" });
|
|
1370
|
+
}
|
|
1371
|
+
}
|
|
1372
|
+
const pkgHint = packageHint(binding.packageName);
|
|
1373
|
+
if (pkgHint) {
|
|
1374
|
+
evidence.push({ type: "runtime-binding", key: "package", value: binding.packageName });
|
|
1375
|
+
}
|
|
1376
|
+
if (evidence.length === 0) return [];
|
|
1377
|
+
return [
|
|
1378
|
+
{
|
|
1379
|
+
id: "action.messaging-send",
|
|
1380
|
+
title: "May send messages or email on your behalf",
|
|
1381
|
+
severity: "medium",
|
|
1382
|
+
blocker: false,
|
|
1383
|
+
symbol: "ACTION",
|
|
1384
|
+
riskClass: "S2",
|
|
1385
|
+
mode: observed ? "OBSERVED" : "INFERRED",
|
|
1386
|
+
confidence: observed ? "high" : "low",
|
|
1387
|
+
detectionMethod: observed ? "tool-metadata" : "package-metadata",
|
|
1388
|
+
evidence,
|
|
1389
|
+
impact: "The server appears able to send messages (email, SMS, chat, or push) as you. A compromised or confused agent could send unwanted or harmful messages.",
|
|
1390
|
+
fix: "Confirm which send tools are exposed, require approval before sending, and scope credentials to the minimum needed.",
|
|
1391
|
+
falsePositiveNote: observed ? void 0 : "Name-based inference; a read-only integration with the same provider would not actually send."
|
|
1392
|
+
}
|
|
1393
|
+
];
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1396
|
+
// ../../packages/static-analyzer/src/detectors/oauthScope.ts
|
|
1397
|
+
var BROAD_SCOPE_HINTS = [
|
|
1398
|
+
"*",
|
|
1399
|
+
"admin",
|
|
1400
|
+
"full_access",
|
|
1401
|
+
"fullaccess",
|
|
1402
|
+
"read_write_all",
|
|
1403
|
+
"offline_access",
|
|
1404
|
+
"all",
|
|
1405
|
+
"write:org",
|
|
1406
|
+
"repo"
|
|
1407
|
+
];
|
|
1408
|
+
function readOauth(raw) {
|
|
1409
|
+
if (typeof raw !== "object" || raw === null) return void 0;
|
|
1410
|
+
const oauth = raw.oauth;
|
|
1411
|
+
if (typeof oauth !== "object" || oauth === null) return void 0;
|
|
1412
|
+
const scopesRaw = oauth.scopes;
|
|
1413
|
+
const scopes = Array.isArray(scopesRaw) ? scopesRaw.filter((s) => typeof s === "string") : void 0;
|
|
1414
|
+
return { scopes };
|
|
1415
|
+
}
|
|
1416
|
+
function broadScope(scopes) {
|
|
1417
|
+
for (const s of scopes) {
|
|
1418
|
+
const lower = s.toLowerCase();
|
|
1419
|
+
if (BROAD_SCOPE_HINTS.some((h) => lower === h || lower.includes(h))) return s;
|
|
1420
|
+
}
|
|
1421
|
+
return void 0;
|
|
1422
|
+
}
|
|
1423
|
+
function detectOauthScope(ctx) {
|
|
1424
|
+
const oauth = readOauth(ctx.server.raw);
|
|
1425
|
+
if (!oauth) return [];
|
|
1426
|
+
const evidence = [{ type: "config", key: "oauth", value: "present" }];
|
|
1427
|
+
if (!oauth.scopes || oauth.scopes.length === 0) {
|
|
1428
|
+
return [
|
|
1429
|
+
{
|
|
1430
|
+
id: "auth.oauth-scope",
|
|
1431
|
+
title: "OAuth scope is not declared",
|
|
1432
|
+
severity: "medium",
|
|
1433
|
+
blocker: false,
|
|
1434
|
+
symbol: "ACTION",
|
|
1435
|
+
riskClass: "S2",
|
|
1436
|
+
mode: "INFERRED",
|
|
1437
|
+
confidence: "low",
|
|
1438
|
+
detectionMethod: "config-analysis",
|
|
1439
|
+
evidence,
|
|
1440
|
+
impact: "This server authenticates via OAuth but does not declare its scopes, so the powers it grants the agent are unknown.",
|
|
1441
|
+
fix: "Declare the exact OAuth scopes the server requests and confirm they are the minimum required.",
|
|
1442
|
+
falsePositiveNote: "Scope may be declared elsewhere; CallLint only sees the provided config."
|
|
1443
|
+
}
|
|
1444
|
+
];
|
|
1445
|
+
}
|
|
1446
|
+
const broad = broadScope(oauth.scopes);
|
|
1447
|
+
if (broad) {
|
|
1448
|
+
evidence.push({ type: "config", key: "scope", value: broad });
|
|
1449
|
+
return [
|
|
1450
|
+
{
|
|
1451
|
+
id: "auth.oauth-scope",
|
|
1452
|
+
title: "OAuth scope is broad or expansive",
|
|
1453
|
+
severity: "high",
|
|
1454
|
+
blocker: false,
|
|
1455
|
+
symbol: "ACTION",
|
|
1456
|
+
riskClass: "S2",
|
|
1457
|
+
mode: "OBSERVED",
|
|
1458
|
+
confidence: "high",
|
|
1459
|
+
detectionMethod: "config-analysis",
|
|
1460
|
+
evidence,
|
|
1461
|
+
impact: `The server requests a broad OAuth scope (\`${broad}\`), granting the agent wide access to the connected account.`,
|
|
1462
|
+
fix: "Narrow the OAuth scopes to only what the exposed tools need."
|
|
1463
|
+
}
|
|
1464
|
+
];
|
|
1465
|
+
}
|
|
1466
|
+
return [];
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
// ../../packages/static-analyzer/src/detectors/gatewayRuntime.ts
|
|
1470
|
+
function gatewayName(raw) {
|
|
1471
|
+
if (typeof raw !== "object" || raw === null) return void 0;
|
|
1472
|
+
const g = raw.gateway;
|
|
1473
|
+
return typeof g === "string" ? g : void 0;
|
|
1474
|
+
}
|
|
1475
|
+
function detectGatewayRuntime(ctx) {
|
|
1476
|
+
const gateway = gatewayName(ctx.server.raw);
|
|
1477
|
+
if (!gateway) return [];
|
|
1478
|
+
const evidence = [{ type: "config", key: "gateway", value: gateway }];
|
|
1479
|
+
return [
|
|
1480
|
+
{
|
|
1481
|
+
id: "runtime.gateway",
|
|
1482
|
+
title: "Long-running gateway runtime",
|
|
1483
|
+
severity: "medium",
|
|
1484
|
+
blocker: false,
|
|
1485
|
+
symbol: "ACTION",
|
|
1486
|
+
riskClass: "S2",
|
|
1487
|
+
mode: "OBSERVED",
|
|
1488
|
+
confidence: "high",
|
|
1489
|
+
detectionMethod: "config-analysis",
|
|
1490
|
+
evidence,
|
|
1491
|
+
impact: "This is a gateway runtime that proxies and re-exposes many downstream tools under its own process and auth. Its tool surface can change after you approve it.",
|
|
1492
|
+
fix: "Treat the gateway as a standing capability: pin its version, review which downstream tools it exposes, and re-verify after upgrades.",
|
|
1493
|
+
falsePositiveNote: "A gateway you fully control and monitor may be acceptable; the verdict reflects standing capability, not a specific exploit."
|
|
1494
|
+
}
|
|
1495
|
+
];
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1210
1498
|
// ../../packages/static-analyzer/src/analyzeServerConfig.ts
|
|
1211
1499
|
var DETECTORS = [
|
|
1212
1500
|
detectBroadFilesystemPath,
|
|
@@ -1218,7 +1506,10 @@ var DETECTORS = [
|
|
|
1218
1506
|
detectExternalMutation,
|
|
1219
1507
|
detectFinancialAction,
|
|
1220
1508
|
detectUnverifiedLocalSource,
|
|
1221
|
-
detectHiddenInstructions
|
|
1509
|
+
detectHiddenInstructions,
|
|
1510
|
+
detectMessagingSend,
|
|
1511
|
+
detectOauthScope,
|
|
1512
|
+
detectGatewayRuntime
|
|
1222
1513
|
];
|
|
1223
1514
|
function analyzeServerConfig(server) {
|
|
1224
1515
|
const binding = resolveRuntimeBinding(server);
|
|
@@ -2040,6 +2331,617 @@ function readBaseline(path = defaultBaselinePath()) {
|
|
|
2040
2331
|
}
|
|
2041
2332
|
}
|
|
2042
2333
|
|
|
2334
|
+
// ../../packages/core/src/extract/fingerprint.ts
|
|
2335
|
+
function deriveLaunch(binding) {
|
|
2336
|
+
switch (binding.runtimeKind) {
|
|
2337
|
+
case "npx":
|
|
2338
|
+
return "local:npx";
|
|
2339
|
+
case "uvx":
|
|
2340
|
+
return "local:uvx";
|
|
2341
|
+
case "node":
|
|
2342
|
+
return "local:node";
|
|
2343
|
+
case "python":
|
|
2344
|
+
return "local:python";
|
|
2345
|
+
case "docker":
|
|
2346
|
+
return "local:docker";
|
|
2347
|
+
case "http":
|
|
2348
|
+
return "remote:http";
|
|
2349
|
+
case "sse":
|
|
2350
|
+
return "remote:sse";
|
|
2351
|
+
default:
|
|
2352
|
+
return "unknown";
|
|
2353
|
+
}
|
|
2354
|
+
}
|
|
2355
|
+
function deriveTransport(binding) {
|
|
2356
|
+
switch (binding.transport) {
|
|
2357
|
+
case "stdio":
|
|
2358
|
+
return "stdio";
|
|
2359
|
+
case "http":
|
|
2360
|
+
return "http";
|
|
2361
|
+
case "sse":
|
|
2362
|
+
return "sse";
|
|
2363
|
+
default:
|
|
2364
|
+
return "unknown";
|
|
2365
|
+
}
|
|
2366
|
+
}
|
|
2367
|
+
function deriveSource(binding) {
|
|
2368
|
+
if (binding.packageName) {
|
|
2369
|
+
const spec = binding.packageVersionSpec ? `${binding.packageName}@${binding.packageVersionSpec}` : binding.packageName;
|
|
2370
|
+
return `npm:${spec}`;
|
|
2371
|
+
}
|
|
2372
|
+
if (binding.remoteUrl) {
|
|
2373
|
+
try {
|
|
2374
|
+
const u = new URL(binding.remoteUrl);
|
|
2375
|
+
return `url:${u.host}${u.pathname}`.replace(/\/$/, "");
|
|
2376
|
+
} catch {
|
|
2377
|
+
return `url:${binding.remoteUrl}`;
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
if (binding.declaredCommand) {
|
|
2381
|
+
return `local:${binding.declaredCommand}`;
|
|
2382
|
+
}
|
|
2383
|
+
return "unknown";
|
|
2384
|
+
}
|
|
2385
|
+
function deriveScope(origin, launch) {
|
|
2386
|
+
if (origin === "workspace") return "workspace";
|
|
2387
|
+
if (origin === "user") return "user";
|
|
2388
|
+
if (origin === "system") return "system";
|
|
2389
|
+
if (origin === "remote") return "external";
|
|
2390
|
+
if (launch === "remote:http" || launch === "remote:sse") return "external";
|
|
2391
|
+
return "unknown";
|
|
2392
|
+
}
|
|
2393
|
+
function deriveEffects(findings, server, kind) {
|
|
2394
|
+
const effects = /* @__PURE__ */ new Set();
|
|
2395
|
+
for (const f of findings) {
|
|
2396
|
+
switch (f.symbol) {
|
|
2397
|
+
case "FILES":
|
|
2398
|
+
effects.add("filesystem_broad");
|
|
2399
|
+
break;
|
|
2400
|
+
case "EXEC":
|
|
2401
|
+
effects.add("local_execution");
|
|
2402
|
+
break;
|
|
2403
|
+
case "NETWORK":
|
|
2404
|
+
effects.add("network_egress");
|
|
2405
|
+
break;
|
|
2406
|
+
case "ACTION":
|
|
2407
|
+
effects.add("external_mutation_unknown");
|
|
2408
|
+
break;
|
|
2409
|
+
case "MONEY":
|
|
2410
|
+
effects.add("payment");
|
|
2411
|
+
break;
|
|
2412
|
+
case "PROMPT":
|
|
2413
|
+
effects.add("prompt_instruction");
|
|
2414
|
+
break;
|
|
2415
|
+
default:
|
|
2416
|
+
break;
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
if (kind === "gateway_runtime") effects.add("gateway_runtime");
|
|
2420
|
+
if (hasOauthMetadata(server)) effects.add("oauth_scope");
|
|
2421
|
+
return [...effects].sort();
|
|
2422
|
+
}
|
|
2423
|
+
function hasOauthMetadata(server) {
|
|
2424
|
+
const raw = server.raw;
|
|
2425
|
+
return typeof raw === "object" && raw !== null && "oauth" in raw;
|
|
2426
|
+
}
|
|
2427
|
+
function deriveIdentity(binding) {
|
|
2428
|
+
return binding.sourceKnown ? "known" : "unknown";
|
|
2429
|
+
}
|
|
2430
|
+
function buildFingerprint(input) {
|
|
2431
|
+
const { server, binding, findings, origin, kind } = input;
|
|
2432
|
+
const launch = deriveLaunch(binding);
|
|
2433
|
+
const resolvedKind = kind ?? "mcp_server";
|
|
2434
|
+
return {
|
|
2435
|
+
schemaVersion: "calllint.fingerprint.v0",
|
|
2436
|
+
kind: resolvedKind,
|
|
2437
|
+
source: deriveSource(binding),
|
|
2438
|
+
launch,
|
|
2439
|
+
transport: deriveTransport(binding),
|
|
2440
|
+
// Env KEY NAMES only — never values (secret redaction by construction).
|
|
2441
|
+
authority: [...new Set(server.envKeys)].sort().map((k) => `env:${k}`),
|
|
2442
|
+
scope: deriveScope(origin, launch),
|
|
2443
|
+
effects: deriveEffects(findings, server, resolvedKind),
|
|
2444
|
+
identity: deriveIdentity(binding)
|
|
2445
|
+
};
|
|
2446
|
+
}
|
|
2447
|
+
function fingerprintHash(fp) {
|
|
2448
|
+
const { schemaVersion: _schemaVersion, ...semantic } = fp;
|
|
2449
|
+
const normalized = {
|
|
2450
|
+
...semantic,
|
|
2451
|
+
authority: [...new Set(fp.authority)].sort(),
|
|
2452
|
+
effects: [...new Set(fp.effects)].sort()
|
|
2453
|
+
};
|
|
2454
|
+
return hashJson(normalized);
|
|
2455
|
+
}
|
|
2456
|
+
|
|
2457
|
+
// ../../packages/core/src/rules/reasonCodes.ts
|
|
2458
|
+
function findingsToReasonCodes(findings) {
|
|
2459
|
+
const present = /* @__PURE__ */ new Set();
|
|
2460
|
+
for (const f of findings) {
|
|
2461
|
+
const code = reasonCodeForFinding(f.id);
|
|
2462
|
+
if (code) present.add(code);
|
|
2463
|
+
}
|
|
2464
|
+
return REASON_CODES.filter((c) => present.has(c));
|
|
2465
|
+
}
|
|
2466
|
+
|
|
2467
|
+
// ../../packages/core/src/rules/sparseRules.ts
|
|
2468
|
+
function sparseDecision(verdict, findings) {
|
|
2469
|
+
return {
|
|
2470
|
+
verdict,
|
|
2471
|
+
reasonCodes: findingsToReasonCodes(findings),
|
|
2472
|
+
nextAction: VERDICT_NEXT_ACTION[verdict]
|
|
2473
|
+
};
|
|
2474
|
+
}
|
|
2475
|
+
|
|
2476
|
+
// ../../packages/core/src/decision/decide.ts
|
|
2477
|
+
function toCompactDecision(report, surface, fingerprint) {
|
|
2478
|
+
const fp = fingerprint ?? report.fingerprint;
|
|
2479
|
+
const hash = fp ? fingerprintHash(fp) : "";
|
|
2480
|
+
const sparse = sparseDecision(report.verdict, report.findings);
|
|
2481
|
+
return {
|
|
2482
|
+
schemaVersion: "calllint.decision.v0",
|
|
2483
|
+
verdict: sparse.verdict,
|
|
2484
|
+
surface,
|
|
2485
|
+
fingerprintHash: hash,
|
|
2486
|
+
reasonCodes: sparse.reasonCodes,
|
|
2487
|
+
nextAction: sparse.nextAction
|
|
2488
|
+
};
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
// ../../packages/core/src/decision/checkParsed.ts
|
|
2492
|
+
function checkParsed(parsed, surface, origin, opts) {
|
|
2493
|
+
resolveScanOptions(opts);
|
|
2494
|
+
return parsed.servers.map((server) => {
|
|
2495
|
+
const binding = resolveRuntimeBinding(server);
|
|
2496
|
+
const findings = analyzeServerConfig(server);
|
|
2497
|
+
const report = scanServer({ server, targetKind: parsed.kind }, opts);
|
|
2498
|
+
const fingerprint = buildFingerprint({ server, binding, findings, origin });
|
|
2499
|
+
const decision = toCompactDecision(report, surface, fingerprint);
|
|
2500
|
+
return { decision: { ...decision }, report: { ...report, fingerprint, decision } };
|
|
2501
|
+
});
|
|
2502
|
+
}
|
|
2503
|
+
|
|
2504
|
+
// ../../packages/core/src/surface/detect.ts
|
|
2505
|
+
var MCP_CONFIG_BASENAMES = /* @__PURE__ */ new Set([
|
|
2506
|
+
".mcp.json",
|
|
2507
|
+
"mcp.json",
|
|
2508
|
+
"claude_desktop_config.json",
|
|
2509
|
+
"mcp_config.json"
|
|
2510
|
+
]);
|
|
2511
|
+
var MCP_PATH_HINTS = [
|
|
2512
|
+
"/.cursor/mcp.json",
|
|
2513
|
+
"/.vscode/mcp.json",
|
|
2514
|
+
"\\.cursor\\mcp.json",
|
|
2515
|
+
"\\.vscode\\mcp.json"
|
|
2516
|
+
];
|
|
2517
|
+
var SNIPPET_MARKERS = [
|
|
2518
|
+
/\bclaude\s+mcp\s+add\b/,
|
|
2519
|
+
/\bnpx\s+-y?\b/,
|
|
2520
|
+
/\buvx\b/,
|
|
2521
|
+
/\bbunx\b/,
|
|
2522
|
+
/\bdocker\s+run\b/,
|
|
2523
|
+
/\bopenclaw\s+mcp\b/
|
|
2524
|
+
];
|
|
2525
|
+
function normalize(path) {
|
|
2526
|
+
return path.replace(/\\/g, "/").toLowerCase();
|
|
2527
|
+
}
|
|
2528
|
+
function basename2(path) {
|
|
2529
|
+
const n = normalize(path);
|
|
2530
|
+
const i = n.lastIndexOf("/");
|
|
2531
|
+
return i === -1 ? n : n.slice(i + 1);
|
|
2532
|
+
}
|
|
2533
|
+
function classifySurface(path, content) {
|
|
2534
|
+
const n = normalize(path);
|
|
2535
|
+
if (n.includes("/node_modules/") || n.startsWith("node_modules/")) return "NOOP";
|
|
2536
|
+
if (/(^|\/)(package-lock\.json|pnpm-lock\.yaml|yarn\.lock)$/.test(n)) return "NOOP";
|
|
2537
|
+
const base = basename2(path);
|
|
2538
|
+
if (MCP_CONFIG_BASENAMES.has(base)) return "SCAN";
|
|
2539
|
+
if (MCP_PATH_HINTS.some((h) => normalize(path).includes(normalize(h)))) return "SCAN";
|
|
2540
|
+
if (base === "config.toml" && content && /\[mcp_servers?\.[^\]]+\]/.test(content)) {
|
|
2541
|
+
return "SCAN";
|
|
2542
|
+
}
|
|
2543
|
+
if (base === "settings.json" && content && /"mcpServers"\s*:/.test(content)) {
|
|
2544
|
+
return "SCAN";
|
|
2545
|
+
}
|
|
2546
|
+
if (/\.(md|markdown)$/.test(n) && content && SNIPPET_MARKERS.some((r) => r.test(content))) {
|
|
2547
|
+
return "SCAN";
|
|
2548
|
+
}
|
|
2549
|
+
if (/\/\.github\/workflows\/[^/]+\.ya?ml$/.test(n) && content && SNIPPET_MARKERS.some((r) => r.test(content))) {
|
|
2550
|
+
return "SCAN";
|
|
2551
|
+
}
|
|
2552
|
+
if ((path === "-" || path === "" || base === "stdin") && content && SNIPPET_MARKERS.some((r) => r.test(content))) {
|
|
2553
|
+
return "SCAN";
|
|
2554
|
+
}
|
|
2555
|
+
return "NOOP";
|
|
2556
|
+
}
|
|
2557
|
+
|
|
2558
|
+
// ../../packages/core/src/surface/load.ts
|
|
2559
|
+
function inferOrigin(path) {
|
|
2560
|
+
const n = path.replace(/\\/g, "/").toLowerCase();
|
|
2561
|
+
if (n.startsWith("npm:") || n.startsWith("http://") || n.startsWith("https://")) {
|
|
2562
|
+
return "remote";
|
|
2563
|
+
}
|
|
2564
|
+
if (n.includes("/library/application support/") || // macOS
|
|
2565
|
+
n.includes("/.config/") || n.includes("/appdata/") || // Windows
|
|
2566
|
+
n.includes("/users/") && n.includes("/.cursor/") === false && n.includes("/.vscode/") === false) {
|
|
2567
|
+
if (!n.includes("/.cursor/") && !n.includes("/.vscode/") && !n.startsWith("./")) {
|
|
2568
|
+
return "user";
|
|
2569
|
+
}
|
|
2570
|
+
}
|
|
2571
|
+
if (n.includes("/.cursor/") || n.includes("/.vscode/") || n.includes("/.github/") || n.startsWith(".cursor/") || n.startsWith(".vscode/") || n.startsWith(".github/") || n === ".mcp.json" || n.startsWith("./")) {
|
|
2572
|
+
return "workspace";
|
|
2573
|
+
}
|
|
2574
|
+
return "unknown";
|
|
2575
|
+
}
|
|
2576
|
+
function loadSurfaceText(text, configPath = "<inline>") {
|
|
2577
|
+
return { parsed: parseConfigText(text, configPath), origin: inferOrigin(configPath) };
|
|
2578
|
+
}
|
|
2579
|
+
|
|
2580
|
+
// ../../packages/core/src/surface/snippet.ts
|
|
2581
|
+
var RUNNER_RE = /\b(?:npx|uvx|bunx|pnpm dlx)\s+(?:-y\s+|--yes\s+)?(@?[\w./-]+(?:@[\w.^~>=<-]+)?)/;
|
|
2582
|
+
var CLAUDE_ADD_RE = /\bclaude\s+mcp\s+add\b.*?--\s+(.+)$/;
|
|
2583
|
+
function extractPackageSpec(text) {
|
|
2584
|
+
const trimmed = text.trim();
|
|
2585
|
+
const claude = CLAUDE_ADD_RE.exec(trimmed);
|
|
2586
|
+
if (claude) {
|
|
2587
|
+
const inner = extractPackageSpec(claude[1]);
|
|
2588
|
+
if (inner) return inner;
|
|
2589
|
+
}
|
|
2590
|
+
const runner = RUNNER_RE.exec(trimmed);
|
|
2591
|
+
if (runner) return runner[1];
|
|
2592
|
+
if (/^@?[\w./-]+(@[\w.^~>=<-]+)?$/.test(trimmed)) return trimmed;
|
|
2593
|
+
return void 0;
|
|
2594
|
+
}
|
|
2595
|
+
function parseSnippet(text) {
|
|
2596
|
+
const spec = extractPackageSpec(text);
|
|
2597
|
+
if (!spec) {
|
|
2598
|
+
throw new Error("No agent-tool package recognized in snippet");
|
|
2599
|
+
}
|
|
2600
|
+
const { text: configText, configPath } = synthesizeNpmConfig(spec);
|
|
2601
|
+
return { parsed: parseConfigText(configText, configPath), packageSpec: spec };
|
|
2602
|
+
}
|
|
2603
|
+
|
|
2604
|
+
// ../../packages/core/src/surface/walk.ts
|
|
2605
|
+
import { readdirSync, readFileSync as readFileSync3, statSync } from "node:fs";
|
|
2606
|
+
import { join as join2, relative } from "node:path";
|
|
2607
|
+
var SKIP_DIRS = /* @__PURE__ */ new Set([
|
|
2608
|
+
"node_modules",
|
|
2609
|
+
".git",
|
|
2610
|
+
"dist",
|
|
2611
|
+
"build",
|
|
2612
|
+
".next",
|
|
2613
|
+
"coverage"
|
|
2614
|
+
]);
|
|
2615
|
+
var MAX_DEPTH = 6;
|
|
2616
|
+
var MAX_FILE_BYTES = 256 * 1024;
|
|
2617
|
+
function findSurfaces(root) {
|
|
2618
|
+
const found = [];
|
|
2619
|
+
function walk(dir, depth) {
|
|
2620
|
+
if (depth > MAX_DEPTH) return;
|
|
2621
|
+
let entries;
|
|
2622
|
+
try {
|
|
2623
|
+
entries = readdirSync(dir, { withFileTypes: true, encoding: "utf8" });
|
|
2624
|
+
} catch {
|
|
2625
|
+
return;
|
|
2626
|
+
}
|
|
2627
|
+
for (const e of entries) {
|
|
2628
|
+
const full = join2(dir, e.name);
|
|
2629
|
+
if (e.isDirectory()) {
|
|
2630
|
+
if (SKIP_DIRS.has(e.name)) continue;
|
|
2631
|
+
walk(full, depth + 1);
|
|
2632
|
+
} else if (e.isFile()) {
|
|
2633
|
+
if (classifySurface(full) === "SCAN") {
|
|
2634
|
+
found.push(full);
|
|
2635
|
+
continue;
|
|
2636
|
+
}
|
|
2637
|
+
if (needsContentCheck(e.name)) {
|
|
2638
|
+
const content = readCapped(full);
|
|
2639
|
+
if (content && classifySurface(full, content) === "SCAN") found.push(full);
|
|
2640
|
+
}
|
|
2641
|
+
}
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
walk(root, 0);
|
|
2645
|
+
return found;
|
|
2646
|
+
}
|
|
2647
|
+
function needsContentCheck(name) {
|
|
2648
|
+
return name === "config.toml" || name === "settings.json" || /\.(md|markdown|ya?ml)$/.test(name);
|
|
2649
|
+
}
|
|
2650
|
+
function readCapped(path) {
|
|
2651
|
+
try {
|
|
2652
|
+
const st = statSync(path);
|
|
2653
|
+
if (!st.isFile() || st.size > MAX_FILE_BYTES) return void 0;
|
|
2654
|
+
return readFileSync3(path, "utf8");
|
|
2655
|
+
} catch {
|
|
2656
|
+
return void 0;
|
|
2657
|
+
}
|
|
2658
|
+
}
|
|
2659
|
+
function unknownDecision(surface) {
|
|
2660
|
+
return {
|
|
2661
|
+
schemaVersion: "calllint.decision.v0",
|
|
2662
|
+
verdict: "UNKNOWN",
|
|
2663
|
+
surface,
|
|
2664
|
+
fingerprintHash: "",
|
|
2665
|
+
reasonCodes: [],
|
|
2666
|
+
nextAction: "gather_more_evidence"
|
|
2667
|
+
};
|
|
2668
|
+
}
|
|
2669
|
+
function decideRepoSurfaces(root, opts) {
|
|
2670
|
+
const surfaces = findSurfaces(root);
|
|
2671
|
+
const decisions = [];
|
|
2672
|
+
for (const abs of surfaces) {
|
|
2673
|
+
const rel = relative(root, abs) || abs;
|
|
2674
|
+
const text = readCapped(abs);
|
|
2675
|
+
if (!text) continue;
|
|
2676
|
+
try {
|
|
2677
|
+
const loaded = loadSurfaceText(text, rel);
|
|
2678
|
+
const results = checkParsed(loaded.parsed, rel, inferOrigin(rel), opts);
|
|
2679
|
+
for (const r of results) decisions.push(r.decision);
|
|
2680
|
+
} catch (e) {
|
|
2681
|
+
if (e instanceof ConfigParseError) {
|
|
2682
|
+
decisions.push(unknownDecision(rel));
|
|
2683
|
+
continue;
|
|
2684
|
+
}
|
|
2685
|
+
throw e;
|
|
2686
|
+
}
|
|
2687
|
+
}
|
|
2688
|
+
return decisions;
|
|
2689
|
+
}
|
|
2690
|
+
|
|
2691
|
+
// ../../packages/core/src/state/approve.ts
|
|
2692
|
+
import { writeFileSync as writeFileSync2, readFileSync as readFileSync4, mkdirSync as mkdirSync2, existsSync as existsSync2 } from "node:fs";
|
|
2693
|
+
import { dirname as dirname2, join as join3 } from "node:path";
|
|
2694
|
+
function defaultApprovedPath(cwd = process.cwd()) {
|
|
2695
|
+
return join3(cwd, ".calllint", "approved.json");
|
|
2696
|
+
}
|
|
2697
|
+
function entryFromDecision(d, approvedAt) {
|
|
2698
|
+
const entry = {
|
|
2699
|
+
fingerprintHash: d.fingerprintHash,
|
|
2700
|
+
surface: d.surface,
|
|
2701
|
+
verdict: d.verdict,
|
|
2702
|
+
approvedAt
|
|
2703
|
+
};
|
|
2704
|
+
if (d.reasonCodes.length > 0) entry.reasonCodes = [...d.reasonCodes];
|
|
2705
|
+
return entry;
|
|
2706
|
+
}
|
|
2707
|
+
function buildApproved(decisions, approvedAt) {
|
|
2708
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2709
|
+
const approved = [];
|
|
2710
|
+
for (const d of decisions) {
|
|
2711
|
+
const key = `${d.surface}\0${d.fingerprintHash}`;
|
|
2712
|
+
if (seen.has(key)) continue;
|
|
2713
|
+
seen.add(key);
|
|
2714
|
+
approved.push(entryFromDecision(d, approvedAt));
|
|
2715
|
+
}
|
|
2716
|
+
approved.sort(
|
|
2717
|
+
(a, b) => a.surface === b.surface ? a.fingerprintHash.localeCompare(b.fingerprintHash) : a.surface.localeCompare(b.surface)
|
|
2718
|
+
);
|
|
2719
|
+
return { schemaVersion: "calllint.approved.v0", approved };
|
|
2720
|
+
}
|
|
2721
|
+
function writeApproved(state, path = defaultApprovedPath()) {
|
|
2722
|
+
mkdirSync2(dirname2(path), { recursive: true });
|
|
2723
|
+
writeFileSync2(path, JSON.stringify(state, null, 2), "utf8");
|
|
2724
|
+
}
|
|
2725
|
+
function readApproved(path = defaultApprovedPath()) {
|
|
2726
|
+
if (!existsSync2(path)) return void 0;
|
|
2727
|
+
try {
|
|
2728
|
+
return JSON.parse(readFileSync4(path, "utf8"));
|
|
2729
|
+
} catch {
|
|
2730
|
+
return void 0;
|
|
2731
|
+
}
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
// ../../packages/core/src/state/verifyApproved.ts
|
|
2735
|
+
function verifyApproved(current, approved, generatedAt) {
|
|
2736
|
+
const approvedBySurface = /* @__PURE__ */ new Map();
|
|
2737
|
+
for (const a of approved.approved) approvedBySurface.set(a.surface, a);
|
|
2738
|
+
const currentBySurface = /* @__PURE__ */ new Map();
|
|
2739
|
+
for (const d of current) currentBySurface.set(d.surface, d);
|
|
2740
|
+
const entries = [];
|
|
2741
|
+
const driftVerdicts = [];
|
|
2742
|
+
for (const a of approved.approved) {
|
|
2743
|
+
const cur = currentBySurface.get(a.surface);
|
|
2744
|
+
if (!cur) {
|
|
2745
|
+
entries.push({
|
|
2746
|
+
surface: a.surface,
|
|
2747
|
+
status: "removed",
|
|
2748
|
+
approvedHash: a.fingerprintHash,
|
|
2749
|
+
approvedVerdict: a.verdict
|
|
2750
|
+
});
|
|
2751
|
+
driftVerdicts.push("REVIEW");
|
|
2752
|
+
continue;
|
|
2753
|
+
}
|
|
2754
|
+
if (cur.fingerprintHash !== a.fingerprintHash) {
|
|
2755
|
+
entries.push({
|
|
2756
|
+
surface: a.surface,
|
|
2757
|
+
status: "hash-changed",
|
|
2758
|
+
approvedHash: a.fingerprintHash,
|
|
2759
|
+
currentHash: cur.fingerprintHash,
|
|
2760
|
+
approvedVerdict: a.verdict,
|
|
2761
|
+
currentVerdict: cur.verdict
|
|
2762
|
+
});
|
|
2763
|
+
driftVerdicts.push(escalate(cur.verdict));
|
|
2764
|
+
continue;
|
|
2765
|
+
}
|
|
2766
|
+
if (cur.verdict !== a.verdict) {
|
|
2767
|
+
entries.push({
|
|
2768
|
+
surface: a.surface,
|
|
2769
|
+
status: "verdict-changed",
|
|
2770
|
+
approvedHash: a.fingerprintHash,
|
|
2771
|
+
currentHash: cur.fingerprintHash,
|
|
2772
|
+
approvedVerdict: a.verdict,
|
|
2773
|
+
currentVerdict: cur.verdict
|
|
2774
|
+
});
|
|
2775
|
+
driftVerdicts.push(escalate(cur.verdict));
|
|
2776
|
+
continue;
|
|
2777
|
+
}
|
|
2778
|
+
entries.push({
|
|
2779
|
+
surface: a.surface,
|
|
2780
|
+
status: "unchanged",
|
|
2781
|
+
approvedHash: a.fingerprintHash,
|
|
2782
|
+
currentHash: cur.fingerprintHash,
|
|
2783
|
+
approvedVerdict: a.verdict,
|
|
2784
|
+
currentVerdict: cur.verdict
|
|
2785
|
+
});
|
|
2786
|
+
}
|
|
2787
|
+
for (const d of current) {
|
|
2788
|
+
if (!approvedBySurface.has(d.surface)) {
|
|
2789
|
+
entries.push({
|
|
2790
|
+
surface: d.surface,
|
|
2791
|
+
status: "added",
|
|
2792
|
+
currentHash: d.fingerprintHash,
|
|
2793
|
+
currentVerdict: d.verdict
|
|
2794
|
+
});
|
|
2795
|
+
driftVerdicts.push(escalate(d.verdict));
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
const drifted = entries.some((e) => e.status !== "unchanged");
|
|
2799
|
+
const verdict = drifted ? mostSevereVerdict(driftVerdicts) : "SAFE";
|
|
2800
|
+
return {
|
|
2801
|
+
schemaVersion: "calllint.approveddrift.v0",
|
|
2802
|
+
drifted,
|
|
2803
|
+
verdict,
|
|
2804
|
+
entries,
|
|
2805
|
+
generatedAt
|
|
2806
|
+
};
|
|
2807
|
+
}
|
|
2808
|
+
function escalate(current) {
|
|
2809
|
+
return current === "BLOCK" || current === "UNKNOWN" ? current : "REVIEW";
|
|
2810
|
+
}
|
|
2811
|
+
|
|
2812
|
+
// ../../packages/core/src/distribution/agentRule.ts
|
|
2813
|
+
var RELEVANT_SURFACES = [
|
|
2814
|
+
".cursor/mcp.json",
|
|
2815
|
+
".vscode/mcp.json",
|
|
2816
|
+
".mcp.json",
|
|
2817
|
+
".claude/settings.json",
|
|
2818
|
+
"claude_desktop_config.json",
|
|
2819
|
+
"~/.claude.json",
|
|
2820
|
+
".codex/config.toml",
|
|
2821
|
+
".gemini/settings.json",
|
|
2822
|
+
"package.json",
|
|
2823
|
+
"README.md",
|
|
2824
|
+
"AGENTS.md",
|
|
2825
|
+
"CLAUDE.md",
|
|
2826
|
+
".cursor/rules/*.mdc",
|
|
2827
|
+
".github/workflows/*.yml"
|
|
2828
|
+
];
|
|
2829
|
+
var UNIVERSAL_AGENT_RULE = `Before adding, editing, approving, installing, running, publishing, or merging any MCP or agent-tool configuration, run CallLint first and explain the result.
|
|
2830
|
+
|
|
2831
|
+
Run:
|
|
2832
|
+
npx -y calllint check <config-file-or-snippet>
|
|
2833
|
+
|
|
2834
|
+
Decision policy:
|
|
2835
|
+
- SAFE: continue only after stating that no blockers were observed.
|
|
2836
|
+
- REVIEW: explain the findings and ask before continuing.
|
|
2837
|
+
- BLOCK: stop. Do not proceed unless the user explicitly overrides.
|
|
2838
|
+
- UNKNOWN: do not auto-approve; ask for explicit review.
|
|
2839
|
+
- ERROR: do not silently continue.
|
|
2840
|
+
|
|
2841
|
+
Always:
|
|
2842
|
+
- Never execute a scanned MCP server just to decide whether it is safe.
|
|
2843
|
+
- Never print secret values.
|
|
2844
|
+
- Treat SAFE as "no blockers observed", not as proof of runtime safety.`;
|
|
2845
|
+
|
|
2846
|
+
// ../../packages/core/src/distribution/hostRules.ts
|
|
2847
|
+
var RULE_HOSTS = [
|
|
2848
|
+
"claude",
|
|
2849
|
+
"agents",
|
|
2850
|
+
"cursor",
|
|
2851
|
+
"copilot",
|
|
2852
|
+
"codex",
|
|
2853
|
+
"gemini",
|
|
2854
|
+
"windsurf",
|
|
2855
|
+
"cline",
|
|
2856
|
+
"command"
|
|
2857
|
+
];
|
|
2858
|
+
var RULE_TARGETS = {
|
|
2859
|
+
claude: { host: "claude", path: "CLAUDE.md", label: "Claude Code / Claude Desktop" },
|
|
2860
|
+
agents: { host: "agents", path: "AGENTS.md", label: "Generic coding/IDE/CI agents" },
|
|
2861
|
+
cursor: { host: "cursor", path: ".cursor/rules/calllint.mdc", label: "Cursor" },
|
|
2862
|
+
copilot: {
|
|
2863
|
+
host: "copilot",
|
|
2864
|
+
path: ".github/copilot-instructions.md",
|
|
2865
|
+
label: "VS Code / GitHub Copilot"
|
|
2866
|
+
},
|
|
2867
|
+
codex: { host: "codex", path: ".codex/AGENTS.md", label: "OpenAI Codex" },
|
|
2868
|
+
gemini: { host: "gemini", path: ".gemini/GEMINI.md", label: "Gemini CLI" },
|
|
2869
|
+
windsurf: { host: "windsurf", path: ".windsurf/rules/calllint.md", label: "Windsurf" },
|
|
2870
|
+
cline: { host: "cline", path: ".clinerules/calllint.md", label: "Cline" },
|
|
2871
|
+
command: { host: "command", path: ".claude/commands/calllint.md", label: "Claude slash command" }
|
|
2872
|
+
};
|
|
2873
|
+
function surfaceList() {
|
|
2874
|
+
return RELEVANT_SURFACES.map((s) => `- ${s}`).join("\n");
|
|
2875
|
+
}
|
|
2876
|
+
function rule() {
|
|
2877
|
+
return UNIVERSAL_AGENT_RULE;
|
|
2878
|
+
}
|
|
2879
|
+
function markdownBody(intro) {
|
|
2880
|
+
return `# CallLint Agent Tool Safety Rule
|
|
2881
|
+
|
|
2882
|
+
${intro}
|
|
2883
|
+
|
|
2884
|
+
Relevant files include:
|
|
2885
|
+
|
|
2886
|
+
${surfaceList()}
|
|
2887
|
+
|
|
2888
|
+
${rule()}
|
|
2889
|
+
`;
|
|
2890
|
+
}
|
|
2891
|
+
function renderHostRule(host) {
|
|
2892
|
+
switch (host) {
|
|
2893
|
+
case "claude":
|
|
2894
|
+
return markdownBody(
|
|
2895
|
+
"Treat MCP and agent-tool configuration as security-sensitive infrastructure in this repository."
|
|
2896
|
+
);
|
|
2897
|
+
case "agents":
|
|
2898
|
+
return markdownBody(
|
|
2899
|
+
"This rule applies to all coding agents, IDE agents, CI agents, and automation agents working in this repository. Treat MCP and agent-tool configuration as security-sensitive infrastructure."
|
|
2900
|
+
);
|
|
2901
|
+
case "copilot":
|
|
2902
|
+
return markdownBody(
|
|
2903
|
+
"When working in this repository, treat MCP and agent-tool configuration as security-sensitive infrastructure."
|
|
2904
|
+
);
|
|
2905
|
+
case "codex":
|
|
2906
|
+
case "gemini":
|
|
2907
|
+
case "windsurf":
|
|
2908
|
+
case "cline":
|
|
2909
|
+
return markdownBody(
|
|
2910
|
+
`Treat MCP and agent-tool configuration as security-sensitive infrastructure (${RULE_TARGETS[host].label}).`
|
|
2911
|
+
);
|
|
2912
|
+
case "cursor":
|
|
2913
|
+
return `---
|
|
2914
|
+
description: Require CallLint before MCP or agent-tool permission changes
|
|
2915
|
+
alwaysApply: true
|
|
2916
|
+
---
|
|
2917
|
+
|
|
2918
|
+
Treat MCP and agent-tool configuration as security-sensitive infrastructure.
|
|
2919
|
+
|
|
2920
|
+
Relevant files include:
|
|
2921
|
+
|
|
2922
|
+
${surfaceList()}
|
|
2923
|
+
|
|
2924
|
+
${rule()}
|
|
2925
|
+
`;
|
|
2926
|
+
case "command":
|
|
2927
|
+
return `# /calllint
|
|
2928
|
+
|
|
2929
|
+
Run CallLint against MCP and agent-tool configuration in this repository.
|
|
2930
|
+
|
|
2931
|
+
Steps:
|
|
2932
|
+
|
|
2933
|
+
1. Find relevant files:
|
|
2934
|
+
|
|
2935
|
+
${surfaceList()}
|
|
2936
|
+
|
|
2937
|
+
2. For each, run \`npx -y calllint check <file>\` (or \`calllint scan-all\` for the whole repo).
|
|
2938
|
+
3. Apply the decision policy below.
|
|
2939
|
+
|
|
2940
|
+
${rule()}
|
|
2941
|
+
`;
|
|
2942
|
+
}
|
|
2943
|
+
}
|
|
2944
|
+
|
|
2043
2945
|
// ../../packages/report-renderer/src/style.ts
|
|
2044
2946
|
var DEFAULT_STYLE = { emoji: true };
|
|
2045
2947
|
var NO_EMOJI_STYLE = { emoji: false };
|
|
@@ -2122,6 +3024,42 @@ function renderCompact(summary, style = DEFAULT_STYLE) {
|
|
|
2122
3024
|
return lines.join("\n");
|
|
2123
3025
|
}
|
|
2124
3026
|
|
|
3027
|
+
// ../../packages/report-renderer/src/renderDecision.ts
|
|
3028
|
+
var NEXT_ACTION_HINT = {
|
|
3029
|
+
continue: "No blockers observed \u2014 safe to continue.",
|
|
3030
|
+
ask_before_continue: "Review required \u2014 confirm before continuing.",
|
|
3031
|
+
stop: "Blocked \u2014 do not proceed without resolving the blocker.",
|
|
3032
|
+
gather_more_evidence: "Insufficient evidence \u2014 gather more before continuing."
|
|
3033
|
+
};
|
|
3034
|
+
function shortSurface(surface) {
|
|
3035
|
+
return surface.length > 60 ? "\u2026" + surface.slice(-59) : surface;
|
|
3036
|
+
}
|
|
3037
|
+
function renderDecision(decision, style = DEFAULT_STYLE) {
|
|
3038
|
+
const lines = [];
|
|
3039
|
+
lines.push(`${verdictTag(decision.verdict, style)} ${shortSurface(decision.surface)}`);
|
|
3040
|
+
if (decision.reasonCodes.length > 0) {
|
|
3041
|
+
lines.push(`Reasons: ${decision.reasonCodes.join(", ")}`);
|
|
3042
|
+
} else {
|
|
3043
|
+
lines.push("Reasons: none observed");
|
|
3044
|
+
}
|
|
3045
|
+
lines.push(`Next: ${NEXT_ACTION_HINT[decision.nextAction]}`);
|
|
3046
|
+
return lines.join("\n");
|
|
3047
|
+
}
|
|
3048
|
+
function renderDecisionTable(decisions, style = DEFAULT_STYLE) {
|
|
3049
|
+
if (decisions.length === 0) {
|
|
3050
|
+
return "0 agent-tool surfaces found";
|
|
3051
|
+
}
|
|
3052
|
+
const lines = [
|
|
3053
|
+
`${decisions.length} agent-tool surface${decisions.length === 1 ? "" : "s"} found`,
|
|
3054
|
+
""
|
|
3055
|
+
];
|
|
3056
|
+
for (const d of decisions) {
|
|
3057
|
+
const codes = d.reasonCodes.length > 0 ? d.reasonCodes.join(", ") : "no blockers observed";
|
|
3058
|
+
lines.push(`${verdictTag(d.verdict, style)} ${shortSurface(d.surface)} ${codes}`);
|
|
3059
|
+
}
|
|
3060
|
+
return lines.join("\n");
|
|
3061
|
+
}
|
|
3062
|
+
|
|
2125
3063
|
// ../../packages/report-renderer/src/renderExplain.ts
|
|
2126
3064
|
function renderFindingDetail(f, n) {
|
|
2127
3065
|
const lines = [];
|
|
@@ -2212,6 +3150,39 @@ function renderDrift(report) {
|
|
|
2212
3150
|
return lines.join("\n");
|
|
2213
3151
|
}
|
|
2214
3152
|
|
|
3153
|
+
// ../../packages/report-renderer/src/renderApprovedDrift.ts
|
|
3154
|
+
function renderApprovedDriftJson(report) {
|
|
3155
|
+
return JSON.stringify(report, null, 2);
|
|
3156
|
+
}
|
|
3157
|
+
var STATUS_TAG2 = {
|
|
3158
|
+
unchanged: "OK ",
|
|
3159
|
+
"hash-changed": "HASH ",
|
|
3160
|
+
"verdict-changed": "VRDCT",
|
|
3161
|
+
added: "ADD ",
|
|
3162
|
+
removed: "DEL "
|
|
3163
|
+
};
|
|
3164
|
+
function renderApprovedDrift(report) {
|
|
3165
|
+
const lines = [];
|
|
3166
|
+
lines.push("CallLint verify (drift vs approved state)");
|
|
3167
|
+
const headline = report.drifted ? `DRIFT \u2014 approved capability surface changed (${report.verdict})` : "no drift \u2014 matches approved state";
|
|
3168
|
+
lines.push(`result: ${headline}`);
|
|
3169
|
+
lines.push("\u2500".repeat(60));
|
|
3170
|
+
for (const e of report.entries) {
|
|
3171
|
+
const tag = STATUS_TAG2[e.status] ?? e.status;
|
|
3172
|
+
lines.push(`${tag} ${e.surface}`);
|
|
3173
|
+
if (e.status === "hash-changed") {
|
|
3174
|
+
lines.push(` \u2022 ${e.approvedHash} \u2192 ${e.currentHash}`);
|
|
3175
|
+
} else if (e.status === "verdict-changed") {
|
|
3176
|
+
lines.push(` \u2022 verdict ${e.approvedVerdict} \u2192 ${e.currentVerdict}`);
|
|
3177
|
+
} else if (e.status === "added") {
|
|
3178
|
+
lines.push(` \u2022 new surface, not in approved state (${e.currentVerdict})`);
|
|
3179
|
+
} else if (e.status === "removed") {
|
|
3180
|
+
lines.push(` \u2022 approved surface no longer present`);
|
|
3181
|
+
}
|
|
3182
|
+
}
|
|
3183
|
+
return lines.join("\n");
|
|
3184
|
+
}
|
|
3185
|
+
|
|
2215
3186
|
// ../../packages/report-renderer/src/renderSarif.ts
|
|
2216
3187
|
function levelForSeverity(sev) {
|
|
2217
3188
|
if (sev === "critical" || sev === "high") return "error";
|
|
@@ -2304,6 +3275,92 @@ function renderSarif(summary) {
|
|
|
2304
3275
|
return JSON.stringify(sarif, null, 2);
|
|
2305
3276
|
}
|
|
2306
3277
|
|
|
3278
|
+
// ../../packages/report-renderer/src/renderMarkdown.ts
|
|
3279
|
+
function renderMarkdown(summary) {
|
|
3280
|
+
const out = [];
|
|
3281
|
+
const c = summary.counts;
|
|
3282
|
+
out.push(`## CallLint: ${summary.verdict} \u2014 ${summary.publicVerdictLabel}`);
|
|
3283
|
+
out.push("");
|
|
3284
|
+
out.push(
|
|
3285
|
+
`CallLint statically checked \`${summary.configPath}\` before the agent tool runs. It never executes, installs, or connects to the server it judges.`
|
|
3286
|
+
);
|
|
3287
|
+
out.push("");
|
|
3288
|
+
out.push("| Server | Verdict | Risk class | Findings |");
|
|
3289
|
+
out.push("| --- | --- | --- | --- |");
|
|
3290
|
+
for (const r of summary.reports) {
|
|
3291
|
+
out.push(
|
|
3292
|
+
`| ${mdCell(r.target.name)} | ${r.verdict} | ${r.riskClass} | ${r.findings.length} |`
|
|
3293
|
+
);
|
|
3294
|
+
}
|
|
3295
|
+
out.push(
|
|
3296
|
+
`| **TOTAL** | **${summary.verdict}** | \u2014 | BLOCK ${c.BLOCK} \xB7 UNKNOWN ${c.UNKNOWN} \xB7 REVIEW ${c.REVIEW} \xB7 SAFE ${c.SAFE} |`
|
|
3297
|
+
);
|
|
3298
|
+
out.push("");
|
|
3299
|
+
for (const r of summary.reports) {
|
|
3300
|
+
renderServer2(out, r);
|
|
3301
|
+
}
|
|
3302
|
+
out.push("---");
|
|
3303
|
+
out.push("");
|
|
3304
|
+
out.push(
|
|
3305
|
+
"> CallLint does not prove runtime safety. `SAFE` means no blockers observed under current evidence; `UNKNOWN` is never `SAFE`. Verdicts are heuristic decision support, not a safety guarantee."
|
|
3306
|
+
);
|
|
3307
|
+
return out.join("\n");
|
|
3308
|
+
}
|
|
3309
|
+
function renderServer2(out, r) {
|
|
3310
|
+
out.push(`### ${r.target.name} \u2014 ${r.verdict}`);
|
|
3311
|
+
out.push("");
|
|
3312
|
+
if (r.summary) {
|
|
3313
|
+
out.push(r.summary);
|
|
3314
|
+
out.push("");
|
|
3315
|
+
}
|
|
3316
|
+
if (r.findings.length === 0) {
|
|
3317
|
+
out.push("_No findings._");
|
|
3318
|
+
out.push("");
|
|
3319
|
+
} else {
|
|
3320
|
+
for (const f of r.findings) {
|
|
3321
|
+
renderFinding(out, f);
|
|
3322
|
+
}
|
|
3323
|
+
}
|
|
3324
|
+
renderRecommendation(out, r.policy);
|
|
3325
|
+
out.push("");
|
|
3326
|
+
}
|
|
3327
|
+
function renderFinding(out, f) {
|
|
3328
|
+
const tag = f.blocker ? "BLOCKER" : f.severity.toUpperCase();
|
|
3329
|
+
out.push(`#### ${tag}: ${f.title}`);
|
|
3330
|
+
out.push("");
|
|
3331
|
+
out.push(`- Risk: ${f.symbol} \xB7 ${f.riskClass} \xB7 ${f.mode} \xB7 confidence ${f.confidence}`);
|
|
3332
|
+
const ev = f.evidence[0];
|
|
3333
|
+
if (ev) {
|
|
3334
|
+
const loc = ev.path ? ` \`${ev.path}\`${ev.line ? `:${ev.line}` : ""}` : "";
|
|
3335
|
+
const shown = ev.snippet ?? ev.value ?? ev.key;
|
|
3336
|
+
out.push(`- Evidence:${loc}`);
|
|
3337
|
+
if (shown) {
|
|
3338
|
+
out.push("");
|
|
3339
|
+
out.push(" ```text");
|
|
3340
|
+
for (const line of String(shown).split("\n")) out.push(` ${line}`);
|
|
3341
|
+
out.push(" ```");
|
|
3342
|
+
}
|
|
3343
|
+
}
|
|
3344
|
+
out.push("");
|
|
3345
|
+
out.push(`Why it matters: ${f.impact}`);
|
|
3346
|
+
out.push("");
|
|
3347
|
+
out.push(`Recommended fix: ${f.fix}`);
|
|
3348
|
+
if (f.falsePositiveNote) {
|
|
3349
|
+
out.push("");
|
|
3350
|
+
out.push(`Note: ${f.falsePositiveNote}`);
|
|
3351
|
+
}
|
|
3352
|
+
out.push("");
|
|
3353
|
+
}
|
|
3354
|
+
function renderRecommendation(out, p) {
|
|
3355
|
+
out.push("Policy recommendation:");
|
|
3356
|
+
out.push(`- Autonomous use: ${p.autonomousUse}`);
|
|
3357
|
+
out.push(`- Manual approval: ${p.manualApproval}`);
|
|
3358
|
+
out.push(`- Sandbox: ${p.sandbox}`);
|
|
3359
|
+
}
|
|
3360
|
+
function mdCell(s) {
|
|
3361
|
+
return s.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
3362
|
+
}
|
|
3363
|
+
|
|
2307
3364
|
// ../../packages/report-renderer/src/renderDiagnostics.ts
|
|
2308
3365
|
function verdictContributionFor(f) {
|
|
2309
3366
|
if (f.blocker) return "blocker";
|
|
@@ -2500,8 +3557,8 @@ function exitCodeFor(summary, policy) {
|
|
|
2500
3557
|
}
|
|
2501
3558
|
|
|
2502
3559
|
// src/commands/resolveInput.ts
|
|
2503
|
-
import { existsSync as
|
|
2504
|
-
import { join as
|
|
3560
|
+
import { existsSync as existsSync3, readFileSync as readFileSync5 } from "node:fs";
|
|
3561
|
+
import { join as join4 } from "node:path";
|
|
2505
3562
|
var DEFAULT_CONFIG_PATHS = [
|
|
2506
3563
|
".cursor/mcp.json",
|
|
2507
3564
|
".mcp.json",
|
|
@@ -2511,8 +3568,8 @@ var DEFAULT_CONFIG_PATHS = [
|
|
|
2511
3568
|
];
|
|
2512
3569
|
function findDefaultConfig(cwd) {
|
|
2513
3570
|
for (const rel of DEFAULT_CONFIG_PATHS) {
|
|
2514
|
-
const p =
|
|
2515
|
-
if (
|
|
3571
|
+
const p = join4(cwd, rel);
|
|
3572
|
+
if (existsSync3(p)) return p;
|
|
2516
3573
|
}
|
|
2517
3574
|
return void 0;
|
|
2518
3575
|
}
|
|
@@ -2546,26 +3603,43 @@ function resolveConfigInput(args, deps) {
|
|
|
2546
3603
|
exitCode: EXIT.USAGE
|
|
2547
3604
|
};
|
|
2548
3605
|
}
|
|
2549
|
-
if (!
|
|
3606
|
+
if (!existsSync3(resolved)) {
|
|
2550
3607
|
return { error: `File not found: ${resolved}`, exitCode: EXIT.USAGE };
|
|
2551
3608
|
}
|
|
2552
|
-
return { text:
|
|
3609
|
+
return { text: readFileSync5(resolved, "utf8"), configPath: resolved };
|
|
3610
|
+
}
|
|
3611
|
+
|
|
3612
|
+
// src/commands/changedConfigs.ts
|
|
3613
|
+
import { basename as basename3, resolve } from "node:path";
|
|
3614
|
+
import { existsSync as existsSync4 } from "node:fs";
|
|
3615
|
+
function changedConfigPaths(cwd, diff) {
|
|
3616
|
+
let raw;
|
|
3617
|
+
try {
|
|
3618
|
+
raw = diff();
|
|
3619
|
+
} catch {
|
|
3620
|
+
return [];
|
|
3621
|
+
}
|
|
3622
|
+
const knownLeaves = new Set(DEFAULT_CONFIG_PATHS.map((p) => basename3(p)));
|
|
3623
|
+
const knownPaths = new Set(DEFAULT_CONFIG_PATHS);
|
|
3624
|
+
return raw.split("\n").map((l) => l.trim()).filter(
|
|
3625
|
+
(f) => f.length > 0 && (knownPaths.has(f) || DEFAULT_CONFIG_PATHS.some((p) => f.endsWith("/" + p)) || knownLeaves.has(basename3(f)))
|
|
3626
|
+
).map((f) => resolve(cwd, f)).filter((abs) => existsSync4(abs));
|
|
2553
3627
|
}
|
|
2554
3628
|
|
|
2555
3629
|
// src/commands/surfaces.ts
|
|
2556
|
-
import { existsSync as
|
|
2557
|
-
import { join as
|
|
3630
|
+
import { existsSync as existsSync5, readFileSync as readFileSync6, statSync as statSync2 } from "node:fs";
|
|
3631
|
+
import { join as join5 } from "node:path";
|
|
2558
3632
|
var SURFACE_SIZE_CAP = 256 * 1024;
|
|
2559
3633
|
var SURFACE_FILES = [
|
|
2560
3634
|
{ file: "README.md", kind: "readme" },
|
|
2561
3635
|
{ file: "SKILL.md", kind: "skill" },
|
|
2562
3636
|
{ file: "AGENTS.md", kind: "agents" }
|
|
2563
3637
|
];
|
|
2564
|
-
function
|
|
3638
|
+
function readCapped2(path) {
|
|
2565
3639
|
try {
|
|
2566
|
-
const st =
|
|
3640
|
+
const st = statSync2(path);
|
|
2567
3641
|
if (!st.isFile()) return void 0;
|
|
2568
|
-
const raw =
|
|
3642
|
+
const raw = readFileSync6(path, "utf8");
|
|
2569
3643
|
if (raw.length > SURFACE_SIZE_CAP) {
|
|
2570
3644
|
return { text: raw.slice(0, SURFACE_SIZE_CAP), truncated: true };
|
|
2571
3645
|
}
|
|
@@ -2576,13 +3650,13 @@ function readCapped(path) {
|
|
|
2576
3650
|
}
|
|
2577
3651
|
function readDocumentSurfaces(dir) {
|
|
2578
3652
|
const surfaces = [];
|
|
2579
|
-
if (!
|
|
3653
|
+
if (!existsSync5(dir)) return surfaces;
|
|
2580
3654
|
for (const { file, kind } of SURFACE_FILES) {
|
|
2581
|
-
const got =
|
|
3655
|
+
const got = readCapped2(join5(dir, file));
|
|
2582
3656
|
if (got) surfaces.push({ path: file, kind, text: got.text, truncated: got.truncated });
|
|
2583
3657
|
}
|
|
2584
|
-
const pkgPath =
|
|
2585
|
-
const pkgRaw =
|
|
3658
|
+
const pkgPath = join5(dir, "package.json");
|
|
3659
|
+
const pkgRaw = readCapped2(pkgPath);
|
|
2586
3660
|
if (pkgRaw) {
|
|
2587
3661
|
try {
|
|
2588
3662
|
const pkg = JSON.parse(pkgRaw.text);
|
|
@@ -2602,7 +3676,11 @@ function readDocumentSurfaces(dir) {
|
|
|
2602
3676
|
}
|
|
2603
3677
|
|
|
2604
3678
|
// src/commands/scan.ts
|
|
3679
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
2605
3680
|
function scanCommand(args, deps) {
|
|
3681
|
+
if (flagBool(args.flags, "changed")) {
|
|
3682
|
+
return scanChangedCommand(args, deps);
|
|
3683
|
+
}
|
|
2606
3684
|
const policyPath = flagStr(args.flags, "policy");
|
|
2607
3685
|
let policy;
|
|
2608
3686
|
try {
|
|
@@ -2619,8 +3697,11 @@ function scanCommand(args, deps) {
|
|
|
2619
3697
|
return { stdout: "", stderr: input.error, exitCode: input.exitCode };
|
|
2620
3698
|
}
|
|
2621
3699
|
const { text, configPath } = input;
|
|
3700
|
+
return scanOneConfig(text, configPath, policy, args, deps);
|
|
3701
|
+
}
|
|
3702
|
+
function scanOneConfig(text, configPath, policy, args, deps) {
|
|
2622
3703
|
const surfaceDir = flagStr(args.flags, "surface-dir");
|
|
2623
|
-
const surfaces = surfaceDir ? readDocumentSurfaces(
|
|
3704
|
+
const surfaces = surfaceDir ? readDocumentSurfaces(resolve2(deps.cwd, surfaceDir)) : void 0;
|
|
2624
3705
|
let summary;
|
|
2625
3706
|
try {
|
|
2626
3707
|
summary = scanConfigText(text, configPath, {
|
|
@@ -2642,20 +3723,200 @@ function scanCommand(args, deps) {
|
|
|
2642
3723
|
}
|
|
2643
3724
|
if (deps.writeCacheFile !== false) {
|
|
2644
3725
|
try {
|
|
2645
|
-
writeCache(summary,
|
|
3726
|
+
writeCache(summary, join6(deps.cwd, ".calllint", "last-scan.json"));
|
|
2646
3727
|
} catch {
|
|
2647
3728
|
}
|
|
2648
3729
|
}
|
|
2649
|
-
const
|
|
2650
|
-
let stdout;
|
|
2651
|
-
if (flagBool(args.flags, "json")) stdout = renderJson(summary);
|
|
2652
|
-
else if (flagBool(args.flags, "sarif")) stdout = renderSarif(summary);
|
|
2653
|
-
else if (flagBool(args.flags, "html")) stdout = renderHtml(summary);
|
|
2654
|
-
else if (flagBool(args.flags, "compact")) stdout = renderCompact(summary, style);
|
|
2655
|
-
else stdout = renderTerminal(summary, style);
|
|
3730
|
+
const stdout = renderSummary(summary, args);
|
|
2656
3731
|
const exitCode = flagBool(args.flags, "ci") ? exitCodeFor(summary, policy) : EXIT.OK;
|
|
2657
3732
|
return { stdout, exitCode };
|
|
2658
3733
|
}
|
|
3734
|
+
function renderSummary(summary, args) {
|
|
3735
|
+
const style = flagBool(args.flags, "no-emoji") ? NO_EMOJI_STYLE : DEFAULT_STYLE;
|
|
3736
|
+
if (flagBool(args.flags, "json")) return renderJson(summary);
|
|
3737
|
+
if (flagBool(args.flags, "sarif")) return renderSarif(summary);
|
|
3738
|
+
if (flagBool(args.flags, "markdown")) return renderMarkdown(summary);
|
|
3739
|
+
if (flagBool(args.flags, "html")) return renderHtml(summary);
|
|
3740
|
+
if (flagBool(args.flags, "compact")) return renderCompact(summary, style);
|
|
3741
|
+
return renderTerminal(summary, style);
|
|
3742
|
+
}
|
|
3743
|
+
function scanChangedCommand(args, deps) {
|
|
3744
|
+
const policyPath = flagStr(args.flags, "policy");
|
|
3745
|
+
let policy;
|
|
3746
|
+
try {
|
|
3747
|
+
policy = loadPolicyOrDefault(policyPath);
|
|
3748
|
+
} catch (err) {
|
|
3749
|
+
return {
|
|
3750
|
+
stdout: "",
|
|
3751
|
+
stderr: err instanceof Error ? err.message : String(err),
|
|
3752
|
+
exitCode: EXIT.ERROR
|
|
3753
|
+
};
|
|
3754
|
+
}
|
|
3755
|
+
if (!deps.getChangedFilesDiff) {
|
|
3756
|
+
return {
|
|
3757
|
+
stdout: "",
|
|
3758
|
+
stderr: "--changed needs a git diff source. Run inside a git repository, or scan a path directly.",
|
|
3759
|
+
exitCode: EXIT.USAGE
|
|
3760
|
+
};
|
|
3761
|
+
}
|
|
3762
|
+
const paths = changedConfigPaths(deps.cwd, deps.getChangedFilesDiff);
|
|
3763
|
+
if (paths.length === 0) {
|
|
3764
|
+
return {
|
|
3765
|
+
stdout: "No agent-tool configs changed in the git diff. Nothing to scan.",
|
|
3766
|
+
exitCode: EXIT.OK
|
|
3767
|
+
};
|
|
3768
|
+
}
|
|
3769
|
+
const results = paths.map((p) => {
|
|
3770
|
+
const text = readFileSync7(p, "utf8");
|
|
3771
|
+
return scanOneConfig(text, p, policy, args, deps);
|
|
3772
|
+
});
|
|
3773
|
+
const exitCode = results.reduce((worst, r) => Math.max(worst, r.exitCode), EXIT.OK);
|
|
3774
|
+
let stdout;
|
|
3775
|
+
if (flagBool(args.flags, "json")) {
|
|
3776
|
+
stdout = "[" + results.map((r) => r.stdout).join(",\n") + "]";
|
|
3777
|
+
} else {
|
|
3778
|
+
stdout = results.map((r) => r.stdout).join("\n\n---\n\n");
|
|
3779
|
+
}
|
|
3780
|
+
const stderr = results.map((r) => r.stderr).filter(Boolean).join("\n");
|
|
3781
|
+
return { stdout, exitCode, ...stderr ? { stderr } : {} };
|
|
3782
|
+
}
|
|
3783
|
+
|
|
3784
|
+
// src/commands/check.ts
|
|
3785
|
+
function checkCommand(args, deps) {
|
|
3786
|
+
const style = flagBool(args.flags, "no-emoji") ? NO_EMOJI_STYLE : DEFAULT_STYLE;
|
|
3787
|
+
const opts = { now: deps.now, generatedAt: deps.generatedAt };
|
|
3788
|
+
const input = resolveConfigInput(args, deps);
|
|
3789
|
+
if (isInputError(input)) {
|
|
3790
|
+
return { stdout: "", stderr: input.error, exitCode: input.exitCode };
|
|
3791
|
+
}
|
|
3792
|
+
let decisions;
|
|
3793
|
+
try {
|
|
3794
|
+
const looksLikeJson = input.text.trim().startsWith("{");
|
|
3795
|
+
if (input.configPath === "<stdin>" && !looksLikeJson) {
|
|
3796
|
+
const { parsed } = parseSnippet(input.text);
|
|
3797
|
+
decisions = checkParsed(parsed, "stdin:snippet", "remote", opts);
|
|
3798
|
+
} else {
|
|
3799
|
+
const loaded = loadSurfaceText(input.text, input.configPath);
|
|
3800
|
+
decisions = checkParsed(loaded.parsed, input.configPath, loaded.origin, opts);
|
|
3801
|
+
}
|
|
3802
|
+
} catch (e) {
|
|
3803
|
+
if (e instanceof ConfigParseError) {
|
|
3804
|
+
return { stdout: "", stderr: `Parse error: ${e.message}`, exitCode: EXIT.ERROR };
|
|
3805
|
+
}
|
|
3806
|
+
if (e instanceof Error) {
|
|
3807
|
+
return { stdout: "", stderr: e.message, exitCode: EXIT.UNKNOWN };
|
|
3808
|
+
}
|
|
3809
|
+
throw e;
|
|
3810
|
+
}
|
|
3811
|
+
if (decisions.length === 0) {
|
|
3812
|
+
return {
|
|
3813
|
+
stdout: "UNKNOWN no agent-tool capability found in input",
|
|
3814
|
+
exitCode: EXIT.UNKNOWN
|
|
3815
|
+
};
|
|
3816
|
+
}
|
|
3817
|
+
if (flagBool(args.flags, "explain") || flagBool(args.flags, "full")) {
|
|
3818
|
+
const stdout2 = decisions.map((d) => renderExplain(d.report, style)).join("\n\n");
|
|
3819
|
+
return { stdout: stdout2, exitCode: worstExit(decisions) };
|
|
3820
|
+
}
|
|
3821
|
+
if (flagBool(args.flags, "json")) {
|
|
3822
|
+
const payload = decisions.length === 1 ? decisions[0].decision : decisions.map((d) => d.decision);
|
|
3823
|
+
return { stdout: JSON.stringify(payload), exitCode: worstExit(decisions) };
|
|
3824
|
+
}
|
|
3825
|
+
const stdout = decisions.map((d) => renderDecision(d.decision, style)).join("\n\n");
|
|
3826
|
+
return { stdout, exitCode: worstExit(decisions) };
|
|
3827
|
+
}
|
|
3828
|
+
function worstExit(decisions) {
|
|
3829
|
+
let worst = EXIT.OK;
|
|
3830
|
+
for (const d of decisions) {
|
|
3831
|
+
const code = exitForVerdict(d.decision.verdict);
|
|
3832
|
+
if (code > worst) worst = code;
|
|
3833
|
+
}
|
|
3834
|
+
return worst;
|
|
3835
|
+
}
|
|
3836
|
+
function exitForVerdict(verdict) {
|
|
3837
|
+
switch (verdict) {
|
|
3838
|
+
case "BLOCK":
|
|
3839
|
+
return EXIT.BLOCK;
|
|
3840
|
+
case "UNKNOWN":
|
|
3841
|
+
return EXIT.UNKNOWN;
|
|
3842
|
+
case "REVIEW":
|
|
3843
|
+
return EXIT.REVIEW;
|
|
3844
|
+
case "SAFE":
|
|
3845
|
+
return EXIT.OK;
|
|
3846
|
+
}
|
|
3847
|
+
}
|
|
3848
|
+
|
|
3849
|
+
// src/commands/scanAll.ts
|
|
3850
|
+
function scanAllCommand(args, deps) {
|
|
3851
|
+
const style = flagBool(args.flags, "no-emoji") ? NO_EMOJI_STYLE : DEFAULT_STYLE;
|
|
3852
|
+
const decisions = decideRepoSurfaces(deps.cwd, {
|
|
3853
|
+
now: deps.now,
|
|
3854
|
+
generatedAt: deps.generatedAt
|
|
3855
|
+
});
|
|
3856
|
+
if (flagBool(args.flags, "json")) {
|
|
3857
|
+
return { stdout: JSON.stringify(decisions), exitCode: worstExit2(decisions) };
|
|
3858
|
+
}
|
|
3859
|
+
return { stdout: renderDecisionTable(decisions, style), exitCode: worstExit2(decisions) };
|
|
3860
|
+
}
|
|
3861
|
+
function worstExit2(decisions) {
|
|
3862
|
+
let worst = EXIT.OK;
|
|
3863
|
+
for (const d of decisions) {
|
|
3864
|
+
const code = d.verdict === "BLOCK" ? EXIT.BLOCK : d.verdict === "UNKNOWN" ? EXIT.UNKNOWN : d.verdict === "REVIEW" ? EXIT.REVIEW : EXIT.OK;
|
|
3865
|
+
if (code > worst) worst = code;
|
|
3866
|
+
}
|
|
3867
|
+
return worst;
|
|
3868
|
+
}
|
|
3869
|
+
|
|
3870
|
+
// src/commands/genRule.ts
|
|
3871
|
+
import { writeFileSync as writeFileSync3, mkdirSync as mkdirSync3 } from "node:fs";
|
|
3872
|
+
import { dirname as dirname3, join as join7, resolve as resolve3 } from "node:path";
|
|
3873
|
+
function isRuleHost(v) {
|
|
3874
|
+
return v !== void 0 && RULE_HOSTS.includes(v);
|
|
3875
|
+
}
|
|
3876
|
+
function genRuleCommand(args, deps) {
|
|
3877
|
+
const host = flagStr(args.flags, "host");
|
|
3878
|
+
if (!host) {
|
|
3879
|
+
const list = RULE_HOSTS.map(
|
|
3880
|
+
(h) => ` ${h.padEnd(10)} \u2192 ${RULE_TARGETS[h].path} (${RULE_TARGETS[h].label})`
|
|
3881
|
+
).join("\n");
|
|
3882
|
+
return {
|
|
3883
|
+
stdout: `Usage: calllint gen-rule --host <host> [--write] [--out <path>]
|
|
3884
|
+
|
|
3885
|
+
Hosts:
|
|
3886
|
+
${list}`,
|
|
3887
|
+
exitCode: EXIT.OK
|
|
3888
|
+
};
|
|
3889
|
+
}
|
|
3890
|
+
if (!isRuleHost(host)) {
|
|
3891
|
+
return {
|
|
3892
|
+
stdout: "",
|
|
3893
|
+
stderr: `Unknown host: ${host}
|
|
3894
|
+
Run \`calllint gen-rule\` to list hosts.`,
|
|
3895
|
+
exitCode: EXIT.USAGE
|
|
3896
|
+
};
|
|
3897
|
+
}
|
|
3898
|
+
const content = renderHostRule(host);
|
|
3899
|
+
if (!flagBool(args.flags, "write")) {
|
|
3900
|
+
return { stdout: content, exitCode: EXIT.OK };
|
|
3901
|
+
}
|
|
3902
|
+
const rel = flagStr(args.flags, "out") ?? RULE_TARGETS[host].path;
|
|
3903
|
+
const abs = resolve3(deps.cwd, rel);
|
|
3904
|
+
const write = deps.writeFile ?? defaultWrite;
|
|
3905
|
+
try {
|
|
3906
|
+
write(abs, content);
|
|
3907
|
+
} catch (e) {
|
|
3908
|
+
return {
|
|
3909
|
+
stdout: "",
|
|
3910
|
+
stderr: `Failed to write ${rel}: ${e instanceof Error ? e.message : String(e)}`,
|
|
3911
|
+
exitCode: EXIT.ERROR
|
|
3912
|
+
};
|
|
3913
|
+
}
|
|
3914
|
+
return { stdout: `Wrote ${rel} (${RULE_TARGETS[host].label})`, exitCode: EXIT.OK };
|
|
3915
|
+
}
|
|
3916
|
+
function defaultWrite(path, content) {
|
|
3917
|
+
mkdirSync3(dirname3(path), { recursive: true });
|
|
3918
|
+
writeFileSync3(path, content, "utf8");
|
|
3919
|
+
}
|
|
2659
3920
|
|
|
2660
3921
|
// src/commands/diagnostics.ts
|
|
2661
3922
|
function diagnosticsCommand(args, deps) {
|
|
@@ -2705,7 +3966,7 @@ function diagnosticsCommand(args, deps) {
|
|
|
2705
3966
|
}
|
|
2706
3967
|
|
|
2707
3968
|
// src/commands/explain.ts
|
|
2708
|
-
import { join as
|
|
3969
|
+
import { join as join8 } from "node:path";
|
|
2709
3970
|
function explainCommand(args, deps) {
|
|
2710
3971
|
const serverName = args.positionals[0];
|
|
2711
3972
|
if (!serverName) {
|
|
@@ -2715,7 +3976,7 @@ function explainCommand(args, deps) {
|
|
|
2715
3976
|
exitCode: EXIT.USAGE
|
|
2716
3977
|
};
|
|
2717
3978
|
}
|
|
2718
|
-
const summary = readCache(
|
|
3979
|
+
const summary = readCache(join8(deps.cwd, ".calllint", "last-scan.json"));
|
|
2719
3980
|
if (!summary) {
|
|
2720
3981
|
return {
|
|
2721
3982
|
stdout: "",
|
|
@@ -2737,20 +3998,20 @@ function explainCommand(args, deps) {
|
|
|
2737
3998
|
}
|
|
2738
3999
|
|
|
2739
4000
|
// src/commands/policy.ts
|
|
2740
|
-
import { existsSync as
|
|
2741
|
-
import { join as
|
|
4001
|
+
import { existsSync as existsSync6, writeFileSync as writeFileSync4 } from "node:fs";
|
|
4002
|
+
import { join as join9 } from "node:path";
|
|
2742
4003
|
function policyCommand(args, deps) {
|
|
2743
4004
|
const sub = args.positionals[0];
|
|
2744
4005
|
if (sub === "init") {
|
|
2745
|
-
const path =
|
|
2746
|
-
if (
|
|
4006
|
+
const path = join9(deps.cwd, "calllint.policy.json");
|
|
4007
|
+
if (existsSync6(path) && !flagBool(args.flags, "force")) {
|
|
2747
4008
|
return {
|
|
2748
4009
|
stdout: "",
|
|
2749
4010
|
stderr: `${path} already exists. Use --force to overwrite.`,
|
|
2750
4011
|
exitCode: EXIT.USAGE
|
|
2751
4012
|
};
|
|
2752
4013
|
}
|
|
2753
|
-
|
|
4014
|
+
writeFileSync4(path, defaultPolicyJson(), "utf8");
|
|
2754
4015
|
return { stdout: `Wrote default policy to ${path}`, exitCode: EXIT.OK };
|
|
2755
4016
|
}
|
|
2756
4017
|
if (sub === "explain") {
|
|
@@ -2773,7 +4034,7 @@ function policyCommand(args, deps) {
|
|
|
2773
4034
|
}
|
|
2774
4035
|
|
|
2775
4036
|
// src/commands/verify.ts
|
|
2776
|
-
import { join as
|
|
4037
|
+
import { join as join10 } from "node:path";
|
|
2777
4038
|
function loadPolicy(args) {
|
|
2778
4039
|
try {
|
|
2779
4040
|
return loadPolicyOrDefault(flagStr(args.flags, "policy"));
|
|
@@ -2782,7 +4043,7 @@ function loadPolicy(args) {
|
|
|
2782
4043
|
}
|
|
2783
4044
|
}
|
|
2784
4045
|
function baselinePathFor(args, cwd) {
|
|
2785
|
-
return flagStr(args.flags, "baseline") ??
|
|
4046
|
+
return flagStr(args.flags, "baseline") ?? join10(cwd, ".calllint", "baseline.json");
|
|
2786
4047
|
}
|
|
2787
4048
|
function baselineCommand(args, deps) {
|
|
2788
4049
|
const policy = loadPolicy(args);
|
|
@@ -2821,6 +4082,9 @@ function baselineCommand(args, deps) {
|
|
|
2821
4082
|
};
|
|
2822
4083
|
}
|
|
2823
4084
|
function verifyCommand(args, deps) {
|
|
4085
|
+
if (flagBool(args.flags, "approved")) {
|
|
4086
|
+
return verifyApprovedMode(args, deps);
|
|
4087
|
+
}
|
|
2824
4088
|
const policy = loadPolicy(args);
|
|
2825
4089
|
if ("error" in policy) return { stdout: "", stderr: policy.error, exitCode: EXIT.ERROR };
|
|
2826
4090
|
const path = baselinePathFor(args, deps.cwd);
|
|
@@ -2852,6 +4116,57 @@ function verifyCommand(args, deps) {
|
|
|
2852
4116
|
const exitCode = flagBool(args.flags, "ci") && drift.drifted ? EXIT.DRIFT : EXIT.OK;
|
|
2853
4117
|
return { stdout, exitCode };
|
|
2854
4118
|
}
|
|
4119
|
+
function approvedPathFor(args, cwd) {
|
|
4120
|
+
const flag = flagStr(args.flags, "approved");
|
|
4121
|
+
return typeof flag === "string" && flag.length > 0 ? flag : defaultApprovedPath(cwd);
|
|
4122
|
+
}
|
|
4123
|
+
function verifyApprovedMode(args, deps) {
|
|
4124
|
+
const path = approvedPathFor(args, deps.cwd);
|
|
4125
|
+
const approved = readApproved(path);
|
|
4126
|
+
if (!approved) {
|
|
4127
|
+
return {
|
|
4128
|
+
stdout: "",
|
|
4129
|
+
stderr: `No approved state found at ${path}. Run \`calllint approve\` first.`,
|
|
4130
|
+
exitCode: EXIT.ERROR
|
|
4131
|
+
};
|
|
4132
|
+
}
|
|
4133
|
+
const current = decideRepoSurfaces(deps.cwd, {
|
|
4134
|
+
now: deps.now ?? (Date.parse(deps.generatedAt) || 0),
|
|
4135
|
+
generatedAt: deps.generatedAt
|
|
4136
|
+
});
|
|
4137
|
+
const drift = verifyApproved(current, approved, deps.generatedAt);
|
|
4138
|
+
const stdout = flagBool(args.flags, "json") ? renderApprovedDriftJson(drift) : renderApprovedDrift(drift);
|
|
4139
|
+
const exitCode = flagBool(args.flags, "ci") && drift.drifted ? EXIT.DRIFT : EXIT.OK;
|
|
4140
|
+
return { stdout, exitCode };
|
|
4141
|
+
}
|
|
4142
|
+
|
|
4143
|
+
// src/commands/approve.ts
|
|
4144
|
+
function approvedPathFor2(args, cwd) {
|
|
4145
|
+
return flagStr(args.flags, "approved") ?? defaultApprovedPath(cwd);
|
|
4146
|
+
}
|
|
4147
|
+
function approveCommand(args, deps) {
|
|
4148
|
+
const decisions = decideRepoSurfaces(deps.cwd, {
|
|
4149
|
+
now: deps.now,
|
|
4150
|
+
generatedAt: deps.generatedAt
|
|
4151
|
+
});
|
|
4152
|
+
const state = buildApproved(decisions, deps.generatedAt);
|
|
4153
|
+
const path = approvedPathFor2(args, deps.cwd);
|
|
4154
|
+
if (deps.writeFile !== false) {
|
|
4155
|
+
try {
|
|
4156
|
+
writeApproved(state, path);
|
|
4157
|
+
} catch (err) {
|
|
4158
|
+
return { stdout: "", stderr: `Could not write approved state: ${String(err)}`, exitCode: EXIT.ERROR };
|
|
4159
|
+
}
|
|
4160
|
+
}
|
|
4161
|
+
if (flagBool(args.flags, "json")) {
|
|
4162
|
+
return { stdout: JSON.stringify(state, null, 2), exitCode: EXIT.OK };
|
|
4163
|
+
}
|
|
4164
|
+
const n = state.approved.length;
|
|
4165
|
+
return {
|
|
4166
|
+
stdout: `Approved ${n} surface${n === 1 ? "" : "s"} \u2192 ${path}`,
|
|
4167
|
+
exitCode: EXIT.OK
|
|
4168
|
+
};
|
|
4169
|
+
}
|
|
2855
4170
|
|
|
2856
4171
|
// src/run.ts
|
|
2857
4172
|
function run(argv, deps) {
|
|
@@ -2861,6 +4176,19 @@ function run(argv, deps) {
|
|
|
2861
4176
|
return helpCommand();
|
|
2862
4177
|
}
|
|
2863
4178
|
switch (cmd) {
|
|
4179
|
+
case "check":
|
|
4180
|
+
return checkCommand(args, {
|
|
4181
|
+
cwd: deps.cwd,
|
|
4182
|
+
readStdin: deps.readStdin,
|
|
4183
|
+
now: deps.now,
|
|
4184
|
+
generatedAt: deps.generatedAt
|
|
4185
|
+
});
|
|
4186
|
+
case "scan-all":
|
|
4187
|
+
return scanAllCommand(args, {
|
|
4188
|
+
cwd: deps.cwd,
|
|
4189
|
+
now: deps.now,
|
|
4190
|
+
generatedAt: deps.generatedAt
|
|
4191
|
+
});
|
|
2864
4192
|
case "scan":
|
|
2865
4193
|
return scanCommand(args, {
|
|
2866
4194
|
cwd: deps.cwd,
|
|
@@ -2868,7 +4196,8 @@ function run(argv, deps) {
|
|
|
2868
4196
|
now: deps.now,
|
|
2869
4197
|
generatedAt: deps.generatedAt,
|
|
2870
4198
|
writeCacheFile: deps.writeCacheFile,
|
|
2871
|
-
online: deps.online
|
|
4199
|
+
online: deps.online,
|
|
4200
|
+
getChangedFilesDiff: deps.getChangedFilesDiff
|
|
2872
4201
|
});
|
|
2873
4202
|
case "diagnostics":
|
|
2874
4203
|
return diagnosticsCommand(args, {
|
|
@@ -2890,12 +4219,22 @@ function run(argv, deps) {
|
|
|
2890
4219
|
return verifyCommand(args, {
|
|
2891
4220
|
cwd: deps.cwd,
|
|
2892
4221
|
readStdin: deps.readStdin,
|
|
4222
|
+
now: deps.now,
|
|
2893
4223
|
generatedAt: deps.generatedAt,
|
|
2894
4224
|
writeBaselineFile: deps.writeCacheFile,
|
|
2895
4225
|
online: deps.online
|
|
2896
4226
|
});
|
|
4227
|
+
case "approve":
|
|
4228
|
+
return approveCommand(args, {
|
|
4229
|
+
cwd: deps.cwd,
|
|
4230
|
+
now: deps.now,
|
|
4231
|
+
generatedAt: deps.generatedAt,
|
|
4232
|
+
writeFile: deps.writeCacheFile
|
|
4233
|
+
});
|
|
2897
4234
|
case "explain":
|
|
2898
4235
|
return explainCommand(args, { cwd: deps.cwd });
|
|
4236
|
+
case "gen-rule":
|
|
4237
|
+
return genRuleCommand(args, { cwd: deps.cwd });
|
|
2899
4238
|
case "policy":
|
|
2900
4239
|
return policyCommand(args, { cwd: deps.cwd });
|
|
2901
4240
|
default:
|
|
@@ -3112,6 +4451,7 @@ function shouldBreathe(argv, deps = {}) {
|
|
|
3112
4451
|
const { flags } = parseArgs(argv);
|
|
3113
4452
|
if (flagBool(flags, "json")) return false;
|
|
3114
4453
|
if (flagBool(flags, "sarif")) return false;
|
|
4454
|
+
if (flagBool(flags, "markdown")) return false;
|
|
3115
4455
|
if (flagBool(flags, "html")) return false;
|
|
3116
4456
|
if (flagBool(flags, "compact")) return false;
|
|
3117
4457
|
if (flagBool(flags, "no-color")) return false;
|
|
@@ -3141,7 +4481,18 @@ async function breathe(argv, deps = {}) {
|
|
|
3141
4481
|
// src/index.ts
|
|
3142
4482
|
function readStdin() {
|
|
3143
4483
|
try {
|
|
3144
|
-
return
|
|
4484
|
+
return readFileSync8(0, "utf8");
|
|
4485
|
+
} catch {
|
|
4486
|
+
return "";
|
|
4487
|
+
}
|
|
4488
|
+
}
|
|
4489
|
+
function gitChangedFiles(cwd) {
|
|
4490
|
+
try {
|
|
4491
|
+
return execFileSync("git", ["diff", "--name-only", "HEAD"], {
|
|
4492
|
+
cwd,
|
|
4493
|
+
encoding: "utf8",
|
|
4494
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
4495
|
+
});
|
|
3145
4496
|
} catch {
|
|
3146
4497
|
return "";
|
|
3147
4498
|
}
|
|
@@ -3178,7 +4529,8 @@ async function main() {
|
|
|
3178
4529
|
readStdin,
|
|
3179
4530
|
now,
|
|
3180
4531
|
generatedAt,
|
|
3181
|
-
online
|
|
4532
|
+
online,
|
|
4533
|
+
getChangedFilesDiff: () => gitChangedFiles(process.cwd())
|
|
3182
4534
|
});
|
|
3183
4535
|
if (result.stdout) process.stdout.write(result.stdout + "\n");
|
|
3184
4536
|
if (result.stderr) process.stderr.write(result.stderr + "\n");
|