calllint 0.5.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.
Files changed (2) hide show
  1. package/dist/index.js +1223 -48
  2. package/package.json +1 -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 readFileSync6 } from "node:fs";
4
+ import { readFileSync as readFileSync8 } from "node:fs";
5
5
  import { execFileSync } from "node:child_process";
6
6
 
7
7
  // src/args.ts
@@ -57,15 +57,27 @@ USAGE
57
57
  calllint <command> [options]
58
58
 
59
59
  COMMANDS
60
- scan [target] Scan an MCP config file, or npm:<pkg> / github:<owner/repo>
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>
61
67
  diagnostics [target] Emit editor/agent-host diagnostics JSON (calllint.diagnostics.v0)
62
68
  baseline [target] Record the approved risk surface as a baseline
63
- verify [target] Compare a fresh scan against the baseline (drift / rug-pull)
64
- explain <server> Explain the verdict for one server from the last scan
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.)
65
71
  policy init Write a default calllint.policy.json
66
72
  policy explain Show the effective policy
67
73
  help Show this help
68
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
+
69
81
  TARGETS
70
82
  <path> A config file (default: detect common locations)
71
83
  npm:<pkg>[@ver] Synthesize a config for an npm package (offline)
@@ -86,19 +98,20 @@ SCAN OPTIONS
86
98
 
87
99
  VERIFY OPTIONS
88
100
  --baseline <file> Baseline path (default: .calllint/baseline.json)
89
- --ci Exit 40 if the risk surface drifted from the baseline
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)
90
104
  --json Emit the drift report JSON
91
105
 
92
106
  EXAMPLES
93
- calllint scan .cursor/mcp.json
94
- calllint scan --changed --markdown
95
- cat .cursor/mcp.json | calllint scan --stdin --json
96
- calllint scan ./mcp.json --ci --no-emoji
97
- calllint diagnostics ./mcp.json --json
98
- calllint scan npm:mcp-weather@1.0.0
99
- calllint scan github:owner/repo --online
100
- 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
101
113
  calllint verify ./mcp.json --ci
114
+ calllint approve && calllint verify --approved --ci
102
115
  calllint explain filesystem
