@neurcode-ai/cli 0.9.36 → 0.9.38
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/README.md +4 -2
- package/dist/api-client.d.ts +300 -1
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +225 -9
- package/dist/api-client.js.map +1 -1
- package/dist/commands/audit.d.ts +3 -0
- package/dist/commands/audit.d.ts.map +1 -0
- package/dist/commands/audit.js +133 -0
- package/dist/commands/audit.js.map +1 -0
- package/dist/commands/contract.d.ts +3 -0
- package/dist/commands/contract.d.ts.map +1 -0
- package/dist/commands/contract.js +235 -0
- package/dist/commands/contract.js.map +1 -0
- package/dist/commands/feedback.d.ts +3 -0
- package/dist/commands/feedback.d.ts.map +1 -0
- package/dist/commands/feedback.js +208 -0
- package/dist/commands/feedback.js.map +1 -0
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/plan.js +19 -3
- package/dist/commands/plan.js.map +1 -1
- package/dist/commands/policy.d.ts.map +1 -1
- package/dist/commands/policy.js +329 -6
- package/dist/commands/policy.js.map +1 -1
- package/dist/commands/remediate.d.ts +17 -0
- package/dist/commands/remediate.d.ts.map +1 -0
- package/dist/commands/remediate.js +252 -0
- package/dist/commands/remediate.js.map +1 -0
- package/dist/commands/ship.d.ts.map +1 -1
- package/dist/commands/ship.js +67 -14
- package/dist/commands/ship.js.map +1 -1
- package/dist/commands/verify.d.ts +12 -0
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +477 -13
- package/dist/commands/verify.js.map +1 -1
- package/dist/index.js +60 -0
- package/dist/index.js.map +1 -1
- package/dist/utils/artifact-signature.d.ts +34 -0
- package/dist/utils/artifact-signature.d.ts.map +1 -0
- package/dist/utils/artifact-signature.js +229 -0
- package/dist/utils/artifact-signature.js.map +1 -0
- package/dist/utils/change-contract.d.ts +2 -0
- package/dist/utils/change-contract.d.ts.map +1 -1
- package/dist/utils/change-contract.js +21 -1
- package/dist/utils/change-contract.js.map +1 -1
- package/dist/utils/policy-compiler.d.ts +2 -0
- package/dist/utils/policy-compiler.d.ts.map +1 -1
- package/dist/utils/policy-compiler.js +15 -0
- package/dist/utils/policy-compiler.js.map +1 -1
- package/package.json +7 -7
package/dist/commands/verify.js
CHANGED
|
@@ -44,6 +44,7 @@ const git_1 = require("../utils/git");
|
|
|
44
44
|
const diff_parser_1 = require("@neurcode-ai/diff-parser");
|
|
45
45
|
const policy_engine_1 = require("@neurcode-ai/policy-engine");
|
|
46
46
|
const governance_runtime_1 = require("@neurcode-ai/governance-runtime");
|
|
47
|
+
const contracts_1 = require("@neurcode-ai/contracts");
|
|
47
48
|
const config_1 = require("../config");
|
|
48
49
|
const api_client_1 = require("../api-client");
|
|
49
50
|
const path_1 = require("path");
|
|
@@ -63,6 +64,7 @@ const policy_audit_1 = require("../utils/policy-audit");
|
|
|
63
64
|
const governance_1 = require("../utils/governance");
|
|
64
65
|
const policy_compiler_1 = require("../utils/policy-compiler");
|
|
65
66
|
const change_contract_1 = require("../utils/change-contract");
|
|
67
|
+
const artifact_signature_1 = require("../utils/artifact-signature");
|
|
66
68
|
const policy_1 = require("@neurcode-ai/policy");
|
|
67
69
|
// Import chalk with fallback
|
|
68
70
|
let chalk;
|
|
@@ -85,6 +87,155 @@ catch {
|
|
|
85
87
|
};
|
|
86
88
|
}
|
|
87
89
|
;
|
|
90
|
+
function toArtifactSignatureSummary(status) {
|
|
91
|
+
return {
|
|
92
|
+
required: status.required,
|
|
93
|
+
present: status.present,
|
|
94
|
+
valid: status.valid,
|
|
95
|
+
keyId: status.keyId,
|
|
96
|
+
verifiedWithKeyId: status.verifiedWithKeyId,
|
|
97
|
+
issues: [...status.issues],
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
function resolveCliComponentVersion() {
|
|
101
|
+
const fromEnv = process.env.NEURCODE_CLI_VERSION || process.env.npm_package_version;
|
|
102
|
+
if (fromEnv && fromEnv.trim()) {
|
|
103
|
+
return fromEnv.trim();
|
|
104
|
+
}
|
|
105
|
+
try {
|
|
106
|
+
const packagePath = (0, path_1.join)(__dirname, '../../package.json');
|
|
107
|
+
const raw = (0, fs_1.readFileSync)(packagePath, 'utf-8');
|
|
108
|
+
const parsed = JSON.parse(raw);
|
|
109
|
+
if (typeof parsed.version === 'string' && parsed.version.trim()) {
|
|
110
|
+
return parsed.version.trim();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
// Ignore and fall back.
|
|
115
|
+
}
|
|
116
|
+
return '0.0.0';
|
|
117
|
+
}
|
|
118
|
+
const CLI_COMPONENT_VERSION = resolveCliComponentVersion();
|
|
119
|
+
function asCompatibilityRecord(value) {
|
|
120
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
121
|
+
return null;
|
|
122
|
+
}
|
|
123
|
+
return value;
|
|
124
|
+
}
|
|
125
|
+
async function probeApiRuntimeCompatibility(apiUrl) {
|
|
126
|
+
const normalizedApiUrl = apiUrl.replace(/\/$/, '');
|
|
127
|
+
const healthUrl = `${normalizedApiUrl}/health`;
|
|
128
|
+
const controller = new AbortController();
|
|
129
|
+
const timeoutId = setTimeout(() => controller.abort(), 5000);
|
|
130
|
+
try {
|
|
131
|
+
const response = await fetch(healthUrl, {
|
|
132
|
+
method: 'GET',
|
|
133
|
+
signal: controller.signal,
|
|
134
|
+
headers: {
|
|
135
|
+
'User-Agent': 'neurcode-cli-verify/compat-probe',
|
|
136
|
+
},
|
|
137
|
+
});
|
|
138
|
+
if (!response.ok) {
|
|
139
|
+
return {
|
|
140
|
+
healthUrl,
|
|
141
|
+
apiVersion: null,
|
|
142
|
+
status: 'warn',
|
|
143
|
+
messages: [`Health endpoint returned status ${response.status}; skipping runtime compatibility handshake.`],
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
const payload = (await response.json().catch(() => ({})));
|
|
147
|
+
const compatibility = asCompatibilityRecord(payload.compatibility);
|
|
148
|
+
const apiVersionFromHealth = typeof payload.version === 'string' && payload.version.trim()
|
|
149
|
+
? payload.version.trim()
|
|
150
|
+
: null;
|
|
151
|
+
if (!compatibility) {
|
|
152
|
+
return {
|
|
153
|
+
healthUrl,
|
|
154
|
+
apiVersion: apiVersionFromHealth,
|
|
155
|
+
status: 'warn',
|
|
156
|
+
messages: ['API health payload is missing compatibility metadata.'],
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
const contractId = typeof compatibility.contractId === 'string' ? compatibility.contractId.trim() : '';
|
|
160
|
+
const runtimeContractVersion = typeof compatibility.runtimeContractVersion === 'string'
|
|
161
|
+
? compatibility.runtimeContractVersion.trim()
|
|
162
|
+
: '';
|
|
163
|
+
const cliJsonContractVersion = typeof compatibility.cliJsonContractVersion === 'string'
|
|
164
|
+
? compatibility.cliJsonContractVersion.trim()
|
|
165
|
+
: '';
|
|
166
|
+
const component = typeof compatibility.component === 'string' ? compatibility.component.trim() : '';
|
|
167
|
+
const componentVersion = typeof compatibility.componentVersion === 'string'
|
|
168
|
+
? compatibility.componentVersion.trim()
|
|
169
|
+
: '';
|
|
170
|
+
const minimumPeerVersions = asCompatibilityRecord(compatibility.minimumPeerVersions) || {};
|
|
171
|
+
const apiRequiresCli = typeof minimumPeerVersions.cli === 'string' && minimumPeerVersions.cli.trim()
|
|
172
|
+
? minimumPeerVersions.cli.trim()
|
|
173
|
+
: undefined;
|
|
174
|
+
const errors = [];
|
|
175
|
+
if (contractId !== contracts_1.RUNTIME_COMPATIBILITY_CONTRACT_ID) {
|
|
176
|
+
errors.push(`API compatibility contractId mismatch (expected ${contracts_1.RUNTIME_COMPATIBILITY_CONTRACT_ID}, got ${contractId || 'missing'}).`);
|
|
177
|
+
}
|
|
178
|
+
if (runtimeContractVersion !== contracts_1.RUNTIME_COMPATIBILITY_CONTRACT_VERSION) {
|
|
179
|
+
errors.push(`API runtimeContractVersion mismatch (expected ${contracts_1.RUNTIME_COMPATIBILITY_CONTRACT_VERSION}, got ${runtimeContractVersion || 'missing'}).`);
|
|
180
|
+
}
|
|
181
|
+
if (cliJsonContractVersion !== contracts_1.CLI_JSON_CONTRACT_VERSION) {
|
|
182
|
+
errors.push(`API cliJsonContractVersion mismatch (expected ${contracts_1.CLI_JSON_CONTRACT_VERSION}, got ${cliJsonContractVersion || 'missing'}).`);
|
|
183
|
+
}
|
|
184
|
+
if (component !== 'api') {
|
|
185
|
+
errors.push(`API compatibility payload component must be "api" (received ${component || 'missing'}).`);
|
|
186
|
+
}
|
|
187
|
+
const resolvedApiVersion = componentVersion || apiVersionFromHealth;
|
|
188
|
+
const minimumApiForCli = (0, contracts_1.getMinimumCompatiblePeerVersion)('cli', 'api');
|
|
189
|
+
if (minimumApiForCli && resolvedApiVersion) {
|
|
190
|
+
const cliRequiresApi = (0, contracts_1.isSemverAtLeast)(resolvedApiVersion, minimumApiForCli);
|
|
191
|
+
if (cliRequiresApi === null) {
|
|
192
|
+
errors.push(`Unable to compare API version "${resolvedApiVersion}" against required minimum "${minimumApiForCli}".`);
|
|
193
|
+
}
|
|
194
|
+
else if (!cliRequiresApi) {
|
|
195
|
+
errors.push(`API version ${resolvedApiVersion} is below CLI required minimum ${minimumApiForCli}.`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
if (apiRequiresCli) {
|
|
199
|
+
const apiRequiresThisCli = (0, contracts_1.isSemverAtLeast)(CLI_COMPONENT_VERSION, apiRequiresCli);
|
|
200
|
+
if (apiRequiresThisCli === null) {
|
|
201
|
+
errors.push(`Unable to compare CLI version "${CLI_COMPONENT_VERSION}" against API required minimum "${apiRequiresCli}".`);
|
|
202
|
+
}
|
|
203
|
+
else if (!apiRequiresThisCli) {
|
|
204
|
+
errors.push(`CLI version ${CLI_COMPONENT_VERSION} is below API required minimum ${apiRequiresCli}.`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (errors.length > 0) {
|
|
208
|
+
return {
|
|
209
|
+
healthUrl,
|
|
210
|
+
apiVersion: resolvedApiVersion || null,
|
|
211
|
+
status: 'error',
|
|
212
|
+
messages: errors,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
healthUrl,
|
|
217
|
+
apiVersion: resolvedApiVersion || null,
|
|
218
|
+
status: 'ok',
|
|
219
|
+
messages: [],
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
catch (error) {
|
|
223
|
+
const message = error instanceof Error && error.name === 'AbortError'
|
|
224
|
+
? 'Health endpoint timed out after 5s.'
|
|
225
|
+
: error instanceof Error
|
|
226
|
+
? error.message
|
|
227
|
+
: 'Unknown error';
|
|
228
|
+
return {
|
|
229
|
+
healthUrl,
|
|
230
|
+
apiVersion: null,
|
|
231
|
+
status: 'warn',
|
|
232
|
+
messages: [`Runtime compatibility probe failed: ${message}`],
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
finally {
|
|
236
|
+
clearTimeout(timeoutId);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
88
239
|
/**
|
|
89
240
|
* Check if a file path should be excluded from verification analysis
|
|
90
241
|
* Excludes internal/system files that should not count towards plan adherence
|
|
@@ -391,6 +542,94 @@ function resolveAuditIntegrityStatus(requireIntegrity, auditIntegrity) {
|
|
|
391
542
|
issues,
|
|
392
543
|
};
|
|
393
544
|
}
|
|
545
|
+
function describePolicyExceptionSource(mode) {
|
|
546
|
+
switch (mode) {
|
|
547
|
+
case 'org':
|
|
548
|
+
return 'org control plane';
|
|
549
|
+
case 'org_fallback_local':
|
|
550
|
+
return 'local file fallback (org unavailable)';
|
|
551
|
+
case 'local':
|
|
552
|
+
default:
|
|
553
|
+
return 'local file';
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
function pickExceptionIdentity(userId, email, allowSet) {
|
|
557
|
+
const normalizedUserId = typeof userId === 'string' ? userId.trim() : '';
|
|
558
|
+
const normalizedEmail = typeof email === 'string' ? email.trim() : '';
|
|
559
|
+
if (allowSet.size > 0) {
|
|
560
|
+
if (normalizedEmail && allowSet.has(normalizedEmail.toLowerCase())) {
|
|
561
|
+
return normalizedEmail;
|
|
562
|
+
}
|
|
563
|
+
if (normalizedUserId && allowSet.has(normalizedUserId.toLowerCase())) {
|
|
564
|
+
return normalizedUserId;
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
if (normalizedEmail)
|
|
568
|
+
return normalizedEmail;
|
|
569
|
+
if (normalizedUserId)
|
|
570
|
+
return normalizedUserId;
|
|
571
|
+
return 'unknown';
|
|
572
|
+
}
|
|
573
|
+
function mapOrgPolicyExceptionToLocalEntry(exception, allowSet) {
|
|
574
|
+
const createdBy = pickExceptionIdentity(exception.createdBy, exception.requestedByEmail, allowSet);
|
|
575
|
+
const requestedBy = pickExceptionIdentity(exception.requestedBy, exception.requestedByEmail, allowSet);
|
|
576
|
+
return {
|
|
577
|
+
id: exception.id,
|
|
578
|
+
rulePattern: exception.rulePattern,
|
|
579
|
+
filePattern: exception.filePattern,
|
|
580
|
+
reason: exception.reason,
|
|
581
|
+
ticket: exception.ticket,
|
|
582
|
+
createdAt: exception.createdAt,
|
|
583
|
+
createdBy,
|
|
584
|
+
requestedBy,
|
|
585
|
+
expiresAt: exception.expiresAt,
|
|
586
|
+
severity: exception.severity,
|
|
587
|
+
active: exception.active === true
|
|
588
|
+
&& exception.workflowState !== 'revoked'
|
|
589
|
+
&& exception.workflowState !== 'rejected',
|
|
590
|
+
approvals: (exception.approvals || []).map((approval) => ({
|
|
591
|
+
approver: pickExceptionIdentity(approval.approverUserId, approval.approverEmail, allowSet),
|
|
592
|
+
approvedAt: approval.createdAt,
|
|
593
|
+
comment: approval.note || null,
|
|
594
|
+
})),
|
|
595
|
+
};
|
|
596
|
+
}
|
|
597
|
+
async function resolveEffectivePolicyExceptions(input) {
|
|
598
|
+
const localExceptions = (0, policy_exceptions_1.readPolicyExceptions)(input.projectRoot);
|
|
599
|
+
if (!input.useOrgControlPlane) {
|
|
600
|
+
return {
|
|
601
|
+
mode: 'local',
|
|
602
|
+
exceptions: localExceptions,
|
|
603
|
+
localConfigured: localExceptions.length,
|
|
604
|
+
orgConfigured: 0,
|
|
605
|
+
warning: null,
|
|
606
|
+
};
|
|
607
|
+
}
|
|
608
|
+
try {
|
|
609
|
+
const orgExceptions = await input.client.listOrgPolicyExceptions({ limit: 250 });
|
|
610
|
+
const allowSet = new Set((input.governance.exceptionApprovals.allowedApprovers || []).map((item) => item.toLowerCase()));
|
|
611
|
+
const mapped = orgExceptions
|
|
612
|
+
.map((entry) => mapOrgPolicyExceptionToLocalEntry(entry, allowSet))
|
|
613
|
+
.sort((left, right) => right.createdAt.localeCompare(left.createdAt));
|
|
614
|
+
return {
|
|
615
|
+
mode: 'org',
|
|
616
|
+
exceptions: mapped,
|
|
617
|
+
localConfigured: localExceptions.length,
|
|
618
|
+
orgConfigured: mapped.length,
|
|
619
|
+
warning: null,
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
catch (error) {
|
|
623
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
624
|
+
return {
|
|
625
|
+
mode: 'org_fallback_local',
|
|
626
|
+
exceptions: localExceptions,
|
|
627
|
+
localConfigured: localExceptions.length,
|
|
628
|
+
orgConfigured: 0,
|
|
629
|
+
warning: `Org policy exceptions unavailable; falling back to local exceptions (${message})`,
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
}
|
|
394
633
|
async function recordVerificationIfRequested(options, config, payload) {
|
|
395
634
|
if (!options.record) {
|
|
396
635
|
return;
|
|
@@ -746,10 +985,20 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
|
|
|
746
985
|
const policyResult = (0, policy_engine_1.evaluateRules)(diffFilesForPolicy, effectiveRules.allRules);
|
|
747
986
|
policyViolations = (policyResult.violations || []);
|
|
748
987
|
policyViolations = policyViolations.filter((v) => !ignoreFilter(v.file));
|
|
749
|
-
const
|
|
988
|
+
const localPolicyGovernance = (0, policy_governance_1.readPolicyGovernanceConfig)(projectRoot);
|
|
989
|
+
const governance = (0, policy_governance_1.mergePolicyGovernanceWithOrgOverrides)(localPolicyGovernance, orgGovernanceSettings?.policyGovernance);
|
|
750
990
|
const auditIntegrity = (0, policy_audit_1.verifyPolicyAuditIntegrity)(projectRoot);
|
|
751
991
|
const auditIntegrityStatus = resolveAuditIntegrityStatus(governance.audit.requireIntegrity, auditIntegrity);
|
|
752
|
-
const
|
|
992
|
+
const policyExceptionResolution = await resolveEffectivePolicyExceptions({
|
|
993
|
+
client,
|
|
994
|
+
projectRoot,
|
|
995
|
+
useOrgControlPlane: Boolean(config.apiKey),
|
|
996
|
+
governance,
|
|
997
|
+
});
|
|
998
|
+
if (policyExceptionResolution.warning && !options.json) {
|
|
999
|
+
console.log(chalk.dim(` ${policyExceptionResolution.warning}`));
|
|
1000
|
+
}
|
|
1001
|
+
const configuredPolicyExceptions = policyExceptionResolution.exceptions;
|
|
753
1002
|
const exceptionDecision = (0, policy_exceptions_1.applyPolicyExceptions)(policyViolations, configuredPolicyExceptions, {
|
|
754
1003
|
requireApproval: governance.exceptionApprovals.required,
|
|
755
1004
|
minApprovals: governance.exceptionApprovals.minApprovals,
|
|
@@ -803,6 +1052,10 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
|
|
|
803
1052
|
? `Policy violations: ${policyViolations.map((v) => `${v.file}: ${v.message || v.rule}`).join('; ')}`
|
|
804
1053
|
: 'Policy check completed';
|
|
805
1054
|
const policyExceptionsSummary = {
|
|
1055
|
+
sourceMode: policyExceptionResolution.mode,
|
|
1056
|
+
sourceWarning: policyExceptionResolution.warning,
|
|
1057
|
+
localConfigured: policyExceptionResolution.localConfigured,
|
|
1058
|
+
orgConfigured: policyExceptionResolution.orgConfigured,
|
|
806
1059
|
configured: configuredPolicyExceptions.length,
|
|
807
1060
|
active: exceptionDecision.activeExceptions.length,
|
|
808
1061
|
usable: exceptionDecision.usableExceptions.length,
|
|
@@ -885,6 +1138,7 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
|
|
|
885
1138
|
console.log(chalk.red(` • ${v.file}: ${v.message || v.rule}`));
|
|
886
1139
|
});
|
|
887
1140
|
}
|
|
1141
|
+
console.log(chalk.dim(` Policy exceptions source: ${describePolicyExceptionSource(policyExceptionsSummary.sourceMode)}`));
|
|
888
1142
|
if (policyExceptionsSummary.suppressed > 0) {
|
|
889
1143
|
console.log(chalk.yellow(` Policy exceptions applied: ${policyExceptionsSummary.suppressed}`));
|
|
890
1144
|
}
|
|
@@ -927,12 +1181,48 @@ async function verifyCommand(options) {
|
|
|
927
1181
|
};
|
|
928
1182
|
const enforceChangeContract = options.enforceChangeContract === true ||
|
|
929
1183
|
isEnabledFlag(process.env.NEURCODE_VERIFY_ENFORCE_CHANGE_CONTRACT);
|
|
930
|
-
const
|
|
1184
|
+
const explicitStrictArtifactMode = options.strictArtifacts === true ||
|
|
931
1185
|
isEnabledFlag(process.env.NEURCODE_VERIFY_STRICT_ARTIFACTS) ||
|
|
932
1186
|
isEnabledFlag(process.env.NEURCODE_ENTERPRISE_MODE);
|
|
1187
|
+
const ciEnterpriseDefaultStrict = process.env.CI === 'true'
|
|
1188
|
+
&& !isEnabledFlag(process.env.NEURCODE_VERIFY_ALLOW_NON_STRICT_CI)
|
|
1189
|
+
&& Boolean(options.apiKey || process.env.NEURCODE_API_KEY);
|
|
1190
|
+
const strictArtifactMode = explicitStrictArtifactMode || ciEnterpriseDefaultStrict;
|
|
1191
|
+
const signingConfig = resolveGovernanceSigningConfig();
|
|
1192
|
+
const aiLogSigningKey = signingConfig.signingKey;
|
|
1193
|
+
const aiLogSigningKeyId = signingConfig.signingKeyId;
|
|
1194
|
+
const aiLogSigningKeys = signingConfig.signingKeys;
|
|
1195
|
+
const aiLogSigner = signingConfig.signer;
|
|
1196
|
+
const hasSigningMaterial = Boolean(aiLogSigningKey) || Object.keys(aiLogSigningKeys).length > 0;
|
|
1197
|
+
const allowUnsignedArtifacts = isEnabledFlag(process.env.NEURCODE_VERIFY_ALLOW_UNSIGNED_ARTIFACTS)
|
|
1198
|
+
|| isEnabledFlag(process.env.NEURCODE_VERIFY_DISABLE_SIGNED_ARTIFACTS);
|
|
1199
|
+
const requireSignedArtifacts = options.requireSignedArtifacts === true
|
|
1200
|
+
|| isEnabledFlag(process.env.NEURCODE_VERIFY_REQUIRE_SIGNED_ARTIFACTS)
|
|
1201
|
+
|| (!allowUnsignedArtifacts && strictArtifactMode && hasSigningMaterial);
|
|
933
1202
|
const changeContractRead = (0, change_contract_1.readChangeContract)(projectRoot, options.changeContract);
|
|
934
1203
|
const compiledPolicyRead = (0, policy_compiler_1.readCompiledPolicyArtifact)(projectRoot, options.compiledPolicy);
|
|
935
1204
|
let compiledPolicyMetadata = resolveCompiledPolicyMetadata(compiledPolicyRead.artifact, compiledPolicyRead.exists ? compiledPolicyRead.path : null);
|
|
1205
|
+
const compiledPolicySignatureStatus = compiledPolicyRead.artifact
|
|
1206
|
+
? (0, artifact_signature_1.verifyGovernanceArtifactSignature)({
|
|
1207
|
+
artifact: compiledPolicyRead.artifact,
|
|
1208
|
+
requireSigned: requireSignedArtifacts,
|
|
1209
|
+
signingKey: aiLogSigningKey,
|
|
1210
|
+
signingKeyId: aiLogSigningKeyId,
|
|
1211
|
+
signingKeys: aiLogSigningKeys,
|
|
1212
|
+
})
|
|
1213
|
+
: null;
|
|
1214
|
+
if (compiledPolicyMetadata && compiledPolicySignatureStatus) {
|
|
1215
|
+
compiledPolicyMetadata.signature = toArtifactSignatureSummary(compiledPolicySignatureStatus);
|
|
1216
|
+
}
|
|
1217
|
+
const changeContractSignatureStatus = changeContractRead.contract
|
|
1218
|
+
? (0, artifact_signature_1.verifyGovernanceArtifactSignature)({
|
|
1219
|
+
artifact: changeContractRead.contract,
|
|
1220
|
+
requireSigned: requireSignedArtifacts,
|
|
1221
|
+
signingKey: aiLogSigningKey,
|
|
1222
|
+
signingKeyId: aiLogSigningKeyId,
|
|
1223
|
+
signingKeys: aiLogSigningKeys,
|
|
1224
|
+
})
|
|
1225
|
+
: null;
|
|
936
1226
|
let changeContractSummary = {
|
|
937
1227
|
path: changeContractRead.path,
|
|
938
1228
|
exists: changeContractRead.exists,
|
|
@@ -940,6 +1230,9 @@ async function verifyCommand(options) {
|
|
|
940
1230
|
valid: changeContractRead.contract ? null : changeContractRead.exists ? false : null,
|
|
941
1231
|
planId: changeContractRead.contract?.planId || null,
|
|
942
1232
|
contractId: changeContractRead.contract?.contractId || null,
|
|
1233
|
+
signature: changeContractSignatureStatus
|
|
1234
|
+
? toArtifactSignatureSummary(changeContractSignatureStatus)
|
|
1235
|
+
: undefined,
|
|
943
1236
|
violations: changeContractRead.error
|
|
944
1237
|
? [
|
|
945
1238
|
{
|
|
@@ -961,6 +1254,16 @@ async function verifyCommand(options) {
|
|
|
961
1254
|
? `Change contract artifact invalid (${changeContractRead.error})`
|
|
962
1255
|
: `Change contract artifact missing (${changeContractRead.path})`);
|
|
963
1256
|
}
|
|
1257
|
+
if (compiledPolicySignatureStatus
|
|
1258
|
+
&& !compiledPolicySignatureStatus.valid
|
|
1259
|
+
&& (requireSignedArtifacts || compiledPolicySignatureStatus.present)) {
|
|
1260
|
+
strictErrors.push(`Compiled policy artifact signature validation failed (${compiledPolicySignatureStatus.issues.join('; ') || 'unknown issue'})`);
|
|
1261
|
+
}
|
|
1262
|
+
if (changeContractSignatureStatus
|
|
1263
|
+
&& !changeContractSignatureStatus.valid
|
|
1264
|
+
&& (requireSignedArtifacts || changeContractSignatureStatus.present)) {
|
|
1265
|
+
strictErrors.push(`Change contract artifact signature validation failed (${changeContractSignatureStatus.issues.join('; ') || 'unknown issue'})`);
|
|
1266
|
+
}
|
|
964
1267
|
if (strictErrors.length > 0) {
|
|
965
1268
|
const message = `Strict artifact mode requires deterministic compiled-policy + change-contract artifacts.\n- ${strictErrors.join('\n- ')}`;
|
|
966
1269
|
if (options.json) {
|
|
@@ -995,7 +1298,60 @@ async function verifyCommand(options) {
|
|
|
995
1298
|
strictErrors.forEach((entry) => {
|
|
996
1299
|
console.log(chalk.red(` • ${entry}`));
|
|
997
1300
|
});
|
|
998
|
-
console.log(chalk.dim('\nSet --compiled-policy and --change-contract with valid artifacts before verify
|
|
1301
|
+
console.log(chalk.dim('\nSet --compiled-policy and --change-contract with valid artifacts before verify.'));
|
|
1302
|
+
if (requireSignedArtifacts) {
|
|
1303
|
+
console.log(chalk.dim('Enable signing keys via NEURCODE_GOVERNANCE_SIGNING_KEY or NEURCODE_GOVERNANCE_SIGNING_KEYS to generate signed artifacts.\n'));
|
|
1304
|
+
}
|
|
1305
|
+
else {
|
|
1306
|
+
console.log('');
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
process.exit(2);
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
if (!strictArtifactMode && requireSignedArtifacts) {
|
|
1313
|
+
const signatureErrors = [];
|
|
1314
|
+
if (compiledPolicyRead.artifact && compiledPolicySignatureStatus && !compiledPolicySignatureStatus.valid) {
|
|
1315
|
+
signatureErrors.push(`Compiled policy artifact signature validation failed (${compiledPolicySignatureStatus.issues.join('; ') || 'unknown issue'})`);
|
|
1316
|
+
}
|
|
1317
|
+
if (changeContractRead.contract && changeContractSignatureStatus && !changeContractSignatureStatus.valid) {
|
|
1318
|
+
signatureErrors.push(`Change contract artifact signature validation failed (${changeContractSignatureStatus.issues.join('; ') || 'unknown issue'})`);
|
|
1319
|
+
}
|
|
1320
|
+
if (signatureErrors.length > 0) {
|
|
1321
|
+
const message = `Signed artifact enforcement failed.\n- ${signatureErrors.join('\n- ')}`;
|
|
1322
|
+
if (options.json) {
|
|
1323
|
+
emitVerifyJson({
|
|
1324
|
+
grade: 'F',
|
|
1325
|
+
score: 0,
|
|
1326
|
+
verdict: 'FAIL',
|
|
1327
|
+
violations: signatureErrors.map((entry) => ({
|
|
1328
|
+
file: entry.toLowerCase().includes('compiled policy') ? compiledPolicyRead.path : changeContractRead.path,
|
|
1329
|
+
rule: 'signed_artifacts_required',
|
|
1330
|
+
severity: 'block',
|
|
1331
|
+
message: entry,
|
|
1332
|
+
})),
|
|
1333
|
+
adherenceScore: 0,
|
|
1334
|
+
bloatCount: 0,
|
|
1335
|
+
bloatFiles: [],
|
|
1336
|
+
plannedFilesModified: 0,
|
|
1337
|
+
totalPlannedFiles: 0,
|
|
1338
|
+
message,
|
|
1339
|
+
scopeGuardPassed: false,
|
|
1340
|
+
mode: 'signed_artifacts_required',
|
|
1341
|
+
policyOnly: options.policyOnly === true,
|
|
1342
|
+
changeContract: changeContractSummary,
|
|
1343
|
+
...(compiledPolicyMetadata ? { policyCompilation: compiledPolicyMetadata } : {}),
|
|
1344
|
+
});
|
|
1345
|
+
}
|
|
1346
|
+
else {
|
|
1347
|
+
(0, scope_telemetry_1.printScopeTelemetry)(chalk, scopeTelemetry, {
|
|
1348
|
+
includeBlockedWarning: true,
|
|
1349
|
+
});
|
|
1350
|
+
console.log(chalk.red('\n⛔ Signed Artifact Requirements Failed'));
|
|
1351
|
+
signatureErrors.forEach((entry) => {
|
|
1352
|
+
console.log(chalk.red(` • ${entry}`));
|
|
1353
|
+
});
|
|
1354
|
+
console.log(chalk.dim('\nEnable signing keys via NEURCODE_GOVERNANCE_SIGNING_KEY or NEURCODE_GOVERNANCE_SIGNING_KEYS and regenerate artifacts.\n'));
|
|
999
1355
|
}
|
|
1000
1356
|
process.exit(2);
|
|
1001
1357
|
}
|
|
@@ -1016,6 +1372,28 @@ async function verifyCommand(options) {
|
|
|
1016
1372
|
else if (changeContractRead.contract) {
|
|
1017
1373
|
console.log(chalk.dim(` Change contract loaded: ${changeContractRead.path}`));
|
|
1018
1374
|
}
|
|
1375
|
+
if (compiledPolicySignatureStatus) {
|
|
1376
|
+
if (compiledPolicySignatureStatus.valid) {
|
|
1377
|
+
console.log(chalk.dim(` Compiled policy signature: valid${compiledPolicySignatureStatus.verifiedWithKeyId ? ` (key ${compiledPolicySignatureStatus.verifiedWithKeyId})` : ''}`));
|
|
1378
|
+
}
|
|
1379
|
+
else if (compiledPolicySignatureStatus.present || requireSignedArtifacts) {
|
|
1380
|
+
console.log(chalk.yellow(` Compiled policy signature: invalid (${compiledPolicySignatureStatus.issues.join('; ') || 'unknown issue'})`));
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
if (changeContractSignatureStatus) {
|
|
1384
|
+
if (changeContractSignatureStatus.valid) {
|
|
1385
|
+
console.log(chalk.dim(` Change contract signature: valid${changeContractSignatureStatus.verifiedWithKeyId ? ` (key ${changeContractSignatureStatus.verifiedWithKeyId})` : ''}`));
|
|
1386
|
+
}
|
|
1387
|
+
else if (changeContractSignatureStatus.present || requireSignedArtifacts) {
|
|
1388
|
+
console.log(chalk.yellow(` Change contract signature: invalid (${changeContractSignatureStatus.issues.join('; ') || 'unknown issue'})`));
|
|
1389
|
+
}
|
|
1390
|
+
}
|
|
1391
|
+
if (ciEnterpriseDefaultStrict && !explicitStrictArtifactMode) {
|
|
1392
|
+
console.log(chalk.dim(' CI enterprise mode detected: strict deterministic artifact enforcement is auto-enabled (set NEURCODE_VERIFY_ALLOW_NON_STRICT_CI=1 to opt out).'));
|
|
1393
|
+
}
|
|
1394
|
+
if (requireSignedArtifacts) {
|
|
1395
|
+
console.log(chalk.dim(' Artifact signature enforcement: enabled (set NEURCODE_VERIFY_ALLOW_UNSIGNED_ARTIFACTS=1 to relax)'));
|
|
1396
|
+
}
|
|
1019
1397
|
}
|
|
1020
1398
|
// Load configuration
|
|
1021
1399
|
const config = (0, config_1.loadConfig)();
|
|
@@ -1043,6 +1421,72 @@ async function verifyCommand(options) {
|
|
|
1043
1421
|
// Ensure no trailing slash
|
|
1044
1422
|
config.apiUrl = config.apiUrl.replace(/\/$/, '');
|
|
1045
1423
|
}
|
|
1424
|
+
const enforceCompatibilityHandshake = isEnabledFlag(process.env.NEURCODE_VERIFY_ENFORCE_COMPAT_HANDSHAKE)
|
|
1425
|
+
|| strictArtifactMode
|
|
1426
|
+
|| (process.env.CI === 'true' && Boolean(config.apiKey));
|
|
1427
|
+
if (config.apiKey && config.apiUrl) {
|
|
1428
|
+
const compatibilityProbe = await probeApiRuntimeCompatibility(config.apiUrl);
|
|
1429
|
+
if (compatibilityProbe.status !== 'ok' && enforceCompatibilityHandshake) {
|
|
1430
|
+
const failureMessages = compatibilityProbe.messages.length > 0
|
|
1431
|
+
? compatibilityProbe.messages
|
|
1432
|
+
: ['Runtime compatibility handshake did not return a successful result.'];
|
|
1433
|
+
const message = `Runtime compatibility handshake failed against ${compatibilityProbe.healthUrl}.\n` +
|
|
1434
|
+
failureMessages.map((entry) => `- ${entry}`).join('\n');
|
|
1435
|
+
if (options.json) {
|
|
1436
|
+
emitVerifyJson({
|
|
1437
|
+
grade: 'F',
|
|
1438
|
+
score: 0,
|
|
1439
|
+
verdict: 'FAIL',
|
|
1440
|
+
violations: failureMessages.map((entry) => ({
|
|
1441
|
+
file: 'runtime-compatibility',
|
|
1442
|
+
rule: 'runtime_compatibility_handshake',
|
|
1443
|
+
severity: 'block',
|
|
1444
|
+
message: entry,
|
|
1445
|
+
})),
|
|
1446
|
+
adherenceScore: 0,
|
|
1447
|
+
bloatCount: 0,
|
|
1448
|
+
bloatFiles: [],
|
|
1449
|
+
plannedFilesModified: 0,
|
|
1450
|
+
totalPlannedFiles: 0,
|
|
1451
|
+
message,
|
|
1452
|
+
scopeGuardPassed: false,
|
|
1453
|
+
mode: 'runtime_compatibility_failed',
|
|
1454
|
+
policyOnly: options.policyOnly === true,
|
|
1455
|
+
changeContract: changeContractSummary,
|
|
1456
|
+
...(compiledPolicyMetadata ? { policyCompilation: compiledPolicyMetadata } : {}),
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
else {
|
|
1460
|
+
console.log(chalk.red('\n⛔ Runtime Compatibility Handshake Failed'));
|
|
1461
|
+
failureMessages.forEach((entry) => {
|
|
1462
|
+
console.log(chalk.red(` • ${entry}`));
|
|
1463
|
+
});
|
|
1464
|
+
console.log(chalk.dim(` Health endpoint: ${compatibilityProbe.healthUrl}`));
|
|
1465
|
+
if (compatibilityProbe.apiVersion) {
|
|
1466
|
+
console.log(chalk.dim(` API version: ${compatibilityProbe.apiVersion}`));
|
|
1467
|
+
}
|
|
1468
|
+
console.log(chalk.dim(` CLI version: ${CLI_COMPONENT_VERSION}`));
|
|
1469
|
+
console.log(chalk.dim(' Upgrade/downgrade CLI, Action, or API to satisfy the runtime compatibility contract before running verify.\n'));
|
|
1470
|
+
}
|
|
1471
|
+
process.exit(2);
|
|
1472
|
+
}
|
|
1473
|
+
if (compatibilityProbe.status === 'error' && !options.json) {
|
|
1474
|
+
console.log(chalk.yellow('\n⚠️ Runtime compatibility mismatch detected (advisory mode).'));
|
|
1475
|
+
compatibilityProbe.messages.forEach((entry) => {
|
|
1476
|
+
console.log(chalk.yellow(` • ${entry}`));
|
|
1477
|
+
});
|
|
1478
|
+
}
|
|
1479
|
+
else if (compatibilityProbe.status === 'warn' && !options.json) {
|
|
1480
|
+
compatibilityProbe.messages.forEach((entry) => {
|
|
1481
|
+
console.log(chalk.dim(` ${entry}`));
|
|
1482
|
+
});
|
|
1483
|
+
}
|
|
1484
|
+
else if (compatibilityProbe.status === 'ok'
|
|
1485
|
+
&& !options.json
|
|
1486
|
+
&& isEnabledFlag(process.env.NEURCODE_VERIFY_VERBOSE_COMPAT_HANDSHAKE)) {
|
|
1487
|
+
console.log(chalk.dim(` Runtime compatibility check passed (CLI ${CLI_COMPONENT_VERSION}, API ${compatibilityProbe.apiVersion || 'unknown'})`));
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1046
1490
|
// Explicitly load config file to get sessionId and lastSessionId
|
|
1047
1491
|
const configPath = (0, path_1.join)(projectRoot, 'neurcode.config.json');
|
|
1048
1492
|
let configData = {};
|
|
@@ -1065,11 +1509,6 @@ async function verifyCommand(options) {
|
|
|
1065
1509
|
orgId: (0, state_1.getOrgId)(),
|
|
1066
1510
|
projectId: projectId || null,
|
|
1067
1511
|
};
|
|
1068
|
-
const signingConfig = resolveGovernanceSigningConfig();
|
|
1069
|
-
const aiLogSigningKey = signingConfig.signingKey;
|
|
1070
|
-
const aiLogSigningKeyId = signingConfig.signingKeyId;
|
|
1071
|
-
const aiLogSigningKeys = signingConfig.signingKeys;
|
|
1072
|
-
const aiLogSigner = signingConfig.signer;
|
|
1073
1512
|
let orgGovernanceSettings = null;
|
|
1074
1513
|
if (config.apiKey) {
|
|
1075
1514
|
try {
|
|
@@ -1095,7 +1534,6 @@ async function verifyCommand(options) {
|
|
|
1095
1534
|
}
|
|
1096
1535
|
}
|
|
1097
1536
|
const signedLogsRequired = isSignedAiLogsRequired(orgGovernanceSettings);
|
|
1098
|
-
const hasSigningMaterial = Boolean(aiLogSigningKey) || Object.keys(aiLogSigningKeys).length > 0;
|
|
1099
1537
|
const recordVerifyEvent = (verdict, note, changedFiles, planId) => {
|
|
1100
1538
|
if (!brainScope.orgId || !brainScope.projectId) {
|
|
1101
1539
|
return;
|
|
@@ -1358,7 +1796,9 @@ async function verifyCommand(options) {
|
|
|
1358
1796
|
if (options.policyOnly) {
|
|
1359
1797
|
await runPolicyOnlyModeAndExit('explicit');
|
|
1360
1798
|
}
|
|
1361
|
-
const requirePlan = options.requirePlan === true
|
|
1799
|
+
const requirePlan = options.requirePlan === true
|
|
1800
|
+
|| process.env.NEURCODE_VERIFY_REQUIRE_PLAN === '1'
|
|
1801
|
+
|| strictArtifactMode;
|
|
1362
1802
|
// Get planId: Priority 1: options flag, Priority 2: state file (.neurcode/config.json), Priority 3: legacy config
|
|
1363
1803
|
let planId = options.planId;
|
|
1364
1804
|
if (!planId) {
|
|
@@ -1865,7 +2305,16 @@ async function verifyCommand(options) {
|
|
|
1865
2305
|
const diffFilesForPolicy = diffFiles.filter((f) => !shouldIgnore(f.path));
|
|
1866
2306
|
const policyResult = (0, policy_engine_1.evaluateRules)(diffFilesForPolicy, effectiveRules.allRules);
|
|
1867
2307
|
policyViolations = policyResult.violations.filter((v) => !shouldIgnore(v.file));
|
|
1868
|
-
const
|
|
2308
|
+
const policyExceptionResolution = await resolveEffectivePolicyExceptions({
|
|
2309
|
+
client,
|
|
2310
|
+
projectRoot,
|
|
2311
|
+
useOrgControlPlane: Boolean(config.apiKey),
|
|
2312
|
+
governance,
|
|
2313
|
+
});
|
|
2314
|
+
if (policyExceptionResolution.warning && !options.json) {
|
|
2315
|
+
console.log(chalk.dim(` ${policyExceptionResolution.warning}`));
|
|
2316
|
+
}
|
|
2317
|
+
const configuredPolicyExceptions = policyExceptionResolution.exceptions;
|
|
1869
2318
|
const exceptionDecision = (0, policy_exceptions_1.applyPolicyExceptions)(policyViolations, configuredPolicyExceptions, {
|
|
1870
2319
|
requireApproval: governance.exceptionApprovals.required,
|
|
1871
2320
|
minApprovals: governance.exceptionApprovals.minApprovals,
|
|
@@ -1904,6 +2353,10 @@ async function verifyCommand(options) {
|
|
|
1904
2353
|
}
|
|
1905
2354
|
policyDecision = resolvePolicyDecisionFromViolations(policyViolations);
|
|
1906
2355
|
const policyExceptionsSummary = {
|
|
2356
|
+
sourceMode: policyExceptionResolution.mode,
|
|
2357
|
+
sourceWarning: policyExceptionResolution.warning,
|
|
2358
|
+
localConfigured: policyExceptionResolution.localConfigured,
|
|
2359
|
+
orgConfigured: policyExceptionResolution.orgConfigured,
|
|
1907
2360
|
configured: configuredPolicyExceptions.length,
|
|
1908
2361
|
active: exceptionDecision.activeExceptions.length,
|
|
1909
2362
|
usable: exceptionDecision.usableExceptions.length,
|
|
@@ -1986,6 +2439,7 @@ async function verifyCommand(options) {
|
|
|
1986
2439
|
valid: changeContractEvaluation.valid,
|
|
1987
2440
|
planId: changeContractRead.contract?.planId || null,
|
|
1988
2441
|
contractId: changeContractRead.contract?.contractId || null,
|
|
2442
|
+
signature: changeContractSummary.signature,
|
|
1989
2443
|
coverage: changeContractEvaluation.coverage,
|
|
1990
2444
|
violations: changeContractEvaluation.violations.map((item) => ({
|
|
1991
2445
|
code: item.code,
|
|
@@ -2065,6 +2519,9 @@ async function verifyCommand(options) {
|
|
|
2065
2519
|
// Call verify API
|
|
2066
2520
|
if (!options.json) {
|
|
2067
2521
|
console.log(chalk.dim(' Sending to Neurcode API...\n'));
|
|
2522
|
+
if (options.asyncMode) {
|
|
2523
|
+
console.log(chalk.dim(' Queue-backed verification enabled (async job mode).'));
|
|
2524
|
+
}
|
|
2068
2525
|
}
|
|
2069
2526
|
try {
|
|
2070
2527
|
let verifySource = 'api';
|
|
@@ -2075,7 +2532,13 @@ async function verifyCommand(options) {
|
|
|
2075
2532
|
.map((policy) => policy.rule_text)
|
|
2076
2533
|
.filter((ruleText) => typeof ruleText === 'string' && ruleText.trim().length > 0);
|
|
2077
2534
|
try {
|
|
2078
|
-
verifyResult = await client.verifyPlan(finalPlanId, diffStats, changedFiles, projectId, intentConstraintsForVerification, deterministicPolicyRules, 'api', compiledPolicyMetadata
|
|
2535
|
+
verifyResult = await client.verifyPlan(finalPlanId, diffStats, changedFiles, projectId, intentConstraintsForVerification, deterministicPolicyRules, 'api', compiledPolicyMetadata, {
|
|
2536
|
+
async: options.asyncMode === true,
|
|
2537
|
+
pollIntervalMs: Number.isFinite(options.verifyJobPollMs) ? options.verifyJobPollMs : undefined,
|
|
2538
|
+
timeoutMs: Number.isFinite(options.verifyJobTimeoutMs) ? options.verifyJobTimeoutMs : undefined,
|
|
2539
|
+
idempotencyKey: options.verifyIdempotencyKey,
|
|
2540
|
+
maxAttempts: Number.isFinite(options.verifyJobMaxAttempts) ? options.verifyJobMaxAttempts : undefined,
|
|
2541
|
+
});
|
|
2079
2542
|
}
|
|
2080
2543
|
catch (verifyApiError) {
|
|
2081
2544
|
if (planFilesForVerification.length === 0) {
|
|
@@ -2291,6 +2754,7 @@ async function verifyCommand(options) {
|
|
|
2291
2754
|
if (governanceResult) {
|
|
2292
2755
|
displayGovernanceInsights(governanceResult, { explain: options.explain });
|
|
2293
2756
|
}
|
|
2757
|
+
console.log(chalk.dim(`\n Policy exceptions source: ${describePolicyExceptionSource(policyExceptionsSummary.sourceMode)}`));
|
|
2294
2758
|
if (policyExceptionsSummary.suppressed > 0) {
|
|
2295
2759
|
console.log(chalk.yellow(`\n⚠️ Policy exceptions applied: ${policyExceptionsSummary.suppressed}`));
|
|
2296
2760
|
if (policyExceptionsSummary.matchedExceptionIds.length > 0) {
|