calllint 1.4.0 → 1.5.1
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 +775 -25
- package/package.json +2 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync21 } from "node:fs";
|
|
5
5
|
import { execFileSync } from "node:child_process";
|
|
6
6
|
|
|
7
7
|
// src/args.ts
|
|
@@ -74,6 +74,10 @@ COMMANDS
|
|
|
74
74
|
diagnostics [target] Emit editor/agent-host diagnostics JSON (calllint.diagnostics.v0)
|
|
75
75
|
baseline [target] Record the approved risk surface as a baseline
|
|
76
76
|
approve Record the repo-wide capability surface as approved state (L4)
|
|
77
|
+
guard Continuous Guard: re-decide on authority change; silent when unchanged
|
|
78
|
+
guard install --host <h> Install a guard hook (git pre-commit, github workflow)
|
|
79
|
+
guard status Show baseline / disable / installed-hook state
|
|
80
|
+
guard disable Turn Continuous Guard off (.calllint/guard.json)
|
|
77
81
|
receipt verify <f> Validate a calllint.receipt.v0 (structure + signature if present)
|
|
78
82
|
receipt sign <f> Sign a receipt with a local key (--key; development/testing)
|
|
79
83
|
receipt keygen Generate a local ed25519 keypair (--out; development/testing)
|
|
@@ -122,6 +126,13 @@ VERIFY OPTIONS
|
|
|
122
126
|
--ci Exit 40 if the surface drifted (from baseline or approved state)
|
|
123
127
|
--json Emit the drift report JSON
|
|
124
128
|
|
|
129
|
+
GUARD OPTIONS
|
|
130
|
+
--host <h> guard install target: git (pre-commit) | github (workflow)
|
|
131
|
+
--approved [file] Approved baseline to diff against (default: .calllint/approved.json)
|
|
132
|
+
--json Emit the guard assessment / status JSON
|
|
133
|
+
(exit) silent/note=0, REVIEW=10, UNKNOWN=20, BLOCK=30; guard self-failure is non-zero
|
|
134
|
+
(env) CALLLINT_GUARD=0 disables the guard
|
|
135
|
+
|
|
125
136
|
EXAMPLES
|
|
126
137
|
calllint inventory # List discovered agent configs
|
|
127
138
|
calllint scan --auto # Discover and scan all agents
|
|
@@ -140,6 +151,8 @@ EXAMPLES
|
|
|
140
151
|
calllint inbox inspect gmail-reply.normalized.json
|
|
141
152
|
calllint verify ./mcp.json --ci
|
|
142
153
|
calllint approve && calllint verify --approved --ci
|
|
154
|
+
calllint approve && calllint guard install --host git # re-decide on every commit
|
|
155
|
+
calllint guard --json # authority-change assessment (silent when unchanged)
|
|
143
156
|
calllint explain filesystem
|
|
144
157
|
`;
|
|
145
158
|
function helpCommand() {
|
|
@@ -407,7 +420,10 @@ var REASON_CODES = [
|
|
|
407
420
|
"PROMPT_METADATA_INSTRUCTION",
|
|
408
421
|
"OAUTH_SCOPE_UNKNOWN_OR_EXPANDED",
|
|
409
422
|
"TOOL_DESCRIPTOR_CHANGED",
|
|
410
|
-
"LONG_RUNNING_GATEWAY_RUNTIME"
|
|
423
|
+
"LONG_RUNNING_GATEWAY_RUNTIME",
|
|
424
|
+
// #13 — appended last (ADR 0044) so the frozen order 0–11 is unchanged. Backed by
|
|
425
|
+
// the flow object (calllint.flow.v0), not a static detector; see REASON_CODE_META.
|
|
426
|
+
"TOXIC_FLOW_COMPOSITION"
|
|
411
427
|
];
|
|
412
428
|
var REASON_CODE_META = {
|
|
413
429
|
UNPINNED_PACKAGE: {
|
|
@@ -471,6 +487,16 @@ var REASON_CODE_META = {
|
|
|
471
487
|
backedBy: ["runtime.gateway"],
|
|
472
488
|
status: "wired",
|
|
473
489
|
label: "Long-running gateway runtime"
|
|
490
|
+
},
|
|
491
|
+
TOXIC_FLOW_COMPOSITION: {
|
|
492
|
+
// Backed by the flow object (calllint.flow.v0, built by the flow-analyzer package),
|
|
493
|
+
// NOT a static detector Finding. The synthetic backing id keeps every wired code
|
|
494
|
+
// naming its backing without inventing a new status value. No detector emits this id,
|
|
495
|
+
// so findingsToReasonCodes never fabricates the code — it is produced only by the
|
|
496
|
+
// explicit flow-fold step. See ADR 0044 / 0040.
|
|
497
|
+
backedBy: ["flow:toxic-composition"],
|
|
498
|
+
status: "wired",
|
|
499
|
+
label: "Cross-tool toxic-flow composition"
|
|
474
500
|
}
|
|
475
501
|
};
|
|
476
502
|
function reasonCodeForFinding(findingId) {
|
|
@@ -491,6 +517,9 @@ var VERDICT_NEXT_ACTION = {
|
|
|
491
517
|
// ../../packages/types/src/authority.ts
|
|
492
518
|
var AUTHORITY_SCHEMA_VERSION = "calllint.authority.v0";
|
|
493
519
|
|
|
520
|
+
// ../../packages/types/src/flow.ts
|
|
521
|
+
var FLOW_SCHEMA_VERSION = "calllint.flow.v0";
|
|
522
|
+
|
|
494
523
|
// ../../packages/types/src/trustDecision.ts
|
|
495
524
|
var DECISION_SCHEMA_VERSION = "calllint.decision.v0";
|
|
496
525
|
|
|
@@ -624,11 +653,13 @@ function evidenceFloor(evidence) {
|
|
|
624
653
|
function decideOverAuthority(input) {
|
|
625
654
|
const { authority, policy } = input;
|
|
626
655
|
const evidence = input.evidence ?? [];
|
|
627
|
-
const
|
|
656
|
+
const capabilityReasons = authority.capabilities.map((c) => {
|
|
628
657
|
const code = reasonCodeFor(c);
|
|
629
658
|
const contributes = moreSevere(baseVerdict(c), policyFloor(code, policy));
|
|
630
659
|
return { code, evidenceSource: c.evidenceSource, contributes };
|
|
631
660
|
});
|
|
661
|
+
const flowReasons = input.flowReasons ?? [];
|
|
662
|
+
const reasons = [...capabilityReasons, ...flowReasons];
|
|
632
663
|
const unknowns = [...authority.unknowns];
|
|
633
664
|
const contributions = reasons.map((r) => r.contributes);
|
|
634
665
|
if (authority.subject.artifactDigest === null) {
|
|
@@ -1863,6 +1894,21 @@ function analyzeDocumentSurfaces(surfaces) {
|
|
|
1863
1894
|
];
|
|
1864
1895
|
}
|
|
1865
1896
|
|
|
1897
|
+
// ../../packages/static-analyzer/src/trustSource.ts
|
|
1898
|
+
function classifyTrustSource(cap) {
|
|
1899
|
+
if (cap.action === "read" && cap.resource === "secret") return "sensitive.secret";
|
|
1900
|
+
if (cap.action === "execute" && cap.resource === "process" && cap.evidenceSource === "server.command") {
|
|
1901
|
+
return "trusted.local_project";
|
|
1902
|
+
}
|
|
1903
|
+
return "unknown";
|
|
1904
|
+
}
|
|
1905
|
+
function withTrustSource(caps) {
|
|
1906
|
+
return caps.map((c) => {
|
|
1907
|
+
const ts = classifyTrustSource(c);
|
|
1908
|
+
return ts === "unknown" ? { ...c } : { ...c, trustSource: ts };
|
|
1909
|
+
});
|
|
1910
|
+
}
|
|
1911
|
+
|
|
1866
1912
|
// ../../packages/static-analyzer/src/instructionAuthority.ts
|
|
1867
1913
|
var RULES = [
|
|
1868
1914
|
// 1. privilege-escalation → execute × process (irreversible, must be approved)
|
|
@@ -2068,7 +2114,7 @@ function extractInstructionAuthority(surfaces) {
|
|
|
2068
2114
|
}
|
|
2069
2115
|
}
|
|
2070
2116
|
}
|
|
2071
|
-
return sortCapabilities([...seen.values()]);
|
|
2117
|
+
return withTrustSource(sortCapabilities([...seen.values()]));
|
|
2072
2118
|
}
|
|
2073
2119
|
function sortCapabilities(caps) {
|
|
2074
2120
|
return caps.sort((a, b) => {
|
|
@@ -2155,7 +2201,7 @@ function deriveConfigCapabilities(server) {
|
|
|
2155
2201
|
completeness: "complete"
|
|
2156
2202
|
});
|
|
2157
2203
|
}
|
|
2158
|
-
return sortCapabilities(caps);
|
|
2204
|
+
return sortCapabilities(withTrustSource(caps));
|
|
2159
2205
|
}
|
|
2160
2206
|
|
|
2161
2207
|
// ../../packages/risk-engine/src/computeRiskClass.ts
|
|
@@ -3366,6 +3412,60 @@ function escalate(current) {
|
|
|
3366
3412
|
return current === "BLOCK" || current === "UNKNOWN" ? current : "REVIEW";
|
|
3367
3413
|
}
|
|
3368
3414
|
|
|
3415
|
+
// ../../packages/core/src/state/continuousGuard.ts
|
|
3416
|
+
function assessGuardDrift(report) {
|
|
3417
|
+
if (!report.drifted) {
|
|
3418
|
+
return {
|
|
3419
|
+
action: "silent",
|
|
3420
|
+
verdict: "SAFE",
|
|
3421
|
+
drifted: false,
|
|
3422
|
+
summary: "No authority change since the approved baseline."
|
|
3423
|
+
};
|
|
3424
|
+
}
|
|
3425
|
+
const changed = report.entries.filter((e) => e.status !== "unchanged");
|
|
3426
|
+
const n = changed.length;
|
|
3427
|
+
const surfaces = `${n} surface${n === 1 ? "" : "s"}`;
|
|
3428
|
+
switch (report.verdict) {
|
|
3429
|
+
case "BLOCK":
|
|
3430
|
+
return {
|
|
3431
|
+
action: "refuse",
|
|
3432
|
+
verdict: "BLOCK",
|
|
3433
|
+
drifted: true,
|
|
3434
|
+
summary: `Authority changed on ${surfaces}; the new surface is blocked by policy.`
|
|
3435
|
+
};
|
|
3436
|
+
case "UNKNOWN":
|
|
3437
|
+
return {
|
|
3438
|
+
action: "request-evidence",
|
|
3439
|
+
verdict: "UNKNOWN",
|
|
3440
|
+
drifted: true,
|
|
3441
|
+
summary: `Authority changed on ${surfaces}; the new surface could not be verified \u2014 evidence or approval required.`
|
|
3442
|
+
};
|
|
3443
|
+
case "REVIEW":
|
|
3444
|
+
return {
|
|
3445
|
+
action: "prompt",
|
|
3446
|
+
verdict: "REVIEW",
|
|
3447
|
+
drifted: true,
|
|
3448
|
+
summary: `Authority changed on ${surfaces}; review the new surface before proceeding.`
|
|
3449
|
+
};
|
|
3450
|
+
case "SAFE":
|
|
3451
|
+
return {
|
|
3452
|
+
action: "note",
|
|
3453
|
+
verdict: "SAFE",
|
|
3454
|
+
drifted: true,
|
|
3455
|
+
summary: `Authority changed on ${surfaces}; no new blockers observed.`
|
|
3456
|
+
};
|
|
3457
|
+
}
|
|
3458
|
+
}
|
|
3459
|
+
function guardFailClosed(failure) {
|
|
3460
|
+
return {
|
|
3461
|
+
action: "fail-closed",
|
|
3462
|
+
verdict: "UNKNOWN",
|
|
3463
|
+
drifted: false,
|
|
3464
|
+
summary: "Continuous Guard could not verify the authority surface; failing closed.",
|
|
3465
|
+
failure
|
|
3466
|
+
};
|
|
3467
|
+
}
|
|
3468
|
+
|
|
3369
3469
|
// ../../packages/core/src/distribution/agentRule.ts
|
|
3370
3470
|
var RELEVANT_SURFACES = [
|
|
3371
3471
|
".cursor/mcp.json",
|
|
@@ -3499,6 +3599,64 @@ ${rule()}
|
|
|
3499
3599
|
}
|
|
3500
3600
|
}
|
|
3501
3601
|
|
|
3602
|
+
// ../../packages/core/src/distribution/ciGate.ts
|
|
3603
|
+
var CI_GATE_PATHS = [
|
|
3604
|
+
"**/mcp.json",
|
|
3605
|
+
"**/.mcp.json",
|
|
3606
|
+
"**/settings.json",
|
|
3607
|
+
"**/config.toml",
|
|
3608
|
+
".cursor/**",
|
|
3609
|
+
".vscode/**",
|
|
3610
|
+
"claude_desktop_config.json",
|
|
3611
|
+
"CLAUDE.md",
|
|
3612
|
+
"AGENTS.md",
|
|
3613
|
+
".calllint/approved.json"
|
|
3614
|
+
];
|
|
3615
|
+
function pathsBlock() {
|
|
3616
|
+
return CI_GATE_PATHS.map((p) => ` - ${JSON.stringify(p)}`).join("\n");
|
|
3617
|
+
}
|
|
3618
|
+
var DRIFT_STEP = ` - name: CallLint drift gate
|
|
3619
|
+
shell: bash
|
|
3620
|
+
run: |
|
|
3621
|
+
if [ -f .calllint/approved.json ]; then
|
|
3622
|
+
npx -y calllint verify --approved --ci --no-emoji
|
|
3623
|
+
else
|
|
3624
|
+
echo "No .calllint/approved.json \u2014 seed it with: npx calllint approve"
|
|
3625
|
+
echo "Running report-only scan-all (not gating)."
|
|
3626
|
+
npx -y calllint scan-all --no-emoji || true
|
|
3627
|
+
fi`;
|
|
3628
|
+
var SCAN_ALL_STEP = ` - name: CallLint scan-all (report-only)
|
|
3629
|
+
shell: bash
|
|
3630
|
+
run: npx -y calllint scan-all --no-emoji || true`;
|
|
3631
|
+
function renderCiGate(opts = {}) {
|
|
3632
|
+
const mode = opts.mode ?? "drift";
|
|
3633
|
+
const step = mode === "scan-all" ? SCAN_ALL_STEP : DRIFT_STEP;
|
|
3634
|
+
const headline = mode === "scan-all" ? "CallLint agent-tool surface report (report-only; never gates)." : "CallLint agent-tool surface drift gate (fails on approved-state drift).";
|
|
3635
|
+
return `# ${headline}
|
|
3636
|
+
# Generated by \`calllint\` (generate_ci_gate_snippet). CallLint is static and
|
|
3637
|
+
# NEVER executes a scanned server.
|
|
3638
|
+
name: calllint
|
|
3639
|
+
|
|
3640
|
+
on:
|
|
3641
|
+
pull_request:
|
|
3642
|
+
paths:
|
|
3643
|
+
${pathsBlock()}
|
|
3644
|
+
|
|
3645
|
+
permissions:
|
|
3646
|
+
contents: read
|
|
3647
|
+
|
|
3648
|
+
jobs:
|
|
3649
|
+
surface-gate:
|
|
3650
|
+
runs-on: ubuntu-latest
|
|
3651
|
+
steps:
|
|
3652
|
+
- uses: actions/checkout@v6
|
|
3653
|
+
- uses: actions/setup-node@v6
|
|
3654
|
+
with:
|
|
3655
|
+
node-version: 20
|
|
3656
|
+
${step}
|
|
3657
|
+
`;
|
|
3658
|
+
}
|
|
3659
|
+
|
|
3502
3660
|
// ../../packages/core/src/receipt/createReceipt.ts
|
|
3503
3661
|
import { randomBytes } from "node:crypto";
|
|
3504
3662
|
function newReceiptId() {
|
|
@@ -5202,8 +5360,7 @@ var WindsurfExtractor = class extends BaseAgentExtractor {
|
|
|
5202
5360
|
return configs;
|
|
5203
5361
|
}
|
|
5204
5362
|
getUserConfigPath() {
|
|
5205
|
-
|
|
5206
|
-
return join10(appDataDir, "Windsurf", "mcp.json");
|
|
5363
|
+
return join10(this.resolveHome(), ".codeium", "mcp_config.json");
|
|
5207
5364
|
}
|
|
5208
5365
|
createConfig(configPath) {
|
|
5209
5366
|
const exists = this.isValidConfig(configPath);
|
|
@@ -7918,6 +8075,235 @@ import { existsSync as existsSync11, mkdirSync as mkdirSync5, readdirSync as rea
|
|
|
7918
8075
|
import { basename as basename4, join as join16, resolve as resolvePath4 } from "node:path";
|
|
7919
8076
|
import { homedir, userInfo } from "node:os";
|
|
7920
8077
|
|
|
8078
|
+
// ../../packages/flow-analyzer/src/flowRules.ts
|
|
8079
|
+
var UNTRUSTED_OR_SENSITIVE = /* @__PURE__ */ new Set([
|
|
8080
|
+
"sensitive.secret",
|
|
8081
|
+
"sensitive.private_data",
|
|
8082
|
+
"untrusted.public_content",
|
|
8083
|
+
"untrusted.tool_output",
|
|
8084
|
+
"untrusted.peer_agent",
|
|
8085
|
+
"untrusted.memory"
|
|
8086
|
+
]);
|
|
8087
|
+
var TRUSTED = /* @__PURE__ */ new Set([
|
|
8088
|
+
"trusted.policy",
|
|
8089
|
+
"trusted.user_explicit",
|
|
8090
|
+
"trusted.local_project",
|
|
8091
|
+
"trusted.signed_component"
|
|
8092
|
+
]);
|
|
8093
|
+
function isUntrustedOrSensitive(ts) {
|
|
8094
|
+
return ts !== void 0 && UNTRUSTED_OR_SENSITIVE.has(ts);
|
|
8095
|
+
}
|
|
8096
|
+
function isTrusted(ts) {
|
|
8097
|
+
return ts !== void 0 && TRUSTED.has(ts);
|
|
8098
|
+
}
|
|
8099
|
+
function sinkKind(sink) {
|
|
8100
|
+
const key = `${sink.action}:${sink.resource}`;
|
|
8101
|
+
if (key === "send:network" || key === "connect:network") return "network";
|
|
8102
|
+
if (key === "send:message") return "message";
|
|
8103
|
+
if (key === "spend:financial") return "financial";
|
|
8104
|
+
return "other";
|
|
8105
|
+
}
|
|
8106
|
+
function hasExternalDestination(sink) {
|
|
8107
|
+
return typeof sink.destination === "string" && sink.destination.length > 0;
|
|
8108
|
+
}
|
|
8109
|
+
var FLOW_RULES = [
|
|
8110
|
+
// CL-FLOW-001 — the canonical toxic flow: untrusted/sensitive data → external network
|
|
8111
|
+
// egress with a concrete outbound destination. The motivating incident (new9): a
|
|
8112
|
+
// secret / injected public content leaving to an attacker-controlled host.
|
|
8113
|
+
{
|
|
8114
|
+
id: "CL-FLOW-001",
|
|
8115
|
+
when: (s, k) => isUntrustedOrSensitive(s.trustSource) && sinkKind(k) === "network" && hasExternalDestination(k),
|
|
8116
|
+
decisionHint: "BLOCK",
|
|
8117
|
+
riskClass: "critical"
|
|
8118
|
+
},
|
|
8119
|
+
// CL-FLOW-002 — untrusted/sensitive data → financial action (spend). Money leaving is
|
|
8120
|
+
// irreversible; a tainted source driving it is a blocker regardless of destination.
|
|
8121
|
+
{
|
|
8122
|
+
id: "CL-FLOW-002",
|
|
8123
|
+
when: (s, k) => isUntrustedOrSensitive(s.trustSource) && sinkKind(k) === "financial",
|
|
8124
|
+
decisionHint: "BLOCK",
|
|
8125
|
+
riskClass: "critical"
|
|
8126
|
+
},
|
|
8127
|
+
// CL-FLOW-003 — untrusted/sensitive data → external network egress WITHOUT a pinned
|
|
8128
|
+
// destination (host unknown). Still dangerous, but the sink is less determined, so it
|
|
8129
|
+
// warrants human review rather than an outright block.
|
|
8130
|
+
{
|
|
8131
|
+
id: "CL-FLOW-003",
|
|
8132
|
+
when: (s, k) => isUntrustedOrSensitive(s.trustSource) && sinkKind(k) === "network" && !hasExternalDestination(k),
|
|
8133
|
+
decisionHint: "REVIEW",
|
|
8134
|
+
riskClass: "high"
|
|
8135
|
+
},
|
|
8136
|
+
// CL-FLOW-004 — untrusted/sensitive data → outbound messaging (email/chat). Exfiltration
|
|
8137
|
+
// via a messaging channel; review-worthy.
|
|
8138
|
+
{
|
|
8139
|
+
id: "CL-FLOW-004",
|
|
8140
|
+
when: (s, k) => isUntrustedOrSensitive(s.trustSource) && sinkKind(k) === "message",
|
|
8141
|
+
decisionHint: "REVIEW",
|
|
8142
|
+
riskClass: "high"
|
|
8143
|
+
},
|
|
8144
|
+
// CL-FLOW-ALLOW-001 — an established TRUSTED source reaching an egress sink is the
|
|
8145
|
+
// benign counterpart (ADR 0041 §2: trusted.user_explicit → send is routine). Only fires
|
|
8146
|
+
// when the source is positively trusted — never on unknown (I-04).
|
|
8147
|
+
{
|
|
8148
|
+
id: "CL-FLOW-ALLOW-001",
|
|
8149
|
+
when: (s, k) => isTrusted(s.trustSource) && sinkKind(k) !== "other",
|
|
8150
|
+
decisionHint: "ALLOW",
|
|
8151
|
+
riskClass: "none"
|
|
8152
|
+
},
|
|
8153
|
+
// CL-FLOW-REVIEW-000 — fail-safe catch-all. Any composition not positively classified
|
|
8154
|
+
// above (including one whose source trust could not be established) is REVIEW, never
|
|
8155
|
+
// ALLOW/SAFE. This keeps the classifier total and dangerous-flow-never-SAFE.
|
|
8156
|
+
{
|
|
8157
|
+
id: "CL-FLOW-REVIEW-000",
|
|
8158
|
+
when: () => true,
|
|
8159
|
+
decisionHint: "REVIEW",
|
|
8160
|
+
riskClass: "medium"
|
|
8161
|
+
}
|
|
8162
|
+
];
|
|
8163
|
+
function classifyFlow(source, sink) {
|
|
8164
|
+
for (const rule2 of FLOW_RULES) {
|
|
8165
|
+
if (rule2.when(source, sink)) {
|
|
8166
|
+
return { ruleId: rule2.id, decisionHint: rule2.decisionHint, riskClass: rule2.riskClass };
|
|
8167
|
+
}
|
|
8168
|
+
}
|
|
8169
|
+
return { ruleId: "CL-FLOW-REVIEW-000", decisionHint: "REVIEW", riskClass: "medium" };
|
|
8170
|
+
}
|
|
8171
|
+
|
|
8172
|
+
// ../../packages/flow-analyzer/src/buildFlows.ts
|
|
8173
|
+
var CLASSIFIED_SOURCE_TRUST = /* @__PURE__ */ new Set([
|
|
8174
|
+
"sensitive.secret",
|
|
8175
|
+
"sensitive.private_data",
|
|
8176
|
+
"untrusted.public_content",
|
|
8177
|
+
"untrusted.tool_output",
|
|
8178
|
+
"untrusted.peer_agent",
|
|
8179
|
+
"untrusted.memory",
|
|
8180
|
+
"trusted.policy",
|
|
8181
|
+
"trusted.user_explicit",
|
|
8182
|
+
"trusted.local_project",
|
|
8183
|
+
"trusted.signed_component"
|
|
8184
|
+
]);
|
|
8185
|
+
var EGRESS_SINKS = /* @__PURE__ */ new Set([
|
|
8186
|
+
"send:network",
|
|
8187
|
+
"send:message",
|
|
8188
|
+
"connect:network",
|
|
8189
|
+
"spend:financial"
|
|
8190
|
+
]);
|
|
8191
|
+
function isSource(c) {
|
|
8192
|
+
return c.action === "read" && c.trustSource !== void 0 && CLASSIFIED_SOURCE_TRUST.has(c.trustSource);
|
|
8193
|
+
}
|
|
8194
|
+
function isSink(c) {
|
|
8195
|
+
return EGRESS_SINKS.has(`${c.action}:${c.resource}`);
|
|
8196
|
+
}
|
|
8197
|
+
function sourceFamily(ts) {
|
|
8198
|
+
if (ts.startsWith("sensitive.")) return "sensitive";
|
|
8199
|
+
if (ts.startsWith("trusted.")) return "trusted";
|
|
8200
|
+
return "untrusted";
|
|
8201
|
+
}
|
|
8202
|
+
var SEVERITY = {
|
|
8203
|
+
none: 0,
|
|
8204
|
+
low: 20,
|
|
8205
|
+
medium: 50,
|
|
8206
|
+
high: 75,
|
|
8207
|
+
critical: 95
|
|
8208
|
+
};
|
|
8209
|
+
function cmp3(a, b) {
|
|
8210
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
8211
|
+
}
|
|
8212
|
+
function capKey(t) {
|
|
8213
|
+
const c = t.cap;
|
|
8214
|
+
return [
|
|
8215
|
+
t.manifestDigest,
|
|
8216
|
+
c.trustSource ?? "",
|
|
8217
|
+
c.action,
|
|
8218
|
+
c.resource,
|
|
8219
|
+
c.scope ?? "",
|
|
8220
|
+
c.destination ?? "",
|
|
8221
|
+
c.evidenceSource
|
|
8222
|
+
].join("|");
|
|
8223
|
+
}
|
|
8224
|
+
function sealFlow(flow) {
|
|
8225
|
+
return { ...flow, digest: hashJson(flow) };
|
|
8226
|
+
}
|
|
8227
|
+
function buildFlows(manifests) {
|
|
8228
|
+
const tagged = manifests.flatMap((m) => m.capabilities.map((cap) => ({ cap, manifestDigest: m.digest }))).sort((a, b) => cmp3(capKey(a), capKey(b)));
|
|
8229
|
+
const sources = tagged.filter((t) => isSource(t.cap));
|
|
8230
|
+
const sinks = tagged.filter((t) => isSink(t.cap));
|
|
8231
|
+
const byDigest = /* @__PURE__ */ new Map();
|
|
8232
|
+
for (const src of sources) {
|
|
8233
|
+
for (const snk of sinks) {
|
|
8234
|
+
if (src.cap === snk.cap) continue;
|
|
8235
|
+
const ts = src.cap.trustSource;
|
|
8236
|
+
const outcome = classifyFlow(src.cap, snk.cap);
|
|
8237
|
+
const risk = {
|
|
8238
|
+
class: outcome.riskClass,
|
|
8239
|
+
severity: SEVERITY[outcome.riskClass]
|
|
8240
|
+
};
|
|
8241
|
+
const sinkTag = `${snk.cap.action}-${snk.cap.resource}`;
|
|
8242
|
+
const flowId = `flow:${sourceFamily(ts)}-to-${sinkTag}`;
|
|
8243
|
+
const evidence = [.../* @__PURE__ */ new Set([src.cap.evidenceSource, snk.cap.evidenceSource])].sort();
|
|
8244
|
+
const authorityDigests = [.../* @__PURE__ */ new Set([src.manifestDigest, snk.manifestDigest])].sort();
|
|
8245
|
+
const flow = sealFlow({
|
|
8246
|
+
schema: FLOW_SCHEMA_VERSION,
|
|
8247
|
+
flowId,
|
|
8248
|
+
source: { trustSource: ts, evidence: [src.cap.evidenceSource] },
|
|
8249
|
+
steps: [{ action: src.cap.action, resource: src.cap.resource, scope: src.cap.scope }],
|
|
8250
|
+
sink: {
|
|
8251
|
+
action: snk.cap.action,
|
|
8252
|
+
resource: snk.cap.resource,
|
|
8253
|
+
destination: snk.cap.destination
|
|
8254
|
+
},
|
|
8255
|
+
risk,
|
|
8256
|
+
decisionHint: outcome.decisionHint,
|
|
8257
|
+
evidence,
|
|
8258
|
+
authorityDigests
|
|
8259
|
+
});
|
|
8260
|
+
if (!byDigest.has(flow.digest)) byDigest.set(flow.digest, flow);
|
|
8261
|
+
}
|
|
8262
|
+
}
|
|
8263
|
+
return [...byDigest.values()].sort(
|
|
8264
|
+
(a, b) => cmp3(a.flowId, b.flowId) || cmp3(a.digest, b.digest)
|
|
8265
|
+
);
|
|
8266
|
+
}
|
|
8267
|
+
|
|
8268
|
+
// ../../packages/flow-analyzer/src/foldFlows.ts
|
|
8269
|
+
function hintToVerdict(hint) {
|
|
8270
|
+
switch (hint) {
|
|
8271
|
+
case "BLOCK":
|
|
8272
|
+
return "BLOCK";
|
|
8273
|
+
case "REVIEW":
|
|
8274
|
+
return "REVIEW";
|
|
8275
|
+
default:
|
|
8276
|
+
return null;
|
|
8277
|
+
}
|
|
8278
|
+
}
|
|
8279
|
+
function foldFlowsIntoReasons(flows) {
|
|
8280
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
8281
|
+
for (const flow of flows) {
|
|
8282
|
+
const contributes = hintToVerdict(flow.decisionHint);
|
|
8283
|
+
if (contributes === null) continue;
|
|
8284
|
+
const firstEvidence = flow.evidence[0];
|
|
8285
|
+
const evidenceSource = firstEvidence ? `${flow.flowId} (${firstEvidence})` : flow.flowId;
|
|
8286
|
+
const reason = {
|
|
8287
|
+
code: "TOXIC_FLOW_COMPOSITION",
|
|
8288
|
+
evidenceSource,
|
|
8289
|
+
contributes
|
|
8290
|
+
};
|
|
8291
|
+
const prev = byKey.get(evidenceSource);
|
|
8292
|
+
if (!prev || severity(contributes) > severity(prev.contributes)) {
|
|
8293
|
+
byKey.set(evidenceSource, reason);
|
|
8294
|
+
}
|
|
8295
|
+
}
|
|
8296
|
+
return [...byKey.values()].sort(
|
|
8297
|
+
(a, b) => cmp4(a.evidenceSource, b.evidenceSource) || severity(b.contributes) - severity(a.contributes)
|
|
8298
|
+
);
|
|
8299
|
+
}
|
|
8300
|
+
function severity(v) {
|
|
8301
|
+
return v === "BLOCK" ? 3 : v === "UNKNOWN" ? 2 : v === "REVIEW" ? 1 : 0;
|
|
8302
|
+
}
|
|
8303
|
+
function cmp4(a, b) {
|
|
8304
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
8305
|
+
}
|
|
8306
|
+
|
|
7921
8307
|
// ../../packages/install-planner/src/buildPlan.ts
|
|
7922
8308
|
function ptr(segment) {
|
|
7923
8309
|
return segment.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
@@ -8517,9 +8903,91 @@ function claudeCodeServerEntry(server) {
|
|
|
8517
8903
|
return entry;
|
|
8518
8904
|
}
|
|
8519
8905
|
|
|
8906
|
+
// ../../packages/install-planner/src/adapters/cursor.ts
|
|
8907
|
+
var CURSOR_HOST_ID = "cursor";
|
|
8908
|
+
var CURSOR_TIER = "A";
|
|
8909
|
+
var cursorAdapter = {
|
|
8910
|
+
id: CURSOR_HOST_ID,
|
|
8911
|
+
tier: CURSOR_TIER,
|
|
8912
|
+
createPlan(ctx, upstream) {
|
|
8913
|
+
return buildInstallPlan({ ...ctx, host: CURSOR_HOST_ID, tier: CURSOR_TIER }, upstream);
|
|
8914
|
+
},
|
|
8915
|
+
validatePlan(plan) {
|
|
8916
|
+
return validatePlan(plan);
|
|
8917
|
+
},
|
|
8918
|
+
applyPlan(plan, ctx) {
|
|
8919
|
+
return applyPlan({
|
|
8920
|
+
plan,
|
|
8921
|
+
approvalDigest: ctx.approvalDigest,
|
|
8922
|
+
configPath: ctx.configPath,
|
|
8923
|
+
backupPath: ctx.backupPath,
|
|
8924
|
+
lockPath: ctx.lockPath,
|
|
8925
|
+
fs: ctx.fs,
|
|
8926
|
+
now: ctx.now
|
|
8927
|
+
});
|
|
8928
|
+
}
|
|
8929
|
+
};
|
|
8930
|
+
function cursorServerEntry(server) {
|
|
8931
|
+
const entry = {};
|
|
8932
|
+
if (server.url) {
|
|
8933
|
+
entry["url"] = server.url;
|
|
8934
|
+
} else if (server.command) {
|
|
8935
|
+
entry["command"] = server.command;
|
|
8936
|
+
entry["args"] = server.args ?? [];
|
|
8937
|
+
}
|
|
8938
|
+
if (server.envKeys && server.envKeys.length > 0) {
|
|
8939
|
+
const env = {};
|
|
8940
|
+
for (const k of [...server.envKeys].sort()) env[k] = "";
|
|
8941
|
+
entry["env"] = env;
|
|
8942
|
+
}
|
|
8943
|
+
return entry;
|
|
8944
|
+
}
|
|
8945
|
+
|
|
8946
|
+
// ../../packages/install-planner/src/adapters/windsurf.ts
|
|
8947
|
+
var WINDSURF_HOST_ID = "windsurf";
|
|
8948
|
+
var WINDSURF_TIER = "A";
|
|
8949
|
+
var windsurfAdapter = {
|
|
8950
|
+
id: WINDSURF_HOST_ID,
|
|
8951
|
+
tier: WINDSURF_TIER,
|
|
8952
|
+
createPlan(ctx, upstream) {
|
|
8953
|
+
return buildInstallPlan({ ...ctx, host: WINDSURF_HOST_ID, tier: WINDSURF_TIER }, upstream);
|
|
8954
|
+
},
|
|
8955
|
+
validatePlan(plan) {
|
|
8956
|
+
return validatePlan(plan);
|
|
8957
|
+
},
|
|
8958
|
+
applyPlan(plan, ctx) {
|
|
8959
|
+
return applyPlan({
|
|
8960
|
+
plan,
|
|
8961
|
+
approvalDigest: ctx.approvalDigest,
|
|
8962
|
+
configPath: ctx.configPath,
|
|
8963
|
+
backupPath: ctx.backupPath,
|
|
8964
|
+
lockPath: ctx.lockPath,
|
|
8965
|
+
fs: ctx.fs,
|
|
8966
|
+
now: ctx.now
|
|
8967
|
+
});
|
|
8968
|
+
}
|
|
8969
|
+
};
|
|
8970
|
+
function windsurfServerEntry(server) {
|
|
8971
|
+
const entry = {};
|
|
8972
|
+
if (server.url) {
|
|
8973
|
+
entry["serverUrl"] = server.url;
|
|
8974
|
+
} else if (server.command) {
|
|
8975
|
+
entry["command"] = server.command;
|
|
8976
|
+
entry["args"] = server.args ?? [];
|
|
8977
|
+
}
|
|
8978
|
+
if (server.envKeys && server.envKeys.length > 0) {
|
|
8979
|
+
const env = {};
|
|
8980
|
+
for (const k of [...server.envKeys].sort()) env[k] = "";
|
|
8981
|
+
entry["env"] = env;
|
|
8982
|
+
}
|
|
8983
|
+
return entry;
|
|
8984
|
+
}
|
|
8985
|
+
|
|
8520
8986
|
// ../../packages/install-planner/src/index.ts
|
|
8521
8987
|
var HOST_ADAPTERS = {
|
|
8522
|
-
[claudeCodeAdapter.id]: claudeCodeAdapter
|
|
8988
|
+
[claudeCodeAdapter.id]: claudeCodeAdapter,
|
|
8989
|
+
[cursorAdapter.id]: cursorAdapter,
|
|
8990
|
+
[windsurfAdapter.id]: windsurfAdapter
|
|
8523
8991
|
};
|
|
8524
8992
|
function getHostAdapter(host) {
|
|
8525
8993
|
return HOST_ADAPTERS[host] ?? null;
|
|
@@ -8694,8 +9162,10 @@ function buildAuthorityForTarget(input, artifactDigest) {
|
|
|
8694
9162
|
}
|
|
8695
9163
|
return buildAuthorityManifest({ artifactDigest, servers, surfaces });
|
|
8696
9164
|
}
|
|
8697
|
-
function defaultHostConfigPath(host) {
|
|
9165
|
+
function defaultHostConfigPath(host, cwd) {
|
|
8698
9166
|
if (host === CLAUDE_CODE_HOST_ID) return join16(homedir(), ".claude.json");
|
|
9167
|
+
if (host === CURSOR_HOST_ID) return join16(cwd, ".cursor", "mcp.json");
|
|
9168
|
+
if (host === WINDSURF_HOST_ID) return join16(homedir(), ".codeium", "mcp_config.json");
|
|
8699
9169
|
return null;
|
|
8700
9170
|
}
|
|
8701
9171
|
function plannedServersFor(input, host) {
|
|
@@ -8706,19 +9176,25 @@ function plannedServersFor(input, host) {
|
|
|
8706
9176
|
} catch {
|
|
8707
9177
|
return [];
|
|
8708
9178
|
}
|
|
9179
|
+
const entryByHost = {
|
|
9180
|
+
[CLAUDE_CODE_HOST_ID]: claudeCodeServerEntry,
|
|
9181
|
+
[CURSOR_HOST_ID]: cursorServerEntry,
|
|
9182
|
+
[WINDSURF_HOST_ID]: windsurfServerEntry
|
|
9183
|
+
};
|
|
9184
|
+
const entryFor = entryByHost[host] ?? claudeCodeServerEntry;
|
|
8709
9185
|
return servers.map((s) => ({
|
|
8710
9186
|
name: s.name,
|
|
8711
|
-
entry:
|
|
9187
|
+
entry: entryFor({ command: s.command, args: s.args, url: s.url, envKeys: s.envKeys })
|
|
8712
9188
|
}));
|
|
8713
9189
|
}
|
|
8714
9190
|
function buildPlanForHost(host, input, artifactDigest, authority, decision, deps, configPathOverride) {
|
|
8715
9191
|
const adapter = getHostAdapter(host);
|
|
8716
9192
|
if (!adapter) {
|
|
8717
|
-
return { error: `Unknown host "${host}". Known hosts: ${
|
|
9193
|
+
return { error: `Unknown host "${host}". Known hosts: ${knownHostList()}`, exitCode: EXIT.USAGE };
|
|
8718
9194
|
}
|
|
8719
9195
|
const servers = plannedServersFor(input, host);
|
|
8720
9196
|
if (servers.length === 0) return null;
|
|
8721
|
-
const configPath = configPathOverride ? resolvePath4(deps.cwd, configPathOverride) : defaultHostConfigPath(host);
|
|
9197
|
+
const configPath = configPathOverride ? resolvePath4(deps.cwd, configPathOverride) : defaultHostConfigPath(host, deps.cwd);
|
|
8722
9198
|
if (!configPath) {
|
|
8723
9199
|
return { error: `No default config path known for host "${host}"; pass --host-config <path>`, exitCode: EXIT.USAGE };
|
|
8724
9200
|
}
|
|
@@ -8812,7 +9288,9 @@ function trustPrepare(args, deps) {
|
|
|
8812
9288
|
const authority = buildAuthorityForTarget(input, artifact.digest ?? null);
|
|
8813
9289
|
const policyPath = flagStr(args.flags, "policy");
|
|
8814
9290
|
const policy = loadPolicyOrDefault(policyPath ? resolvePath4(deps.cwd, policyPath) : void 0);
|
|
8815
|
-
const
|
|
9291
|
+
const flows = buildFlows([authority]);
|
|
9292
|
+
const flowReasons = foldFlowsIntoReasons(flows);
|
|
9293
|
+
const decision = artifact.resolution === "resolved" ? decideOverAuthority({ authority, evidence, policy, flowReasons }) : void 0;
|
|
8816
9294
|
let plan;
|
|
8817
9295
|
const hostFlag = flagStr(args.flags, "host");
|
|
8818
9296
|
if (hostFlag && decision && decision.verdict !== "UNKNOWN") {
|
|
@@ -8839,10 +9317,18 @@ function trustPrepare(args, deps) {
|
|
|
8839
9317
|
plan written: ${file}
|
|
8840
9318
|
`;
|
|
8841
9319
|
}
|
|
9320
|
+
const showFlows = flagBool(args.flags, "flows");
|
|
8842
9321
|
if (flagBool(args.flags, "json")) {
|
|
8843
|
-
|
|
9322
|
+
const payload = showFlows ? { preparation, flows } : preparation;
|
|
9323
|
+
return { stdout: JSON.stringify(payload, null, 2), stderr: "", exitCode };
|
|
8844
9324
|
}
|
|
8845
|
-
|
|
9325
|
+
const flowNote = showFlows ? renderFlows(flows) : "";
|
|
9326
|
+
const conversion = renderConversionPrompt(preparation);
|
|
9327
|
+
return {
|
|
9328
|
+
stdout: renderPreparation(preparation) + flowNote + planNote + conversion,
|
|
9329
|
+
stderr: "",
|
|
9330
|
+
exitCode
|
|
9331
|
+
};
|
|
8846
9332
|
}
|
|
8847
9333
|
function loadPreparation(args, deps) {
|
|
8848
9334
|
const file = args.positionals[1];
|
|
@@ -8911,7 +9397,7 @@ function trustApply(args, deps) {
|
|
|
8911
9397
|
return { stdout: "", stderr: `Not a calllint.install-plan.v1 document: ${planFile}`, exitCode: EXIT.ERROR };
|
|
8912
9398
|
}
|
|
8913
9399
|
const adapter = getHostAdapter(plan.host);
|
|
8914
|
-
if (!adapter) return usageErr(`Unknown host "${plan.host}". Known hosts: ${
|
|
9400
|
+
if (!adapter) return usageErr(`Unknown host "${plan.host}". Known hosts: ${knownHostList()}`);
|
|
8915
9401
|
if (!adapter.applyPlan) {
|
|
8916
9402
|
return usageErr(`Host "${plan.host}" is tier ${adapter.tier} \u2014 plan-only; copy the patch or use a Tier-A host to apply.`);
|
|
8917
9403
|
}
|
|
@@ -9137,6 +9623,46 @@ This is the READ-ONLY half of the Trust Gateway. It touched no live config
|
|
|
9137
9623
|
`;
|
|
9138
9624
|
return out;
|
|
9139
9625
|
}
|
|
9626
|
+
function renderConversionPrompt(p) {
|
|
9627
|
+
const d = p.decision;
|
|
9628
|
+
if (!d || d.verdict === "BLOCK" || d.verdict === "UNKNOWN") return "";
|
|
9629
|
+
return `
|
|
9630
|
+
Next step (nothing is persisted unless you run one of these):
|
|
9631
|
+
\u2022 Record this surface as approved: calllint approve
|
|
9632
|
+
\u2022 Re-decide on every authority change: calllint guard install --host git
|
|
9633
|
+
\u2022 Gate pull requests in CI: calllint guard install --host github
|
|
9634
|
+
\u2022 Teach your agent the safety rule: calllint gen-rule --host claude --write
|
|
9635
|
+
`;
|
|
9636
|
+
}
|
|
9637
|
+
var FLOW_HINT_SYMBOL = {
|
|
9638
|
+
BLOCK: "\u26D4 BLOCK",
|
|
9639
|
+
REVIEW: "\u26A0 REVIEW",
|
|
9640
|
+
ALLOW: "\u{1F6E1} ALLOW"
|
|
9641
|
+
};
|
|
9642
|
+
function renderFlows(flows) {
|
|
9643
|
+
if (flows.length === 0) {
|
|
9644
|
+
return `
|
|
9645
|
+
toxic-flows: (none \u2014 no cross-capability composition detected)
|
|
9646
|
+
`;
|
|
9647
|
+
}
|
|
9648
|
+
let out = `
|
|
9649
|
+
toxic-flows: ${flows.length} path(s) [calllint.flow.v0]
|
|
9650
|
+
`;
|
|
9651
|
+
for (const f of flows) {
|
|
9652
|
+
const dst = f.sink.destination ? ` \u2192 ${f.sink.destination}` : "";
|
|
9653
|
+
out += ` ${FLOW_HINT_SYMBOL[f.decisionHint] ?? f.decisionHint} ${f.flowId} (${f.risk.class})
|
|
9654
|
+
`;
|
|
9655
|
+
out += ` source: ${f.source.trustSource} \u2192 sink: ${f.sink.action} ${f.sink.resource}${dst}
|
|
9656
|
+
`;
|
|
9657
|
+
out += ` evidence: ${f.evidence.join(", ")}
|
|
9658
|
+
`;
|
|
9659
|
+
}
|
|
9660
|
+
out += ` A dangerous flow is folded into the decision as a TOXIC_FLOW_COMPOSITION reason;
|
|
9661
|
+
`;
|
|
9662
|
+
out += ` it can only raise the verdict, never lower it. An ALLOW flow contributes nothing.
|
|
9663
|
+
`;
|
|
9664
|
+
return out;
|
|
9665
|
+
}
|
|
9140
9666
|
function renderExplanation(p) {
|
|
9141
9667
|
let out = `
|
|
9142
9668
|
CallLint trust explain
|
|
@@ -9270,6 +9796,15 @@ The write could not be verified AND the automatic rollback failed. Your
|
|
|
9270
9796
|
}
|
|
9271
9797
|
return out;
|
|
9272
9798
|
}
|
|
9799
|
+
function hostHelpDescriptions() {
|
|
9800
|
+
return Object.values(HOST_ADAPTERS).map((a) => {
|
|
9801
|
+
const capability = a.applyPlan ? `Tier ${a.tier}, applies` : `Tier ${a.tier}, plan-only \u2014 you apply the emitted patch`;
|
|
9802
|
+
return `${a.id} (${capability})`;
|
|
9803
|
+
}).join("; ");
|
|
9804
|
+
}
|
|
9805
|
+
function knownHostList() {
|
|
9806
|
+
return Object.keys(HOST_ADAPTERS).join(", ");
|
|
9807
|
+
}
|
|
9273
9808
|
function trustHelp() {
|
|
9274
9809
|
return `
|
|
9275
9810
|
calllint trust \u2014 Automated Trust Gateway (prepare \u2192 approve \u2192 apply \u2192 verify)
|
|
@@ -9301,11 +9836,16 @@ OPTIONS (prepare)
|
|
|
9301
9836
|
--format json|sarif Force the evidence format (default: auto-detect)
|
|
9302
9837
|
--provider <name> Force the evidence provider adapter (default: auto-detect)
|
|
9303
9838
|
--policy <file> Use a policy file for the decision (default: built-in defaults)
|
|
9304
|
-
--host <id> Build an install plan for a host (
|
|
9305
|
-
the host config READ-ONLY; never applies. Plan
|
|
9306
|
-
only for a non-blocking decision
|
|
9307
|
-
--host-config <path> Override the host config path (default:
|
|
9839
|
+
--host <id> Build an install plan for a host \u2014 ${hostHelpDescriptions()}.
|
|
9840
|
+
Reads the host config READ-ONLY; never applies here. Plan
|
|
9841
|
+
is emitted only for a non-blocking decision.
|
|
9842
|
+
--host-config <path> Override the host config path (default: per host \u2014
|
|
9843
|
+
~/.claude.json for claude-code, .cursor/mcp.json for
|
|
9844
|
+
cursor, ~/.codeium/mcp_config.json for windsurf)
|
|
9308
9845
|
--write-plan Persist the plan to .calllint/plans/<plan-id>.json
|
|
9846
|
+
--flows Show static toxic-flow paths (calllint.flow.v0) behind the
|
|
9847
|
+
decision's TOXIC_FLOW_COMPOSITION reasons. With --json, emits
|
|
9848
|
+
{ preparation, flows }. A dangerous flow only raises the verdict.
|
|
9309
9849
|
--no-llm Default posture: no LLM in the verdict path (accepted, no-op)
|
|
9310
9850
|
--json Emit the raw calllint.trust-preparation.v0 document
|
|
9311
9851
|
|
|
@@ -9350,6 +9890,209 @@ SEE ALSO
|
|
|
9350
9890
|
`;
|
|
9351
9891
|
}
|
|
9352
9892
|
|
|
9893
|
+
// src/commands/guard.ts
|
|
9894
|
+
import { existsSync as existsSync12, readFileSync as readFileSync19, writeFileSync as writeFileSync11, mkdirSync as mkdirSync6 } from "node:fs";
|
|
9895
|
+
import { dirname as dirname6, join as join17, resolve as resolve10 } from "node:path";
|
|
9896
|
+
var GUARD_HOSTS = ["git", "github"];
|
|
9897
|
+
function guardConfigPath(cwd) {
|
|
9898
|
+
return join17(cwd, ".calllint", "guard.json");
|
|
9899
|
+
}
|
|
9900
|
+
function readGuardConfig(cwd) {
|
|
9901
|
+
const path = guardConfigPath(cwd);
|
|
9902
|
+
if (!existsSync12(path)) return void 0;
|
|
9903
|
+
try {
|
|
9904
|
+
return JSON.parse(readFileSync19(path, "utf8"));
|
|
9905
|
+
} catch {
|
|
9906
|
+
return void 0;
|
|
9907
|
+
}
|
|
9908
|
+
}
|
|
9909
|
+
function isDisabled(cwd, env) {
|
|
9910
|
+
if (env["CALLLINT_GUARD"] === "0") return true;
|
|
9911
|
+
return readGuardConfig(cwd)?.enabled === false;
|
|
9912
|
+
}
|
|
9913
|
+
function guardCommand(args, deps) {
|
|
9914
|
+
const sub = args.positionals[0];
|
|
9915
|
+
const env = deps.env ?? process.env;
|
|
9916
|
+
switch (sub) {
|
|
9917
|
+
case "install":
|
|
9918
|
+
return guardInstall(args, deps);
|
|
9919
|
+
case "status":
|
|
9920
|
+
return guardStatus(args, deps, env);
|
|
9921
|
+
case "disable":
|
|
9922
|
+
return guardSetEnabled(deps, false);
|
|
9923
|
+
case "enable":
|
|
9924
|
+
return guardSetEnabled(deps, true);
|
|
9925
|
+
case void 0:
|
|
9926
|
+
return guardRun(args, deps, env);
|
|
9927
|
+
default:
|
|
9928
|
+
return {
|
|
9929
|
+
stdout: "",
|
|
9930
|
+
stderr: `Unknown guard subcommand: ${sub}
|
|
9931
|
+
Run \`calllint guard\` (assess) or \`calllint guard install|status|disable|enable\`.`,
|
|
9932
|
+
exitCode: EXIT.USAGE
|
|
9933
|
+
};
|
|
9934
|
+
}
|
|
9935
|
+
}
|
|
9936
|
+
function exitForAction(a) {
|
|
9937
|
+
switch (a.action) {
|
|
9938
|
+
case "silent":
|
|
9939
|
+
case "note":
|
|
9940
|
+
return EXIT.OK;
|
|
9941
|
+
case "prompt":
|
|
9942
|
+
return EXIT.REVIEW;
|
|
9943
|
+
case "request-evidence":
|
|
9944
|
+
return EXIT.UNKNOWN;
|
|
9945
|
+
case "refuse":
|
|
9946
|
+
return EXIT.BLOCK;
|
|
9947
|
+
case "fail-closed":
|
|
9948
|
+
return EXIT.ERROR;
|
|
9949
|
+
}
|
|
9950
|
+
}
|
|
9951
|
+
function guardRun(args, deps, env) {
|
|
9952
|
+
const json = flagBool(args.flags, "json");
|
|
9953
|
+
if (isDisabled(deps.cwd, env)) {
|
|
9954
|
+
const note = "Continuous Guard is disabled (CALLLINT_GUARD=0 or .calllint/guard.json).";
|
|
9955
|
+
return {
|
|
9956
|
+
stdout: json ? JSON.stringify({ enabled: false, note }) : note,
|
|
9957
|
+
exitCode: EXIT.OK
|
|
9958
|
+
};
|
|
9959
|
+
}
|
|
9960
|
+
const approvedPath = flagStr(args.flags, "approved") ?? defaultApprovedPath(deps.cwd);
|
|
9961
|
+
const approved = readApproved(approvedPath);
|
|
9962
|
+
if (!approved) {
|
|
9963
|
+
return {
|
|
9964
|
+
stdout: "",
|
|
9965
|
+
stderr: `No approved baseline at ${approvedPath}. Run \`calllint approve\` first.`,
|
|
9966
|
+
exitCode: EXIT.USAGE
|
|
9967
|
+
};
|
|
9968
|
+
}
|
|
9969
|
+
let assessment;
|
|
9970
|
+
try {
|
|
9971
|
+
const current = decideRepoSurfaces(deps.cwd, { now: deps.now, generatedAt: deps.generatedAt });
|
|
9972
|
+
const drift = verifyApproved(current, approved, deps.generatedAt);
|
|
9973
|
+
assessment = assessGuardDrift(drift);
|
|
9974
|
+
} catch (err2) {
|
|
9975
|
+
assessment = guardFailClosed(err2 instanceof Error ? err2.message : String(err2));
|
|
9976
|
+
}
|
|
9977
|
+
const exitCode = exitForAction(assessment);
|
|
9978
|
+
if (json) {
|
|
9979
|
+
return { stdout: JSON.stringify(assessment), exitCode };
|
|
9980
|
+
}
|
|
9981
|
+
if (assessment.action === "silent") {
|
|
9982
|
+
return { stdout: "", exitCode: EXIT.OK };
|
|
9983
|
+
}
|
|
9984
|
+
const prefix = assessment.action === "fail-closed" ? "GUARD FAILED CLOSED" : assessment.action === "refuse" ? "BLOCK" : assessment.action === "request-evidence" ? "UNKNOWN" : assessment.action === "prompt" ? "REVIEW" : "NOTE";
|
|
9985
|
+
const failLine = assessment.failure ? `
|
|
9986
|
+
reason: ${assessment.failure}` : "";
|
|
9987
|
+
return { stdout: `${prefix}: ${assessment.summary}${failLine}`, exitCode };
|
|
9988
|
+
}
|
|
9989
|
+
function isGuardHost(v) {
|
|
9990
|
+
return v !== void 0 && GUARD_HOSTS.includes(v);
|
|
9991
|
+
}
|
|
9992
|
+
var GIT_HOOK = `#!/usr/bin/env bash
|
|
9993
|
+
# CallLint Continuous Guard (ADR 0045). Re-decides the agent-tool authority
|
|
9994
|
+
# surface on commit; silent when nothing changed. Generated by \`calllint guard install\`.
|
|
9995
|
+
# CallLint is static and NEVER executes a scanned server.
|
|
9996
|
+
npx -y calllint guard --no-emoji
|
|
9997
|
+
`;
|
|
9998
|
+
function guardArtifact(host) {
|
|
9999
|
+
if (host === "git") {
|
|
10000
|
+
return { path: join17(".git", "hooks", "pre-commit"), content: GIT_HOOK, label: "git pre-commit hook" };
|
|
10001
|
+
}
|
|
10002
|
+
return {
|
|
10003
|
+
path: join17(".github", "workflows", "calllint.yml"),
|
|
10004
|
+
content: renderCiGate({ mode: "drift" }),
|
|
10005
|
+
label: "GitHub Actions drift-gate workflow"
|
|
10006
|
+
};
|
|
10007
|
+
}
|
|
10008
|
+
function guardInstall(args, deps) {
|
|
10009
|
+
const host = flagStr(args.flags, "host");
|
|
10010
|
+
if (!host) {
|
|
10011
|
+
const list = GUARD_HOSTS.map((h2) => ` ${h2.padEnd(8)} \u2192 ${guardArtifact(h2).label}`).join("\n");
|
|
10012
|
+
return {
|
|
10013
|
+
stdout: `Usage: calllint guard install --host <host>
|
|
10014
|
+
|
|
10015
|
+
Hosts:
|
|
10016
|
+
${list}`,
|
|
10017
|
+
exitCode: EXIT.OK
|
|
10018
|
+
};
|
|
10019
|
+
}
|
|
10020
|
+
if (!isGuardHost(host)) {
|
|
10021
|
+
return {
|
|
10022
|
+
stdout: "",
|
|
10023
|
+
stderr: `Unknown guard host: ${host}
|
|
10024
|
+
Run \`calllint guard install\` to list hosts.`,
|
|
10025
|
+
exitCode: EXIT.USAGE
|
|
10026
|
+
};
|
|
10027
|
+
}
|
|
10028
|
+
const art = guardArtifact(host);
|
|
10029
|
+
const rel = flagStr(args.flags, "out") ?? art.path;
|
|
10030
|
+
if (deps.writeFile === false) {
|
|
10031
|
+
return { stdout: `Would write ${rel} (${art.label})`, exitCode: EXIT.OK };
|
|
10032
|
+
}
|
|
10033
|
+
const abs = resolve10(deps.cwd, rel);
|
|
10034
|
+
try {
|
|
10035
|
+
mkdirSync6(dirname6(abs), { recursive: true });
|
|
10036
|
+
writeFileSync11(abs, art.content, "utf8");
|
|
10037
|
+
} catch (e) {
|
|
10038
|
+
return {
|
|
10039
|
+
stdout: "",
|
|
10040
|
+
stderr: `Failed to write ${rel}: ${e instanceof Error ? e.message : String(e)}`,
|
|
10041
|
+
exitCode: EXIT.ERROR
|
|
10042
|
+
};
|
|
10043
|
+
}
|
|
10044
|
+
const note = host === "git" ? `
|
|
10045
|
+
Make it executable: chmod +x ${rel}` : "";
|
|
10046
|
+
return { stdout: `Wrote ${rel} (${art.label})${note}`, exitCode: EXIT.OK };
|
|
10047
|
+
}
|
|
10048
|
+
function guardStatus(args, deps, env) {
|
|
10049
|
+
const approvedPath = flagStr(args.flags, "approved") ?? defaultApprovedPath(deps.cwd);
|
|
10050
|
+
const hasBaseline = existsSync12(approvedPath);
|
|
10051
|
+
const disabled = isDisabled(deps.cwd, env);
|
|
10052
|
+
const gitHook = existsSync12(join17(deps.cwd, ".git", "hooks", "pre-commit"));
|
|
10053
|
+
const ghWorkflow = existsSync12(join17(deps.cwd, ".github", "workflows", "calllint.yml"));
|
|
10054
|
+
const status = {
|
|
10055
|
+
enabled: !disabled,
|
|
10056
|
+
disabledBy: disabled ? env["CALLLINT_GUARD"] === "0" ? "env:CALLLINT_GUARD=0" : "flag:.calllint/guard.json" : null,
|
|
10057
|
+
approvedBaseline: hasBaseline ? approvedPath : null,
|
|
10058
|
+
installedHooks: {
|
|
10059
|
+
"git:pre-commit": gitHook,
|
|
10060
|
+
"github:workflow": ghWorkflow
|
|
10061
|
+
}
|
|
10062
|
+
};
|
|
10063
|
+
if (flagBool(args.flags, "json")) {
|
|
10064
|
+
return { stdout: JSON.stringify(status), exitCode: EXIT.OK };
|
|
10065
|
+
}
|
|
10066
|
+
const lines = [
|
|
10067
|
+
`Continuous Guard: ${status.enabled ? "enabled" : `disabled (${status.disabledBy})`}`,
|
|
10068
|
+
`Approved baseline: ${hasBaseline ? approvedPath : "none \u2014 run `calllint approve`"}`,
|
|
10069
|
+
`git pre-commit hook: ${gitHook ? "installed" : "not installed"}`,
|
|
10070
|
+
`GitHub workflow: ${ghWorkflow ? "installed" : "not installed"}`
|
|
10071
|
+
];
|
|
10072
|
+
return { stdout: lines.join("\n"), exitCode: EXIT.OK };
|
|
10073
|
+
}
|
|
10074
|
+
function guardSetEnabled(deps, enabled) {
|
|
10075
|
+
const cfg = { schemaVersion: "calllint.guard-config.v0", enabled };
|
|
10076
|
+
if (deps.writeFile === false) {
|
|
10077
|
+
return { stdout: enabled ? "Would enable Continuous Guard" : "Would disable Continuous Guard", exitCode: EXIT.OK };
|
|
10078
|
+
}
|
|
10079
|
+
const path = guardConfigPath(deps.cwd);
|
|
10080
|
+
try {
|
|
10081
|
+
mkdirSync6(dirname6(path), { recursive: true });
|
|
10082
|
+
writeFileSync11(path, JSON.stringify(cfg, null, 2), "utf8");
|
|
10083
|
+
} catch (e) {
|
|
10084
|
+
return {
|
|
10085
|
+
stdout: "",
|
|
10086
|
+
stderr: `Failed to write ${path}: ${e instanceof Error ? e.message : String(e)}`,
|
|
10087
|
+
exitCode: EXIT.ERROR
|
|
10088
|
+
};
|
|
10089
|
+
}
|
|
10090
|
+
return {
|
|
10091
|
+
stdout: enabled ? "Continuous Guard enabled (.calllint/guard.json)." : "Continuous Guard disabled (.calllint/guard.json). Re-enable with `calllint guard enable`.",
|
|
10092
|
+
exitCode: EXIT.OK
|
|
10093
|
+
};
|
|
10094
|
+
}
|
|
10095
|
+
|
|
9353
10096
|
// src/run.ts
|
|
9354
10097
|
function run(argv, deps) {
|
|
9355
10098
|
const args = parseArgs(argv);
|
|
@@ -9428,6 +10171,13 @@ function run(argv, deps) {
|
|
|
9428
10171
|
return evidenceCommand(args, { cwd: deps.cwd });
|
|
9429
10172
|
case "trust":
|
|
9430
10173
|
return trustCommand(args, { cwd: deps.cwd, generatedAt: deps.generatedAt, toolVersion: deps.toolVersion });
|
|
10174
|
+
case "guard":
|
|
10175
|
+
return guardCommand(args, {
|
|
10176
|
+
cwd: deps.cwd,
|
|
10177
|
+
now: deps.now,
|
|
10178
|
+
generatedAt: deps.generatedAt,
|
|
10179
|
+
writeFile: deps.writeCacheFile
|
|
10180
|
+
});
|
|
9431
10181
|
case "gen-rule":
|
|
9432
10182
|
return genRuleCommand(args, { cwd: deps.cwd });
|
|
9433
10183
|
case "policy":
|
|
@@ -9701,13 +10451,13 @@ async function breathe(argv, deps = {}) {
|
|
|
9701
10451
|
}
|
|
9702
10452
|
|
|
9703
10453
|
// src/version.ts
|
|
9704
|
-
import { readFileSync as
|
|
10454
|
+
import { readFileSync as readFileSync20 } from "node:fs";
|
|
9705
10455
|
import { fileURLToPath } from "node:url";
|
|
9706
|
-
import { dirname as
|
|
10456
|
+
import { dirname as dirname7, join as join18 } from "node:path";
|
|
9707
10457
|
function resolveToolVersion() {
|
|
9708
10458
|
try {
|
|
9709
|
-
const pkgPath =
|
|
9710
|
-
const pkg = JSON.parse(
|
|
10459
|
+
const pkgPath = join18(dirname7(fileURLToPath(import.meta.url)), "..", "package.json");
|
|
10460
|
+
const pkg = JSON.parse(readFileSync20(pkgPath, "utf8"));
|
|
9711
10461
|
return typeof pkg.version === "string" && pkg.version.length > 0 ? pkg.version : "unknown";
|
|
9712
10462
|
} catch {
|
|
9713
10463
|
return "unknown";
|
|
@@ -9717,7 +10467,7 @@ function resolveToolVersion() {
|
|
|
9717
10467
|
// src/index.ts
|
|
9718
10468
|
function readStdin() {
|
|
9719
10469
|
try {
|
|
9720
|
-
return
|
|
10470
|
+
return readFileSync21(0, "utf8");
|
|
9721
10471
|
} catch {
|
|
9722
10472
|
return "";
|
|
9723
10473
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "calllint",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.1",
|
|
4
4
|
"description": "Evidence-backed security verdicts for MCP servers and agent tools. Lint agent tool-call risk before tools run — SAFE / REVIEW / BLOCK / UNKNOWN, with evidence. Never executes the server it judges.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"mcp",
|
|
@@ -55,6 +55,7 @@
|
|
|
55
55
|
"@calllint/evidence": "workspace:*",
|
|
56
56
|
"@calllint/fingerprint": "workspace:*",
|
|
57
57
|
"@calllint/fixtures": "workspace:*",
|
|
58
|
+
"@calllint/flow-analyzer": "workspace:*",
|
|
58
59
|
"@calllint/install-planner": "workspace:*",
|
|
59
60
|
"@calllint/online": "workspace:*",
|
|
60
61
|
"@calllint/policy": "workspace:*",
|