103
116
  `;
104
117
  function helpCommand() {
@@ -106,7 +119,7 @@ function helpCommand() {
106
119
  }
107
120
 
108
121
  // src/commands/scan.ts
109
- import { join as join4, resolve as resolve2 } from "node:path";
122
+ import { join as join6, resolve as resolve2 } from "node:path";
110
123
 
111
124
  // ../../packages/policy/src/defaultPolicy.ts
112
125
  function defaultPolicy() {
@@ -364,6 +377,100 @@ function highestRiskClass(classes) {
364
377
  return worst;
365
378
  }
366
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
+
367
474
  // ../../packages/resolver/src/npmSpec.ts
368
475
  var PACKAGE_RUNNERS = /* @__PURE__ */ new Set(["npx", "pnpm", "yarn", "bunx", "uvx", "pipx"]);
369
476
  var SHELL_COMMANDS = /* @__PURE__ */ new Set([
@@ -1217,6 +1324,177 @@ function detectHiddenInstructions(ctx) {
1217
1324
  ];
1218
1325
  }
1219
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
+
1220
1498
  // ../../packages/static-analyzer/src/analyzeServerConfig.ts
1221
1499
  var DETECTORS = [
1222
1500
  detectBroadFilesystemPath,
@@ -1228,7 +1506,10 @@ var DETECTORS = [
1228
1506
  detectExternalMutation,
1229
1507
  detectFinancialAction,
1230
1508
  detectUnverifiedLocalSource,
1231
- detectHiddenInstructions
1509
+ detectHiddenInstructions,
1510
+ detectMessagingSend,
1511
+ detectOauthScope,
1512
+ detectGatewayRuntime
1232
1513
  ];
1233
1514
  function analyzeServerConfig(server) {
1234
1515
  const binding = resolveRuntimeBinding(server);
@@ -2050,6 +2331,617 @@ function readBaseline(path = defaultBaselinePath()) {
2050
2331
  }
2051
2332
  }
2052
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
+
2053
2945
  // ../../packages/report-renderer/src/style.ts
2054
2946
  var DEFAULT_STYLE = { emoji: true };
2055
2947
  var NO_EMOJI_STYLE = { emoji: false };
@@ -2132,6 +3024,42 @@ function renderCompact(summary, style = DEFAULT_STYLE) {
2132
3024
  return lines.join("\n");
2133
3025
  }
2134
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
+
2135
3063
  // ../../packages/report-renderer/src/renderExplain.ts
2136
3064
  function renderFindingDetail(f, n) {
2137
3065
  const lines = [];
@@ -2222,6 +3150,39 @@ function renderDrift(report) {
2222
3150
  return lines.join("\n");
2223
3151
  }
2224
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
+
2225
3186
  // ../../packages/report-renderer/src/renderSarif.ts
2226
3187
  function levelForSeverity(sev) {
2227
3188
  if (sev === "critical" || sev === "high") return "error";
@@ -2596,8 +3557,8 @@ function exitCodeFor(summary, policy) {
2596
3557
  }
2597
3558
 
2598
3559
  // src/commands/resolveInput.ts
2599
- import { existsSync as existsSync2, readFileSync as readFileSync3 } from "node:fs";
2600
- import { join as join2 } from "node:path";
3560
+ import { existsSync as existsSync3, readFileSync as readFileSync5 } from "node:fs";
3561
+ import { join as join4 } from "node:path";
2601
3562
  var DEFAULT_CONFIG_PATHS = [
2602
3563
  ".cursor/mcp.json",
2603
3564
  ".mcp.json",
@@ -2607,8 +3568,8 @@ var DEFAULT_CONFIG_PATHS = [
2607
3568
  ];
2608
3569
  function findDefaultConfig(cwd) {
2609
3570
  for (const rel of DEFAULT_CONFIG_PATHS) {
2610
- const p = join2(cwd, rel);
2611
- if (existsSync2(p)) return p;
3571
+ const p = join4(cwd, rel);
3572
+ if (existsSync3(p)) return p;
2612
3573
  }
2613
3574
  return void 0;
2614
3575
  }
@@ -2642,15 +3603,15 @@ function resolveConfigInput(args, deps) {
2642
3603
  exitCode: EXIT.USAGE
2643
3604
  };
2644
3605
  }
2645
- if (!existsSync2(resolved)) {
3606
+ if (!existsSync3(resolved)) {
2646
3607
  return { error: `File not found: ${resolved}`, exitCode: EXIT.USAGE };
2647
3608
  }
2648
- return { text: readFileSync3(resolved, "utf8"), configPath: resolved };
3609
+ return { text: readFileSync5(resolved, "utf8"), configPath: resolved };
2649
3610
  }
2650
3611
 
2651
3612
  // src/commands/changedConfigs.ts
2652
- import { basename as basename2, resolve } from "node:path";
2653
- import { existsSync as existsSync3 } from "node:fs";
3613
+ import { basename as basename3, resolve } from "node:path";
3614
+ import { existsSync as existsSync4 } from "node:fs";
2654
3615
  function changedConfigPaths(cwd, diff) {
2655
3616
  let raw;
2656
3617
  try {
@@ -2658,27 +3619,27 @@ function changedConfigPaths(cwd, diff) {
2658
3619
  } catch {
2659
3620
  return [];
2660
3621
  }
2661
- const knownLeaves = new Set(DEFAULT_CONFIG_PATHS.map((p) => basename2(p)));
3622
+ const knownLeaves = new Set(DEFAULT_CONFIG_PATHS.map((p) => basename3(p)));
2662
3623
  const knownPaths = new Set(DEFAULT_CONFIG_PATHS);
2663
3624
  return raw.split("\n").map((l) => l.trim()).filter(
2664
- (f) => f.length > 0 && (knownPaths.has(f) || DEFAULT_CONFIG_PATHS.some((p) => f.endsWith("/" + p)) || knownLeaves.has(basename2(f)))
2665
- ).map((f) => resolve(cwd, f)).filter((abs) => existsSync3(abs));
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));
2666
3627
  }
2667
3628
 
2668
3629
  // src/commands/surfaces.ts
2669
- import { existsSync as existsSync4, readFileSync as readFileSync4, statSync } from "node:fs";
2670
- import { join as join3 } from "node:path";
3630
+ import { existsSync as existsSync5, readFileSync as readFileSync6, statSync as statSync2 } from "node:fs";
3631
+ import { join as join5 } from "node:path";
2671
3632
  var SURFACE_SIZE_CAP = 256 * 1024;
2672
3633
  var SURFACE_FILES = [
2673
3634
  { file: "README.md", kind: "readme" },
2674
3635
  { file: "SKILL.md", kind: "skill" },
2675
3636
  { file: "AGENTS.md", kind: "agents" }
2676
3637
  ];
2677
- function readCapped(path) {
3638
+ function readCapped2(path) {
2678
3639
  try {
2679
- const st = statSync(path);
3640
+ const st = statSync2(path);
2680
3641
  if (!st.isFile()) return void 0;
2681
- const raw = readFileSync4(path, "utf8");
3642
+ const raw = readFileSync6(path, "utf8");
2682
3643
  if (raw.length > SURFACE_SIZE_CAP) {
2683
3644
  return { text: raw.slice(0, SURFACE_SIZE_CAP), truncated: true };
2684
3645
  }
@@ -2689,13 +3650,13 @@ function readCapped(path) {
2689
3650
  }
2690
3651
  function readDocumentSurfaces(dir) {
2691
3652
  const surfaces = [];
2692
- if (!existsSync4(dir)) return surfaces;
3653
+ if (!existsSync5(dir)) return surfaces;
2693
3654
  for (const { file, kind } of SURFACE_FILES) {
2694
- const got = readCapped(join3(dir, file));
3655
+ const got = readCapped2(join5(dir, file));
2695
3656
  if (got) surfaces.push({ path: file, kind, text: got.text, truncated: got.truncated });
2696
3657
  }
2697
- const pkgPath = join3(dir, "package.json");
2698
- const pkgRaw = readCapped(pkgPath);
3658
+ const pkgPath = join5(dir, "package.json");
3659
+ const pkgRaw = readCapped2(pkgPath);
2699
3660
  if (pkgRaw) {
2700
3661
  try {
2701
3662
  const pkg = JSON.parse(pkgRaw.text);
@@ -2715,7 +3676,7 @@ function readDocumentSurfaces(dir) {
2715
3676
  }
2716
3677
 
2717
3678
  // src/commands/scan.ts
2718
- import { readFileSync as readFileSync5 } from "node:fs";
3679
+ import { readFileSync as readFileSync7 } from "node:fs";
2719
3680
  function scanCommand(args, deps) {
2720
3681
  if (flagBool(args.flags, "changed")) {
2721
3682
  return scanChangedCommand(args, deps);
@@ -2762,7 +3723,7 @@ function scanOneConfig(text, configPath, policy, args, deps) {
2762
3723
  }
2763
3724
  if (deps.writeCacheFile !== false) {
2764
3725
  try {
2765
- writeCache(summary, join4(deps.cwd, ".calllint", "last-scan.json"));
3726
+ writeCache(summary, join6(deps.cwd, ".calllint", "last-scan.json"));
2766
3727
  } catch {
2767
3728
  }
2768
3729
  }
@@ -2806,7 +3767,7 @@ function scanChangedCommand(args, deps) {
2806
3767
  };
2807
3768
  }
2808
3769
  const results = paths.map((p) => {
2809
- const text = readFileSync5(p, "utf8");
3770
+ const text = readFileSync7(p, "utf8");
2810
3771
  return scanOneConfig(text, p, policy, args, deps);
2811
3772
  });
2812
3773
  const exitCode = results.reduce((worst, r) => Math.max(worst, r.exitCode), EXIT.OK);
@@ -2820,6 +3781,143 @@ function scanChangedCommand(args, deps) {
2820
3781
  return { stdout, exitCode, ...stderr ? { stderr } : {} };
2821
3782
  }
2822
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
+ }
3920
+
2823
3921
  // src/commands/diagnostics.ts
2824
3922
  function diagnosticsCommand(args, deps) {
2825
3923
  if (!flagBool(args.flags, "json")) {
@@ -2868,7 +3966,7 @@ function diagnosticsCommand(args, deps) {
2868
3966
  }
2869
3967
 
2870
3968
  // src/commands/explain.ts
2871
- import { join as join5 } from "node:path";
3969
+ import { join as join8 } from "node:path";
2872
3970
  function explainCommand(args, deps) {
2873
3971
  const serverName = args.positionals[0];
2874
3972
  if (!serverName) {
@@ -2878,7 +3976,7 @@ function explainCommand(args, deps) {
2878
3976
  exitCode: EXIT.USAGE
2879
3977
  };
2880
3978
  }
2881
- const summary = readCache(join5(deps.cwd, ".calllint", "last-scan.json"));
3979
+ const summary = readCache(join8(deps.cwd, ".calllint", "last-scan.json"));
2882
3980
  if (!summary) {
2883
3981
  return {
2884
3982
  stdout: "",
@@ -2900,20 +3998,20 @@ function explainCommand(args, deps) {
2900
3998
  }
2901
3999
 
2902
4000
  // src/commands/policy.ts
2903
- import { existsSync as existsSync5, writeFileSync as writeFileSync2 } from "node:fs";
2904
- import { join as join6 } from "node:path";
4001
+ import { existsSync as existsSync6, writeFileSync as writeFileSync4 } from "node:fs";
4002
+ import { join as join9 } from "node:path";
2905
4003
  function policyCommand(args, deps) {
2906
4004
  const sub = args.positionals[0];
2907
4005
  if (sub === "init") {
2908
- const path = join6(deps.cwd, "calllint.policy.json");
2909
- if (existsSync5(path) && !flagBool(args.flags, "force")) {
4006
+ const path = join9(deps.cwd, "calllint.policy.json");
4007
+ if (existsSync6(path) && !flagBool(args.flags, "force")) {
2910
4008
  return {
2911
4009
  stdout: "",
2912
4010
  stderr: `${path} already exists. Use --force to overwrite.`,
2913
4011
  exitCode: EXIT.USAGE
2914
4012
  };
2915
4013
  }
2916
- writeFileSync2(path, defaultPolicyJson(), "utf8");
4014
+ writeFileSync4(path, defaultPolicyJson(), "utf8");
2917
4015
  return { stdout: `Wrote default policy to ${path}`, exitCode: EXIT.OK };
2918
4016
  }
2919
4017
  if (sub === "explain") {
@@ -2936,7 +4034,7 @@ function policyCommand(args, deps) {
2936
4034
  }
2937
4035
 
2938
4036
  // src/commands/verify.ts
2939
- import { join as join7 } from "node:path";
4037
+ import { join as join10 } from "node:path";
2940
4038
  function loadPolicy(args) {
2941
4039
  try {
2942
4040
  return loadPolicyOrDefault(flagStr(args.flags, "policy"));
@@ -2945,7 +4043,7 @@ function loadPolicy(args) {
2945
4043
  }
2946
4044
  }
2947
4045
  function baselinePathFor(args, cwd) {
2948
- return flagStr(args.flags, "baseline") ?? join7(cwd, ".calllint", "baseline.json");
4046
+ return flagStr(args.flags, "baseline") ?? join10(cwd, ".calllint", "baseline.json");
2949
4047
  }
2950
4048
  function baselineCommand(args, deps) {
2951
4049
  const policy = loadPolicy(args);
@@ -2984,6 +4082,9 @@ function baselineCommand(args, deps) {
2984
4082
  };
2985
4083
  }
2986
4084
  function verifyCommand(args, deps) {
4085
+ if (flagBool(args.flags, "approved")) {
4086
+ return verifyApprovedMode(args, deps);
4087
+ }
2987
4088
  const policy = loadPolicy(args);
2988
4089
  if ("error" in policy) return { stdout: "", stderr: policy.error, exitCode: EXIT.ERROR };
2989
4090
  const path = baselinePathFor(args, deps.cwd);
@@ -3015,6 +4116,57 @@ function verifyCommand(args, deps) {
3015
4116
  const exitCode = flagBool(args.flags, "ci") && drift.drifted ? EXIT.DRIFT : EXIT.OK;
3016
4117
  return { stdout, exitCode };
3017
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
+ }
3018
4170
 
3019
4171
  // src/run.ts
3020
4172
  function run(argv, deps) {
@@ -3024,6 +4176,19 @@ function run(argv, deps) {
3024
4176
  return helpCommand();
3025
4177
  }
3026
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
+ });
3027
4192
  case "scan":
3028
4193
  return scanCommand(args, {
3029
4194
  cwd: deps.cwd,
@@ -3054,12 +4219,22 @@ function run(argv, deps) {
3054
4219
  return verifyCommand(args, {
3055
4220
  cwd: deps.cwd,
3056
4221
  readStdin: deps.readStdin,
4222
+ now: deps.now,
3057
4223
  generatedAt: deps.generatedAt,
3058
4224
  writeBaselineFile: deps.writeCacheFile,
3059
4225
  online: deps.online
3060
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
+ });
3061
4234
  case "explain":
3062
4235
  return explainCommand(args, { cwd: deps.cwd });
4236
+ case "gen-rule":
4237
+ return genRuleCommand(args, { cwd: deps.cwd });
3063
4238
  case "policy":
3064
4239
  return policyCommand(args, { cwd: deps.cwd });
3065
4240
  default:
@@ -3306,7 +4481,7 @@ async function breathe(argv, deps = {}) {
3306
4481
  // src/index.ts
3307
4482
  function readStdin() {
3308
4483
  try {
3309
- return readFileSync6(0, "utf8");
4484
+ return readFileSync8(0, "utf8");
3310
4485
  } catch {
3311
4486
  return "";
3312
4487
  }