@neurcode-ai/cli 0.9.35 → 0.9.37
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 +345 -1
- package/dist/api-client.d.ts.map +1 -1
- package/dist/api-client.js +237 -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 +611 -26
- 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 +14 -0
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +564 -14
- package/dist/commands/verify.js.map +1 -1
- package/dist/index.js +94 -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/dist/utils/policy-exceptions.d.ts +11 -1
- package/dist/utils/policy-exceptions.d.ts.map +1 -1
- package/dist/utils/policy-exceptions.js +94 -6
- package/dist/utils/policy-exceptions.js.map +1 -1
- package/dist/utils/policy-governance.d.ts +22 -1
- package/dist/utils/policy-governance.d.ts.map +1 -1
- package/dist/utils/policy-governance.js +178 -14
- package/dist/utils/policy-governance.js.map +1 -1
- package/dist/utils/policy-packs.d.ts +1 -1
- package/dist/utils/policy-packs.d.ts.map +1 -1
- package/dist/utils/policy-packs.js +185 -0
- package/dist/utils/policy-packs.js.map +1 -1
- package/package.json +15 -13
- package/LICENSE +0 -201
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
|
|
@@ -363,8 +514,14 @@ function resolvePolicyDecisionFromViolations(violations) {
|
|
|
363
514
|
}
|
|
364
515
|
function explainExceptionEligibilityReason(reason) {
|
|
365
516
|
switch (reason) {
|
|
517
|
+
case 'reason_required':
|
|
518
|
+
return 'exception reason does not meet governance minimum length';
|
|
519
|
+
case 'duration_exceeds_max':
|
|
520
|
+
return 'exception expiry window exceeds governance maximum duration';
|
|
366
521
|
case 'approval_required':
|
|
367
522
|
return 'exception exists but approvals are required';
|
|
523
|
+
case 'critical_approvals_required':
|
|
524
|
+
return 'critical rule exception requires additional independent approvals';
|
|
368
525
|
case 'insufficient_approvals':
|
|
369
526
|
return 'exception exists but approval threshold is not met';
|
|
370
527
|
case 'self_approval_only':
|
|
@@ -385,6 +542,94 @@ function resolveAuditIntegrityStatus(requireIntegrity, auditIntegrity) {
|
|
|
385
542
|
issues,
|
|
386
543
|
};
|
|
387
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
|
+
}
|
|
388
633
|
async function recordVerificationIfRequested(options, config, payload) {
|
|
389
634
|
if (!options.record) {
|
|
390
635
|
return;
|
|
@@ -740,15 +985,30 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
|
|
|
740
985
|
const policyResult = (0, policy_engine_1.evaluateRules)(diffFilesForPolicy, effectiveRules.allRules);
|
|
741
986
|
policyViolations = (policyResult.violations || []);
|
|
742
987
|
policyViolations = policyViolations.filter((v) => !ignoreFilter(v.file));
|
|
743
|
-
const
|
|
988
|
+
const localPolicyGovernance = (0, policy_governance_1.readPolicyGovernanceConfig)(projectRoot);
|
|
989
|
+
const governance = (0, policy_governance_1.mergePolicyGovernanceWithOrgOverrides)(localPolicyGovernance, orgGovernanceSettings?.policyGovernance);
|
|
744
990
|
const auditIntegrity = (0, policy_audit_1.verifyPolicyAuditIntegrity)(projectRoot);
|
|
745
991
|
const auditIntegrityStatus = resolveAuditIntegrityStatus(governance.audit.requireIntegrity, auditIntegrity);
|
|
746
|
-
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;
|
|
747
1002
|
const exceptionDecision = (0, policy_exceptions_1.applyPolicyExceptions)(policyViolations, configuredPolicyExceptions, {
|
|
748
1003
|
requireApproval: governance.exceptionApprovals.required,
|
|
749
1004
|
minApprovals: governance.exceptionApprovals.minApprovals,
|
|
750
1005
|
disallowSelfApproval: governance.exceptionApprovals.disallowSelfApproval,
|
|
751
1006
|
allowedApprovers: governance.exceptionApprovals.allowedApprovers,
|
|
1007
|
+
requireReason: governance.exceptionApprovals.requireReason,
|
|
1008
|
+
minReasonLength: governance.exceptionApprovals.minReasonLength,
|
|
1009
|
+
maxExpiryDays: governance.exceptionApprovals.maxExpiryDays,
|
|
1010
|
+
criticalRulePatterns: governance.exceptionApprovals.criticalRulePatterns,
|
|
1011
|
+
criticalMinApprovals: governance.exceptionApprovals.criticalMinApprovals,
|
|
752
1012
|
});
|
|
753
1013
|
const suppressedViolations = exceptionDecision.suppressedViolations.filter((item) => !ignoreFilter(item.file));
|
|
754
1014
|
const blockedViolations = exceptionDecision.blockedViolations
|
|
@@ -757,7 +1017,10 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
|
|
|
757
1017
|
file: item.file,
|
|
758
1018
|
rule: item.rule,
|
|
759
1019
|
severity: 'block',
|
|
760
|
-
message: `Exception ${item.exceptionId} cannot be applied: ${explainExceptionEligibilityReason(item.eligibilityReason)}
|
|
1020
|
+
message: `Exception ${item.exceptionId} cannot be applied: ${explainExceptionEligibilityReason(item.eligibilityReason)}` +
|
|
1021
|
+
(item.requiredApprovals > 0
|
|
1022
|
+
? ` (approvals ${item.effectiveApprovals}/${item.requiredApprovals}${item.critical ? ', critical rule gate' : ''})`
|
|
1023
|
+
: ''),
|
|
761
1024
|
...(item.line != null ? { line: item.line } : {}),
|
|
762
1025
|
}));
|
|
763
1026
|
policyViolations = [
|
|
@@ -789,6 +1052,10 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
|
|
|
789
1052
|
? `Policy violations: ${policyViolations.map((v) => `${v.file}: ${v.message || v.rule}`).join('; ')}`
|
|
790
1053
|
: 'Policy check completed';
|
|
791
1054
|
const policyExceptionsSummary = {
|
|
1055
|
+
sourceMode: policyExceptionResolution.mode,
|
|
1056
|
+
sourceWarning: policyExceptionResolution.warning,
|
|
1057
|
+
localConfigured: policyExceptionResolution.localConfigured,
|
|
1058
|
+
orgConfigured: policyExceptionResolution.orgConfigured,
|
|
792
1059
|
configured: configuredPolicyExceptions.length,
|
|
793
1060
|
active: exceptionDecision.activeExceptions.length,
|
|
794
1061
|
usable: exceptionDecision.usableExceptions.length,
|
|
@@ -871,6 +1138,7 @@ async function executePolicyOnlyMode(options, diffFiles, ignoreFilter, projectRo
|
|
|
871
1138
|
console.log(chalk.red(` • ${v.file}: ${v.message || v.rule}`));
|
|
872
1139
|
});
|
|
873
1140
|
}
|
|
1141
|
+
console.log(chalk.dim(` Policy exceptions source: ${describePolicyExceptionSource(policyExceptionsSummary.sourceMode)}`));
|
|
874
1142
|
if (policyExceptionsSummary.suppressed > 0) {
|
|
875
1143
|
console.log(chalk.yellow(` Policy exceptions applied: ${policyExceptionsSummary.suppressed}`));
|
|
876
1144
|
}
|
|
@@ -913,9 +1181,48 @@ async function verifyCommand(options) {
|
|
|
913
1181
|
};
|
|
914
1182
|
const enforceChangeContract = options.enforceChangeContract === true ||
|
|
915
1183
|
isEnabledFlag(process.env.NEURCODE_VERIFY_ENFORCE_CHANGE_CONTRACT);
|
|
1184
|
+
const explicitStrictArtifactMode = options.strictArtifacts === true ||
|
|
1185
|
+
isEnabledFlag(process.env.NEURCODE_VERIFY_STRICT_ARTIFACTS) ||
|
|
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);
|
|
916
1202
|
const changeContractRead = (0, change_contract_1.readChangeContract)(projectRoot, options.changeContract);
|
|
917
1203
|
const compiledPolicyRead = (0, policy_compiler_1.readCompiledPolicyArtifact)(projectRoot, options.compiledPolicy);
|
|
918
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;
|
|
919
1226
|
let changeContractSummary = {
|
|
920
1227
|
path: changeContractRead.path,
|
|
921
1228
|
exists: changeContractRead.exists,
|
|
@@ -923,6 +1230,9 @@ async function verifyCommand(options) {
|
|
|
923
1230
|
valid: changeContractRead.contract ? null : changeContractRead.exists ? false : null,
|
|
924
1231
|
planId: changeContractRead.contract?.planId || null,
|
|
925
1232
|
contractId: changeContractRead.contract?.contractId || null,
|
|
1233
|
+
signature: changeContractSignatureStatus
|
|
1234
|
+
? toArtifactSignatureSummary(changeContractSignatureStatus)
|
|
1235
|
+
: undefined,
|
|
926
1236
|
violations: changeContractRead.error
|
|
927
1237
|
? [
|
|
928
1238
|
{
|
|
@@ -932,6 +1242,120 @@ async function verifyCommand(options) {
|
|
|
932
1242
|
]
|
|
933
1243
|
: [],
|
|
934
1244
|
};
|
|
1245
|
+
if (strictArtifactMode) {
|
|
1246
|
+
const strictErrors = [];
|
|
1247
|
+
if (!compiledPolicyRead.artifact) {
|
|
1248
|
+
strictErrors.push(compiledPolicyRead.error
|
|
1249
|
+
? `Compiled policy artifact invalid (${compiledPolicyRead.error})`
|
|
1250
|
+
: `Compiled policy artifact missing (${compiledPolicyRead.path})`);
|
|
1251
|
+
}
|
|
1252
|
+
if (!changeContractRead.contract) {
|
|
1253
|
+
strictErrors.push(changeContractRead.error
|
|
1254
|
+
? `Change contract artifact invalid (${changeContractRead.error})`
|
|
1255
|
+
: `Change contract artifact missing (${changeContractRead.path})`);
|
|
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
|
+
}
|
|
1267
|
+
if (strictErrors.length > 0) {
|
|
1268
|
+
const message = `Strict artifact mode requires deterministic compiled-policy + change-contract artifacts.\n- ${strictErrors.join('\n- ')}`;
|
|
1269
|
+
if (options.json) {
|
|
1270
|
+
emitVerifyJson({
|
|
1271
|
+
grade: 'F',
|
|
1272
|
+
score: 0,
|
|
1273
|
+
verdict: 'FAIL',
|
|
1274
|
+
violations: strictErrors.map((entry) => ({
|
|
1275
|
+
file: entry.toLowerCase().includes('compiled policy') ? compiledPolicyRead.path : changeContractRead.path,
|
|
1276
|
+
rule: 'deterministic_artifacts_required',
|
|
1277
|
+
severity: 'block',
|
|
1278
|
+
message: entry,
|
|
1279
|
+
})),
|
|
1280
|
+
adherenceScore: 0,
|
|
1281
|
+
bloatCount: 0,
|
|
1282
|
+
bloatFiles: [],
|
|
1283
|
+
plannedFilesModified: 0,
|
|
1284
|
+
totalPlannedFiles: 0,
|
|
1285
|
+
message,
|
|
1286
|
+
scopeGuardPassed: false,
|
|
1287
|
+
mode: 'strict_artifacts_required',
|
|
1288
|
+
policyOnly: options.policyOnly === true,
|
|
1289
|
+
changeContract: changeContractSummary,
|
|
1290
|
+
...(compiledPolicyMetadata ? { policyCompilation: compiledPolicyMetadata } : {}),
|
|
1291
|
+
});
|
|
1292
|
+
}
|
|
1293
|
+
else {
|
|
1294
|
+
(0, scope_telemetry_1.printScopeTelemetry)(chalk, scopeTelemetry, {
|
|
1295
|
+
includeBlockedWarning: true,
|
|
1296
|
+
});
|
|
1297
|
+
console.log(chalk.red('\n⛔ Deterministic Artifact Requirements Failed'));
|
|
1298
|
+
strictErrors.forEach((entry) => {
|
|
1299
|
+
console.log(chalk.red(` • ${entry}`));
|
|
1300
|
+
});
|
|
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'));
|
|
1355
|
+
}
|
|
1356
|
+
process.exit(2);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
935
1359
|
if (!options.json) {
|
|
936
1360
|
(0, scope_telemetry_1.printScopeTelemetry)(chalk, scopeTelemetry, {
|
|
937
1361
|
includeBlockedWarning: true,
|
|
@@ -948,6 +1372,28 @@ async function verifyCommand(options) {
|
|
|
948
1372
|
else if (changeContractRead.contract) {
|
|
949
1373
|
console.log(chalk.dim(` Change contract loaded: ${changeContractRead.path}`));
|
|
950
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
|
+
}
|
|
951
1397
|
}
|
|
952
1398
|
// Load configuration
|
|
953
1399
|
const config = (0, config_1.loadConfig)();
|
|
@@ -975,6 +1421,72 @@ async function verifyCommand(options) {
|
|
|
975
1421
|
// Ensure no trailing slash
|
|
976
1422
|
config.apiUrl = config.apiUrl.replace(/\/$/, '');
|
|
977
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
|
+
}
|
|
978
1490
|
// Explicitly load config file to get sessionId and lastSessionId
|
|
979
1491
|
const configPath = (0, path_1.join)(projectRoot, 'neurcode.config.json');
|
|
980
1492
|
let configData = {};
|
|
@@ -997,11 +1509,6 @@ async function verifyCommand(options) {
|
|
|
997
1509
|
orgId: (0, state_1.getOrgId)(),
|
|
998
1510
|
projectId: projectId || null,
|
|
999
1511
|
};
|
|
1000
|
-
const signingConfig = resolveGovernanceSigningConfig();
|
|
1001
|
-
const aiLogSigningKey = signingConfig.signingKey;
|
|
1002
|
-
const aiLogSigningKeyId = signingConfig.signingKeyId;
|
|
1003
|
-
const aiLogSigningKeys = signingConfig.signingKeys;
|
|
1004
|
-
const aiLogSigner = signingConfig.signer;
|
|
1005
1512
|
let orgGovernanceSettings = null;
|
|
1006
1513
|
if (config.apiKey) {
|
|
1007
1514
|
try {
|
|
@@ -1012,6 +1519,9 @@ async function verifyCommand(options) {
|
|
|
1012
1519
|
requireSignedAiLogs: remoteSettings.requireSignedAiLogs === true,
|
|
1013
1520
|
requireManualApproval: remoteSettings.requireManualApproval !== false,
|
|
1014
1521
|
minimumManualApprovals: Math.max(1, Math.min(5, Math.floor(remoteSettings.minimumManualApprovals || 1))),
|
|
1522
|
+
...(remoteSettings.policyGovernance && typeof remoteSettings.policyGovernance === 'object'
|
|
1523
|
+
? { policyGovernance: remoteSettings.policyGovernance }
|
|
1524
|
+
: {}),
|
|
1015
1525
|
updatedAt: remoteSettings.updatedAt,
|
|
1016
1526
|
};
|
|
1017
1527
|
}
|
|
@@ -1024,7 +1534,6 @@ async function verifyCommand(options) {
|
|
|
1024
1534
|
}
|
|
1025
1535
|
}
|
|
1026
1536
|
const signedLogsRequired = isSignedAiLogsRequired(orgGovernanceSettings);
|
|
1027
|
-
const hasSigningMaterial = Boolean(aiLogSigningKey) || Object.keys(aiLogSigningKeys).length > 0;
|
|
1028
1537
|
const recordVerifyEvent = (verdict, note, changedFiles, planId) => {
|
|
1029
1538
|
if (!brainScope.orgId || !brainScope.projectId) {
|
|
1030
1539
|
return;
|
|
@@ -1287,7 +1796,9 @@ async function verifyCommand(options) {
|
|
|
1287
1796
|
if (options.policyOnly) {
|
|
1288
1797
|
await runPolicyOnlyModeAndExit('explicit');
|
|
1289
1798
|
}
|
|
1290
|
-
const requirePlan = options.requirePlan === true
|
|
1799
|
+
const requirePlan = options.requirePlan === true
|
|
1800
|
+
|| process.env.NEURCODE_VERIFY_REQUIRE_PLAN === '1'
|
|
1801
|
+
|| strictArtifactMode;
|
|
1291
1802
|
// Get planId: Priority 1: options flag, Priority 2: state file (.neurcode/config.json), Priority 3: legacy config
|
|
1292
1803
|
let planId = options.planId;
|
|
1293
1804
|
if (!planId) {
|
|
@@ -1724,7 +2235,11 @@ async function verifyCommand(options) {
|
|
|
1724
2235
|
}
|
|
1725
2236
|
}
|
|
1726
2237
|
// Check user tier - Policy Compliance and A-F Grading are PRO features
|
|
1727
|
-
const
|
|
2238
|
+
const localPolicyGovernance = (0, policy_governance_1.readPolicyGovernanceConfig)(projectRoot);
|
|
2239
|
+
const governance = (0, policy_governance_1.mergePolicyGovernanceWithOrgOverrides)(localPolicyGovernance, orgGovernanceSettings?.policyGovernance);
|
|
2240
|
+
if (!options.json && orgGovernanceSettings?.policyGovernance) {
|
|
2241
|
+
console.log(chalk.dim(' Org policy governance controls active: local config merged with org-level enforcement floor'));
|
|
2242
|
+
}
|
|
1728
2243
|
const auditIntegrity = (0, policy_audit_1.verifyPolicyAuditIntegrity)(projectRoot);
|
|
1729
2244
|
const auditIntegrityStatus = resolveAuditIntegrityStatus(governance.audit.requireIntegrity, auditIntegrity);
|
|
1730
2245
|
const { getUserTier } = await Promise.resolve().then(() => __importStar(require('../utils/tier')));
|
|
@@ -1790,12 +2305,26 @@ async function verifyCommand(options) {
|
|
|
1790
2305
|
const diffFilesForPolicy = diffFiles.filter((f) => !shouldIgnore(f.path));
|
|
1791
2306
|
const policyResult = (0, policy_engine_1.evaluateRules)(diffFilesForPolicy, effectiveRules.allRules);
|
|
1792
2307
|
policyViolations = policyResult.violations.filter((v) => !shouldIgnore(v.file));
|
|
1793
|
-
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;
|
|
1794
2318
|
const exceptionDecision = (0, policy_exceptions_1.applyPolicyExceptions)(policyViolations, configuredPolicyExceptions, {
|
|
1795
2319
|
requireApproval: governance.exceptionApprovals.required,
|
|
1796
2320
|
minApprovals: governance.exceptionApprovals.minApprovals,
|
|
1797
2321
|
disallowSelfApproval: governance.exceptionApprovals.disallowSelfApproval,
|
|
1798
2322
|
allowedApprovers: governance.exceptionApprovals.allowedApprovers,
|
|
2323
|
+
requireReason: governance.exceptionApprovals.requireReason,
|
|
2324
|
+
minReasonLength: governance.exceptionApprovals.minReasonLength,
|
|
2325
|
+
maxExpiryDays: governance.exceptionApprovals.maxExpiryDays,
|
|
2326
|
+
criticalRulePatterns: governance.exceptionApprovals.criticalRulePatterns,
|
|
2327
|
+
criticalMinApprovals: governance.exceptionApprovals.criticalMinApprovals,
|
|
1799
2328
|
});
|
|
1800
2329
|
const suppressedPolicyViolations = exceptionDecision.suppressedViolations.filter((item) => !shouldIgnore(item.file));
|
|
1801
2330
|
const blockedPolicyViolations = exceptionDecision.blockedViolations
|
|
@@ -1804,7 +2333,10 @@ async function verifyCommand(options) {
|
|
|
1804
2333
|
file: item.file,
|
|
1805
2334
|
rule: item.rule,
|
|
1806
2335
|
severity: 'block',
|
|
1807
|
-
message: `Exception ${item.exceptionId} cannot be applied: ${explainExceptionEligibilityReason(item.eligibilityReason)}
|
|
2336
|
+
message: `Exception ${item.exceptionId} cannot be applied: ${explainExceptionEligibilityReason(item.eligibilityReason)}` +
|
|
2337
|
+
(item.requiredApprovals > 0
|
|
2338
|
+
? ` (approvals ${item.effectiveApprovals}/${item.requiredApprovals}${item.critical ? ', critical rule gate' : ''})`
|
|
2339
|
+
: ''),
|
|
1808
2340
|
...(item.line != null ? { line: item.line } : {}),
|
|
1809
2341
|
}));
|
|
1810
2342
|
policyViolations = [
|
|
@@ -1821,6 +2353,10 @@ async function verifyCommand(options) {
|
|
|
1821
2353
|
}
|
|
1822
2354
|
policyDecision = resolvePolicyDecisionFromViolations(policyViolations);
|
|
1823
2355
|
const policyExceptionsSummary = {
|
|
2356
|
+
sourceMode: policyExceptionResolution.mode,
|
|
2357
|
+
sourceWarning: policyExceptionResolution.warning,
|
|
2358
|
+
localConfigured: policyExceptionResolution.localConfigured,
|
|
2359
|
+
orgConfigured: policyExceptionResolution.orgConfigured,
|
|
1824
2360
|
configured: configuredPolicyExceptions.length,
|
|
1825
2361
|
active: exceptionDecision.activeExceptions.length,
|
|
1826
2362
|
usable: exceptionDecision.usableExceptions.length,
|
|
@@ -1903,6 +2439,7 @@ async function verifyCommand(options) {
|
|
|
1903
2439
|
valid: changeContractEvaluation.valid,
|
|
1904
2440
|
planId: changeContractRead.contract?.planId || null,
|
|
1905
2441
|
contractId: changeContractRead.contract?.contractId || null,
|
|
2442
|
+
signature: changeContractSummary.signature,
|
|
1906
2443
|
coverage: changeContractEvaluation.coverage,
|
|
1907
2444
|
violations: changeContractEvaluation.violations.map((item) => ({
|
|
1908
2445
|
code: item.code,
|
|
@@ -1982,6 +2519,9 @@ async function verifyCommand(options) {
|
|
|
1982
2519
|
// Call verify API
|
|
1983
2520
|
if (!options.json) {
|
|
1984
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
|
+
}
|
|
1985
2525
|
}
|
|
1986
2526
|
try {
|
|
1987
2527
|
let verifySource = 'api';
|
|
@@ -1992,7 +2532,13 @@ async function verifyCommand(options) {
|
|
|
1992
2532
|
.map((policy) => policy.rule_text)
|
|
1993
2533
|
.filter((ruleText) => typeof ruleText === 'string' && ruleText.trim().length > 0);
|
|
1994
2534
|
try {
|
|
1995
|
-
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
|
+
});
|
|
1996
2542
|
}
|
|
1997
2543
|
catch (verifyApiError) {
|
|
1998
2544
|
if (planFilesForVerification.length === 0) {
|
|
@@ -2208,6 +2754,7 @@ async function verifyCommand(options) {
|
|
|
2208
2754
|
if (governanceResult) {
|
|
2209
2755
|
displayGovernanceInsights(governanceResult, { explain: options.explain });
|
|
2210
2756
|
}
|
|
2757
|
+
console.log(chalk.dim(`\n Policy exceptions source: ${describePolicyExceptionSource(policyExceptionsSummary.sourceMode)}`));
|
|
2211
2758
|
if (policyExceptionsSummary.suppressed > 0) {
|
|
2212
2759
|
console.log(chalk.yellow(`\n⚠️ Policy exceptions applied: ${policyExceptionsSummary.suppressed}`));
|
|
2213
2760
|
if (policyExceptionsSummary.matchedExceptionIds.length > 0) {
|
|
@@ -2596,6 +3143,9 @@ function buildGovernancePayload(governance, orgGovernanceSettings, options) {
|
|
|
2596
3143
|
requireSignedAiLogs: orgGovernanceSettings.requireSignedAiLogs,
|
|
2597
3144
|
requireManualApproval: orgGovernanceSettings.requireManualApproval,
|
|
2598
3145
|
minimumManualApprovals: orgGovernanceSettings.minimumManualApprovals,
|
|
3146
|
+
...(orgGovernanceSettings.policyGovernance
|
|
3147
|
+
? { policyGovernance: orgGovernanceSettings.policyGovernance }
|
|
3148
|
+
: {}),
|
|
2599
3149
|
updatedAt: orgGovernanceSettings.updatedAt || null,
|
|
2600
3150
|
}
|
|
2601
3151
|
: null,
|