@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/policy.js
CHANGED
|
@@ -10,6 +10,7 @@ const policy_governance_1 = require("../utils/policy-governance");
|
|
|
10
10
|
const policy_audit_1 = require("../utils/policy-audit");
|
|
11
11
|
const policy_packs_1 = require("../utils/policy-packs");
|
|
12
12
|
const policy_compiler_1 = require("../utils/policy-compiler");
|
|
13
|
+
const artifact_signature_1 = require("../utils/artifact-signature");
|
|
13
14
|
// Import chalk with fallback
|
|
14
15
|
let chalk;
|
|
15
16
|
try {
|
|
@@ -63,6 +64,21 @@ function resolveActor(explicit) {
|
|
|
63
64
|
return explicit.trim();
|
|
64
65
|
return process.env.NEURCODE_ACTOR || process.env.GITHUB_ACTOR || process.env.USER || 'unknown';
|
|
65
66
|
}
|
|
67
|
+
function validateExceptionWindowByGovernance(expiresAt, maxExpiryDays) {
|
|
68
|
+
const expiryMs = Date.parse(expiresAt);
|
|
69
|
+
if (!Number.isFinite(expiryMs)) {
|
|
70
|
+
throw new Error('expiresAt must be a valid ISO datetime');
|
|
71
|
+
}
|
|
72
|
+
const maxWindowMs = Math.max(1, maxExpiryDays) * 24 * 60 * 60 * 1000;
|
|
73
|
+
if (expiryMs - Date.now() > maxWindowMs) {
|
|
74
|
+
throw new Error(`exception expiry exceeds governance max window (${maxExpiryDays} days)`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function normalizeListLimit(value, fallback, min, max) {
|
|
78
|
+
if (!Number.isFinite(value))
|
|
79
|
+
return fallback;
|
|
80
|
+
return Math.max(min, Math.min(max, Math.floor(Number(value))));
|
|
81
|
+
}
|
|
66
82
|
async function resolveCustomPolicies(client, includeDashboardPolicies, requireDashboardPolicies) {
|
|
67
83
|
if (!includeDashboardPolicies) {
|
|
68
84
|
return {
|
|
@@ -153,7 +169,7 @@ function policyCommand(program) {
|
|
|
153
169
|
policy
|
|
154
170
|
.command('install')
|
|
155
171
|
.description('Install a policy pack for this repository')
|
|
156
|
-
.argument('<pack-id>', 'Policy pack ID (
|
|
172
|
+
.argument('<pack-id>', 'Policy pack ID (run `neurcode policy list` for all available stacks)')
|
|
157
173
|
.option('--force', 'Replace any existing installed policy pack')
|
|
158
174
|
.option('--json', 'Output as JSON')
|
|
159
175
|
.action((packId, options) => {
|
|
@@ -372,6 +388,7 @@ function policyCommand(program) {
|
|
|
372
388
|
.option('--intent <text>', 'Optional intent constraints to compile alongside policy rules')
|
|
373
389
|
.option('--no-dashboard', 'Exclude dashboard custom policies from compiled artifact')
|
|
374
390
|
.option('--require-dashboard', 'Fail if dashboard custom policies cannot be loaded')
|
|
391
|
+
.option('--require-deterministic-match', 'Fail if any intent statement cannot be compiled into deterministic enforcement rules')
|
|
375
392
|
.option('--output <path>', 'Output file path (default: neurcode.policy.compiled.json)')
|
|
376
393
|
.option('--json', 'Output as JSON')
|
|
377
394
|
.action(async (options) => {
|
|
@@ -392,7 +409,7 @@ function policyCommand(program) {
|
|
|
392
409
|
customRules,
|
|
393
410
|
includeDashboardPolicies: customPolicyResolution.includeDashboardPolicies,
|
|
394
411
|
});
|
|
395
|
-
const
|
|
412
|
+
const compiledUnsigned = (0, policy_compiler_1.buildCompiledPolicyArtifact)({
|
|
396
413
|
includeDashboardPolicies: customPolicyResolution.includeDashboardPolicies,
|
|
397
414
|
policyLockPath: (0, policy_packs_1.getPolicyLockPath)(cwd),
|
|
398
415
|
policyLockFingerprint: snapshot.effective.fingerprint,
|
|
@@ -410,6 +427,19 @@ function policyCommand(program) {
|
|
|
410
427
|
intentConstraints: options.intent,
|
|
411
428
|
policyRules: customPolicyResolution.customPolicies.map((policy) => policy.rule_text),
|
|
412
429
|
});
|
|
430
|
+
const artifactSigningConfig = (0, artifact_signature_1.resolveGovernanceArtifactSigningConfigFromEnv)();
|
|
431
|
+
const compiled = (0, artifact_signature_1.signGovernanceArtifact)(compiledUnsigned, artifactSigningConfig);
|
|
432
|
+
if (options.requireDeterministicMatch === true
|
|
433
|
+
&& compiled.compilation.unmatchedStatements.length > 0) {
|
|
434
|
+
const unmatchedError = new Error(`Deterministic policy compilation blocked: ${compiled.compilation.unmatchedStatements.length} intent statement(s) could not be converted into enforceable rules.`);
|
|
435
|
+
unmatchedError.code = 'POLICY_COMPILE_UNMATCHED_INTENT';
|
|
436
|
+
unmatchedError.unmatchedStatements = [
|
|
437
|
+
...compiled.compilation.unmatchedStatements,
|
|
438
|
+
];
|
|
439
|
+
unmatchedError.deterministicRuleCount =
|
|
440
|
+
compiled.compilation.deterministicRuleCount;
|
|
441
|
+
throw unmatchedError;
|
|
442
|
+
}
|
|
413
443
|
const outputPath = (0, policy_compiler_1.writeCompiledPolicyArtifact)(cwd, compiled, options.output);
|
|
414
444
|
const readBack = (0, policy_compiler_1.readCompiledPolicyArtifact)(cwd, options.output);
|
|
415
445
|
try {
|
|
@@ -423,6 +453,8 @@ function policyCommand(program) {
|
|
|
423
453
|
deterministicRuleCount: compiled.compilation.deterministicRuleCount,
|
|
424
454
|
unmatchedStatements: compiled.compilation.unmatchedStatements.length,
|
|
425
455
|
dashboardMode: compiled.source.includeDashboardPolicies ? 'dashboard' : 'disabled',
|
|
456
|
+
signaturePresent: Boolean(compiled.signature && compiled.signature.value),
|
|
457
|
+
signatureKeyId: compiled.signature?.keyId || null,
|
|
426
458
|
},
|
|
427
459
|
});
|
|
428
460
|
}
|
|
@@ -444,6 +476,12 @@ function policyCommand(program) {
|
|
|
444
476
|
console.log(chalk.dim(`Fingerprint: ${compiled.fingerprint}`));
|
|
445
477
|
console.log(chalk.dim(`Deterministic rules: ${compiled.compilation.deterministicRuleCount}`));
|
|
446
478
|
console.log(chalk.dim(`Unmatched statements: ${compiled.compilation.unmatchedStatements.length}`));
|
|
479
|
+
if (compiled.signature?.value) {
|
|
480
|
+
console.log(chalk.dim(`Artifact signature: signed (${compiled.signature.keyId ? `key ${compiled.signature.keyId}` : 'inline key'})`));
|
|
481
|
+
}
|
|
482
|
+
else {
|
|
483
|
+
console.log(chalk.dim('Artifact signature: unsigned (set NEURCODE_GOVERNANCE_SIGNING_KEY to sign artifacts)'));
|
|
484
|
+
}
|
|
447
485
|
console.log(chalk.dim(`Policy source: ${compiled.source.includeDashboardPolicies ? 'dashboard + local packs' : 'local packs only'}`));
|
|
448
486
|
if (customPolicyResolution.dashboardWarning) {
|
|
449
487
|
console.log(chalk.yellow(`\n⚠️ ${customPolicyResolution.dashboardWarning}`));
|
|
@@ -453,7 +491,22 @@ function policyCommand(program) {
|
|
|
453
491
|
catch (error) {
|
|
454
492
|
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
455
493
|
if (options.json) {
|
|
456
|
-
|
|
494
|
+
const payload = { error: message };
|
|
495
|
+
if (error && typeof error === 'object') {
|
|
496
|
+
const maybeCode = error.code;
|
|
497
|
+
const maybeUnmatched = error.unmatchedStatements;
|
|
498
|
+
const maybeRuleCount = error.deterministicRuleCount;
|
|
499
|
+
if (typeof maybeCode === 'string') {
|
|
500
|
+
payload.code = maybeCode;
|
|
501
|
+
}
|
|
502
|
+
if (Array.isArray(maybeUnmatched)) {
|
|
503
|
+
payload.unmatchedStatements = maybeUnmatched.filter((item) => typeof item === 'string');
|
|
504
|
+
}
|
|
505
|
+
if (typeof maybeRuleCount === 'number' && Number.isFinite(maybeRuleCount)) {
|
|
506
|
+
payload.deterministicRuleCount = maybeRuleCount;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
457
510
|
process.exit(1);
|
|
458
511
|
}
|
|
459
512
|
console.error(chalk.red(`\n❌ ${message}\n`));
|
|
@@ -466,8 +519,61 @@ function policyCommand(program) {
|
|
|
466
519
|
governance
|
|
467
520
|
.command('status')
|
|
468
521
|
.description('Show policy governance settings for this repository')
|
|
522
|
+
.option('--org', 'Fetch centralized organization governance settings from Neurcode Cloud')
|
|
469
523
|
.option('--json', 'Output as JSON')
|
|
470
|
-
.action((options) => {
|
|
524
|
+
.action(async (options) => {
|
|
525
|
+
if (options.org) {
|
|
526
|
+
try {
|
|
527
|
+
const config = loadPolicyRuntimeConfig();
|
|
528
|
+
const client = new api_client_1.ApiClient(config);
|
|
529
|
+
const settings = await client.getOrgGovernanceSettings();
|
|
530
|
+
if (!settings) {
|
|
531
|
+
throw new Error('Organization governance settings not found');
|
|
532
|
+
}
|
|
533
|
+
const policyGovernance = settings.policyGovernance || null;
|
|
534
|
+
if (options.json) {
|
|
535
|
+
console.log(JSON.stringify({
|
|
536
|
+
source: 'org',
|
|
537
|
+
settings,
|
|
538
|
+
}, null, 2));
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
console.log(chalk.bold('\n🏢 Org Policy Governance\n'));
|
|
542
|
+
console.log(chalk.dim('Source: Neurcode Cloud (/api/v1/org/governance/settings)'));
|
|
543
|
+
if (!policyGovernance) {
|
|
544
|
+
console.log(chalk.yellow('No org-level policy governance configured.\n'));
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
console.log(chalk.dim(`Exception approvals required: ${policyGovernance.exceptionApprovals?.required ? 'yes' : 'no'}`));
|
|
548
|
+
console.log(chalk.dim(`Minimum approvals: ${policyGovernance.exceptionApprovals?.minApprovals ?? 1}`));
|
|
549
|
+
console.log(chalk.dim(`Disallow self approval: ${policyGovernance.exceptionApprovals?.disallowSelfApproval !== false ? 'yes' : 'no'}`));
|
|
550
|
+
console.log(chalk.dim(`Reason required: ${policyGovernance.exceptionApprovals?.requireReason !== false ? 'yes' : 'no'}`));
|
|
551
|
+
console.log(chalk.dim(`Minimum reason length: ${policyGovernance.exceptionApprovals?.minReasonLength ?? 12}`));
|
|
552
|
+
console.log(chalk.dim(`Maximum exception window (days): ${policyGovernance.exceptionApprovals?.maxExpiryDays ?? 30}`));
|
|
553
|
+
console.log(chalk.dim(`Allowed approvers: ${Array.isArray(policyGovernance.exceptionApprovals?.allowedApprovers)
|
|
554
|
+
&& policyGovernance.exceptionApprovals.allowedApprovers.length > 0
|
|
555
|
+
? policyGovernance.exceptionApprovals.allowedApprovers.join(', ')
|
|
556
|
+
: '(any)'}`));
|
|
557
|
+
console.log(chalk.dim(`Critical rule patterns: ${Array.isArray(policyGovernance.exceptionApprovals?.criticalRulePatterns)
|
|
558
|
+
&& policyGovernance.exceptionApprovals.criticalRulePatterns.length > 0
|
|
559
|
+
? policyGovernance.exceptionApprovals.criticalRulePatterns.join(', ')
|
|
560
|
+
: '(none)'}`));
|
|
561
|
+
console.log(chalk.dim(`Critical minimum approvals: ${policyGovernance.exceptionApprovals?.criticalMinApprovals ?? 2}`));
|
|
562
|
+
console.log(chalk.dim(`Require audit integrity: ${policyGovernance.audit?.requireIntegrity ? 'yes' : 'no'}`));
|
|
563
|
+
console.log(chalk.dim(`Updated at: ${settings.updatedAt || '(unknown)'}`));
|
|
564
|
+
console.log('');
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
catch (error) {
|
|
568
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
569
|
+
if (options.json) {
|
|
570
|
+
console.log(JSON.stringify({ error: message }, null, 2));
|
|
571
|
+
process.exit(1);
|
|
572
|
+
}
|
|
573
|
+
console.error(chalk.red(`\n❌ ${message}\n`));
|
|
574
|
+
process.exit(1);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
471
577
|
const cwd = (0, project_root_1.resolveNeurcodeProjectRoot)(process.cwd());
|
|
472
578
|
const config = (0, policy_governance_1.readPolicyGovernanceConfig)(cwd);
|
|
473
579
|
if (options.json) {
|
|
@@ -482,37 +588,155 @@ function policyCommand(program) {
|
|
|
482
588
|
console.log(chalk.dim(`Exception approvals required: ${config.exceptionApprovals.required ? 'yes' : 'no'}`));
|
|
483
589
|
console.log(chalk.dim(`Minimum approvals: ${config.exceptionApprovals.minApprovals}`));
|
|
484
590
|
console.log(chalk.dim(`Disallow self approval: ${config.exceptionApprovals.disallowSelfApproval ? 'yes' : 'no'}`));
|
|
591
|
+
console.log(chalk.dim(`Reason required: ${config.exceptionApprovals.requireReason ? 'yes' : 'no'}`));
|
|
592
|
+
console.log(chalk.dim(`Minimum reason length: ${config.exceptionApprovals.minReasonLength}`));
|
|
593
|
+
console.log(chalk.dim(`Maximum exception window (days): ${config.exceptionApprovals.maxExpiryDays}`));
|
|
485
594
|
console.log(chalk.dim(`Allowed approvers: ${config.exceptionApprovals.allowedApprovers.length > 0 ? config.exceptionApprovals.allowedApprovers.join(', ') : '(any)'}`));
|
|
595
|
+
console.log(chalk.dim(`Critical rule patterns: ${config.exceptionApprovals.criticalRulePatterns.length > 0 ? config.exceptionApprovals.criticalRulePatterns.join(', ') : '(none)'}`));
|
|
596
|
+
console.log(chalk.dim(`Critical minimum approvals: ${config.exceptionApprovals.criticalMinApprovals}`));
|
|
486
597
|
console.log(chalk.dim(`Require audit integrity: ${config.audit.requireIntegrity ? 'yes' : 'no'}`));
|
|
487
598
|
console.log('');
|
|
488
599
|
});
|
|
489
600
|
governance
|
|
490
601
|
.command('set')
|
|
491
602
|
.description('Update policy governance settings')
|
|
603
|
+
.option('--org', 'Update centralized organization governance settings in Neurcode Cloud')
|
|
492
604
|
.option('--require-approval', 'Require approvals before exceptions become effective')
|
|
493
605
|
.option('--no-require-approval', 'Do not require approvals for exceptions')
|
|
494
606
|
.option('--min-approvals <n>', 'Minimum approvals required when approval mode is enabled', (value) => parseInt(value, 10))
|
|
495
607
|
.option('--allow-self-approval', 'Allow requester to approve their own exception')
|
|
496
608
|
.option('--restrict-approvers <csv>', 'Comma-separated allow-list of approver identities')
|
|
497
609
|
.option('--clear-approvers', 'Clear approver allow-list (allow any approver)')
|
|
610
|
+
.option('--require-reason', 'Require non-trivial exception reason text')
|
|
611
|
+
.option('--no-require-reason', 'Do not enforce minimum reason text length')
|
|
612
|
+
.option('--min-reason-length <n>', 'Minimum exception reason length (default: 12)', (value) => parseInt(value, 10))
|
|
613
|
+
.option('--max-expiry-days <n>', 'Maximum exception expiry window in days (default: 30)', (value) => parseInt(value, 10))
|
|
614
|
+
.option('--critical-rules <csv>', 'Comma-separated critical rule patterns requiring elevated approvals')
|
|
615
|
+
.option('--clear-critical-rules', 'Clear critical rule patterns')
|
|
616
|
+
.option('--critical-min-approvals <n>', 'Minimum approvals for critical rule exceptions (default: 2)', (value) => parseInt(value, 10))
|
|
498
617
|
.option('--require-audit-integrity', 'Fail verify if policy audit chain integrity is broken')
|
|
499
618
|
.option('--no-require-audit-integrity', 'Do not enforce policy audit integrity in verify')
|
|
500
619
|
.option('--json', 'Output as JSON')
|
|
501
|
-
.action((options) => {
|
|
620
|
+
.action(async (options) => {
|
|
621
|
+
const hasRestrictApprovers = typeof options.restrictApprovers === 'string';
|
|
622
|
+
const hasCriticalRules = typeof options.criticalRules === 'string';
|
|
623
|
+
const parsedApprovers = options.clearApprovers
|
|
624
|
+
? []
|
|
625
|
+
: hasRestrictApprovers
|
|
626
|
+
? options.restrictApprovers
|
|
627
|
+
.split(',')
|
|
628
|
+
.map((item) => item.trim())
|
|
629
|
+
.filter(Boolean)
|
|
630
|
+
: undefined;
|
|
631
|
+
const parsedCriticalRules = options.clearCriticalRules
|
|
632
|
+
? []
|
|
633
|
+
: hasCriticalRules
|
|
634
|
+
? options.criticalRules
|
|
635
|
+
.split(',')
|
|
636
|
+
.map((item) => item.trim())
|
|
637
|
+
.filter(Boolean)
|
|
638
|
+
: undefined;
|
|
639
|
+
if (options.org) {
|
|
640
|
+
try {
|
|
641
|
+
const exceptionApprovalsPatch = {};
|
|
642
|
+
if (typeof options.requireApproval === 'boolean') {
|
|
643
|
+
exceptionApprovalsPatch.required = options.requireApproval;
|
|
644
|
+
}
|
|
645
|
+
if (Number.isFinite(options.minApprovals)) {
|
|
646
|
+
exceptionApprovalsPatch.minApprovals = options.minApprovals;
|
|
647
|
+
}
|
|
648
|
+
if (typeof options.allowSelfApproval === 'boolean') {
|
|
649
|
+
exceptionApprovalsPatch.disallowSelfApproval = !options.allowSelfApproval;
|
|
650
|
+
}
|
|
651
|
+
if (parsedApprovers) {
|
|
652
|
+
exceptionApprovalsPatch.allowedApprovers = parsedApprovers;
|
|
653
|
+
}
|
|
654
|
+
if (typeof options.requireReason === 'boolean') {
|
|
655
|
+
exceptionApprovalsPatch.requireReason = options.requireReason;
|
|
656
|
+
}
|
|
657
|
+
if (Number.isFinite(options.minReasonLength)) {
|
|
658
|
+
exceptionApprovalsPatch.minReasonLength = options.minReasonLength;
|
|
659
|
+
}
|
|
660
|
+
if (Number.isFinite(options.maxExpiryDays)) {
|
|
661
|
+
exceptionApprovalsPatch.maxExpiryDays = options.maxExpiryDays;
|
|
662
|
+
}
|
|
663
|
+
if (parsedCriticalRules) {
|
|
664
|
+
exceptionApprovalsPatch.criticalRulePatterns = parsedCriticalRules;
|
|
665
|
+
}
|
|
666
|
+
if (Number.isFinite(options.criticalMinApprovals)) {
|
|
667
|
+
exceptionApprovalsPatch.criticalMinApprovals = options.criticalMinApprovals;
|
|
668
|
+
}
|
|
669
|
+
const auditPatch = {};
|
|
670
|
+
if (typeof options.requireAuditIntegrity === 'boolean') {
|
|
671
|
+
auditPatch.requireIntegrity = options.requireAuditIntegrity;
|
|
672
|
+
}
|
|
673
|
+
const policyGovernancePatch = {};
|
|
674
|
+
if (Object.keys(exceptionApprovalsPatch).length > 0) {
|
|
675
|
+
policyGovernancePatch.exceptionApprovals = exceptionApprovalsPatch;
|
|
676
|
+
}
|
|
677
|
+
if (Object.keys(auditPatch).length > 0) {
|
|
678
|
+
policyGovernancePatch.audit = auditPatch;
|
|
679
|
+
}
|
|
680
|
+
const config = loadPolicyRuntimeConfig();
|
|
681
|
+
const client = new api_client_1.ApiClient(config);
|
|
682
|
+
const settings = await client.updateOrgGovernanceSettings({
|
|
683
|
+
policyGovernance: policyGovernancePatch,
|
|
684
|
+
});
|
|
685
|
+
if (!settings) {
|
|
686
|
+
throw new Error('Failed to update org governance settings');
|
|
687
|
+
}
|
|
688
|
+
const next = settings.policyGovernance;
|
|
689
|
+
if (options.json) {
|
|
690
|
+
console.log(JSON.stringify({
|
|
691
|
+
source: 'org',
|
|
692
|
+
settings,
|
|
693
|
+
}, null, 2));
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
console.log(chalk.green('\n✅ Organization policy governance updated.\n'));
|
|
697
|
+
console.log(chalk.dim('Source: Neurcode Cloud (/api/v1/org/governance/settings)'));
|
|
698
|
+
if (!next) {
|
|
699
|
+
console.log(chalk.yellow('No org-level policy governance payload returned.\n'));
|
|
700
|
+
return;
|
|
701
|
+
}
|
|
702
|
+
console.log(chalk.dim(`Approval required: ${next.exceptionApprovals?.required ? 'yes' : 'no'}`));
|
|
703
|
+
console.log(chalk.dim(`Min approvals: ${next.exceptionApprovals?.minApprovals ?? 1}`));
|
|
704
|
+
console.log(chalk.dim(`Disallow self approval: ${next.exceptionApprovals?.disallowSelfApproval !== false ? 'yes' : 'no'}`));
|
|
705
|
+
console.log(chalk.dim(`Reason required: ${next.exceptionApprovals?.requireReason !== false ? 'yes' : 'no'}`));
|
|
706
|
+
console.log(chalk.dim(`Min reason length: ${next.exceptionApprovals?.minReasonLength ?? 12}`));
|
|
707
|
+
console.log(chalk.dim(`Max expiry days: ${next.exceptionApprovals?.maxExpiryDays ?? 30}`));
|
|
708
|
+
console.log(chalk.dim(`Critical min approvals: ${next.exceptionApprovals?.criticalMinApprovals ?? 2}`));
|
|
709
|
+
console.log(chalk.dim(`Critical rule patterns: ${Array.isArray(next.exceptionApprovals?.criticalRulePatterns)
|
|
710
|
+
&& next.exceptionApprovals.criticalRulePatterns.length > 0
|
|
711
|
+
? next.exceptionApprovals.criticalRulePatterns.join(', ')
|
|
712
|
+
: '(none)'}`));
|
|
713
|
+
console.log(chalk.dim(`Require audit integrity: ${next.audit?.requireIntegrity ? 'yes' : 'no'}`));
|
|
714
|
+
console.log(chalk.dim(`Updated at: ${settings.updatedAt || '(unknown)'}`));
|
|
715
|
+
console.log('');
|
|
716
|
+
return;
|
|
717
|
+
}
|
|
718
|
+
catch (error) {
|
|
719
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
720
|
+
if (options.json) {
|
|
721
|
+
console.log(JSON.stringify({ error: message }, null, 2));
|
|
722
|
+
process.exit(1);
|
|
723
|
+
}
|
|
724
|
+
console.error(chalk.red(`\n❌ ${message}\n`));
|
|
725
|
+
process.exit(1);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
502
728
|
const cwd = (0, project_root_1.resolveNeurcodeProjectRoot)(process.cwd());
|
|
503
729
|
try {
|
|
504
730
|
const next = (0, policy_governance_1.updatePolicyGovernanceConfig)(cwd, {
|
|
505
731
|
required: typeof options.requireApproval === 'boolean' ? options.requireApproval : undefined,
|
|
506
732
|
minApprovals: Number.isFinite(options.minApprovals) ? options.minApprovals : undefined,
|
|
507
733
|
disallowSelfApproval: typeof options.allowSelfApproval === 'boolean' ? !options.allowSelfApproval : undefined,
|
|
508
|
-
allowedApprovers:
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
.filter(Boolean)
|
|
515
|
-
: undefined,
|
|
734
|
+
allowedApprovers: parsedApprovers,
|
|
735
|
+
requireReason: typeof options.requireReason === 'boolean' ? options.requireReason : undefined,
|
|
736
|
+
minReasonLength: Number.isFinite(options.minReasonLength) ? options.minReasonLength : undefined,
|
|
737
|
+
maxExpiryDays: Number.isFinite(options.maxExpiryDays) ? options.maxExpiryDays : undefined,
|
|
738
|
+
criticalRulePatterns: parsedCriticalRules,
|
|
739
|
+
criticalMinApprovals: Number.isFinite(options.criticalMinApprovals) ? options.criticalMinApprovals : undefined,
|
|
516
740
|
requireAuditIntegrity: typeof options.requireAuditIntegrity === 'boolean' ? options.requireAuditIntegrity : undefined,
|
|
517
741
|
});
|
|
518
742
|
try {
|
|
@@ -526,6 +750,11 @@ function policyCommand(program) {
|
|
|
526
750
|
minApprovals: next.exceptionApprovals.minApprovals,
|
|
527
751
|
disallowSelfApproval: next.exceptionApprovals.disallowSelfApproval,
|
|
528
752
|
allowedApprovers: next.exceptionApprovals.allowedApprovers,
|
|
753
|
+
requireReason: next.exceptionApprovals.requireReason,
|
|
754
|
+
minReasonLength: next.exceptionApprovals.minReasonLength,
|
|
755
|
+
maxExpiryDays: next.exceptionApprovals.maxExpiryDays,
|
|
756
|
+
criticalRulePatterns: next.exceptionApprovals.criticalRulePatterns,
|
|
757
|
+
criticalMinApprovals: next.exceptionApprovals.criticalMinApprovals,
|
|
529
758
|
requireAuditIntegrity: next.audit.requireIntegrity,
|
|
530
759
|
},
|
|
531
760
|
});
|
|
@@ -542,6 +771,11 @@ function policyCommand(program) {
|
|
|
542
771
|
console.log(chalk.dim(`Approval required: ${next.exceptionApprovals.required ? 'yes' : 'no'}`));
|
|
543
772
|
console.log(chalk.dim(`Min approvals: ${next.exceptionApprovals.minApprovals}`));
|
|
544
773
|
console.log(chalk.dim(`Disallow self approval: ${next.exceptionApprovals.disallowSelfApproval ? 'yes' : 'no'}`));
|
|
774
|
+
console.log(chalk.dim(`Reason required: ${next.exceptionApprovals.requireReason ? 'yes' : 'no'}`));
|
|
775
|
+
console.log(chalk.dim(`Min reason length: ${next.exceptionApprovals.minReasonLength}`));
|
|
776
|
+
console.log(chalk.dim(`Max expiry days: ${next.exceptionApprovals.maxExpiryDays}`));
|
|
777
|
+
console.log(chalk.dim(`Critical min approvals: ${next.exceptionApprovals.criticalMinApprovals}`));
|
|
778
|
+
console.log(chalk.dim(`Critical rule patterns: ${next.exceptionApprovals.criticalRulePatterns.length > 0 ? next.exceptionApprovals.criticalRulePatterns.join(', ') : '(none)'}`));
|
|
545
779
|
console.log(chalk.dim(`Require audit integrity: ${next.audit.requireIntegrity ? 'yes' : 'no'}`));
|
|
546
780
|
console.log(chalk.dim('Commit governance + audit files so CI can enforce approval and integrity rules.'));
|
|
547
781
|
console.log('');
|
|
@@ -592,9 +826,59 @@ function policyCommand(program) {
|
|
|
592
826
|
exception
|
|
593
827
|
.command('list')
|
|
594
828
|
.description('List policy exceptions for this repository')
|
|
829
|
+
.option('--org', 'List centralized organization policy exceptions from Neurcode Cloud')
|
|
595
830
|
.option('--all', 'Include inactive/expired exceptions')
|
|
596
831
|
.option('--json', 'Output as JSON')
|
|
597
|
-
.action((options) => {
|
|
832
|
+
.action(async (options) => {
|
|
833
|
+
if (options.org) {
|
|
834
|
+
try {
|
|
835
|
+
const config = loadPolicyRuntimeConfig();
|
|
836
|
+
const client = new api_client_1.ApiClient(config);
|
|
837
|
+
const exceptions = await client.listOrgPolicyExceptions({ limit: 250 });
|
|
838
|
+
const items = options.all
|
|
839
|
+
? exceptions
|
|
840
|
+
: exceptions.filter((entry) => entry.effectiveState !== 'revoked' && entry.effectiveState !== 'expired');
|
|
841
|
+
if (options.json) {
|
|
842
|
+
console.log(JSON.stringify({
|
|
843
|
+
source: 'org',
|
|
844
|
+
total: exceptions.length,
|
|
845
|
+
exceptions: items,
|
|
846
|
+
}, null, 2));
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
if (items.length === 0) {
|
|
850
|
+
console.log(chalk.yellow('\n⚠️ No organization policy exceptions found.\n'));
|
|
851
|
+
console.log(chalk.dim('Add one: neurcode policy exception add --org --rule <pattern> --file <glob> --reason "<why>"\n'));
|
|
852
|
+
return;
|
|
853
|
+
}
|
|
854
|
+
console.log(chalk.bold('\n🏢 Org Policy Exceptions\n'));
|
|
855
|
+
items.forEach((entry) => {
|
|
856
|
+
console.log(chalk.cyan(`• ${entry.id}`));
|
|
857
|
+
console.log(chalk.dim(` state=${entry.effectiveState} workflow=${entry.workflowState}`));
|
|
858
|
+
console.log(chalk.dim(` rule=${entry.rulePattern} file=${entry.filePattern}`));
|
|
859
|
+
console.log(chalk.dim(` expires=${entry.expiresAt} active=${entry.active ? 'yes' : 'no'}`));
|
|
860
|
+
console.log(chalk.dim(` approvals=${entry.approvalCount}` +
|
|
861
|
+
`${entry.requiredApprovals > 0 ? ` required=${entry.requiredApprovals}` : ''}` +
|
|
862
|
+
`${entry.critical ? ' critical=yes' : ''}`));
|
|
863
|
+
console.log(chalk.dim(` reason=${entry.reason}`));
|
|
864
|
+
if (entry.ticket) {
|
|
865
|
+
console.log(chalk.dim(` ticket=${entry.ticket}`));
|
|
866
|
+
}
|
|
867
|
+
console.log('');
|
|
868
|
+
});
|
|
869
|
+
console.log(chalk.dim('Source: Neurcode Cloud (/api/v1/org/policy-exceptions)\n'));
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
catch (error) {
|
|
873
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
874
|
+
if (options.json) {
|
|
875
|
+
console.log(JSON.stringify({ error: message }, null, 2));
|
|
876
|
+
process.exit(1);
|
|
877
|
+
}
|
|
878
|
+
console.error(chalk.red(`\n❌ ${message}\n`));
|
|
879
|
+
process.exit(1);
|
|
880
|
+
}
|
|
881
|
+
}
|
|
598
882
|
const cwd = (0, project_root_1.resolveNeurcodeProjectRoot)(process.cwd());
|
|
599
883
|
const data = (0, policy_exceptions_1.listPolicyExceptions)(cwd);
|
|
600
884
|
const governance = (0, policy_governance_1.readPolicyGovernanceConfig)(cwd);
|
|
@@ -609,17 +893,27 @@ function policyCommand(program) {
|
|
|
609
893
|
if (allowedApprovers.size > 0) {
|
|
610
894
|
effectiveApprovals = effectiveApprovals.filter((item) => allowedApprovers.has(item.approver.toLowerCase()));
|
|
611
895
|
}
|
|
896
|
+
const requiredApprovalResolution = (0, policy_governance_1.resolveRequiredApprovalsForRule)(entry.rulePattern, governance);
|
|
897
|
+
const requiredApprovals = governance.exceptionApprovals.required
|
|
898
|
+
? requiredApprovalResolution.requiredApprovals
|
|
899
|
+
: 0;
|
|
900
|
+
const reasonValid = !governance.exceptionApprovals.requireReason
|
|
901
|
+
|| (entry.reason || '').trim().length >= governance.exceptionApprovals.minReasonLength;
|
|
612
902
|
const status = !entry.active || !unexpired
|
|
613
903
|
? 'inactive'
|
|
614
|
-
: !
|
|
615
|
-
? '
|
|
616
|
-
:
|
|
617
|
-
? '
|
|
618
|
-
:
|
|
904
|
+
: !reasonValid
|
|
905
|
+
? 'invalid_reason'
|
|
906
|
+
: !governance.exceptionApprovals.required
|
|
907
|
+
? 'active'
|
|
908
|
+
: effectiveApprovals.length >= requiredApprovals
|
|
909
|
+
? 'approved'
|
|
910
|
+
: 'pending';
|
|
619
911
|
return {
|
|
620
912
|
...entry,
|
|
621
913
|
status,
|
|
622
914
|
effectiveApprovals: effectiveApprovals.length,
|
|
915
|
+
requiredApprovals,
|
|
916
|
+
criticalRule: requiredApprovalResolution.critical,
|
|
623
917
|
};
|
|
624
918
|
});
|
|
625
919
|
const items = options.all ? withStatus : withStatus.filter((entry) => entry.status !== 'inactive');
|
|
@@ -645,7 +939,10 @@ function policyCommand(program) {
|
|
|
645
939
|
console.log(chalk.dim(` status=${entry.status || (entry.active ? 'active' : 'inactive')}`));
|
|
646
940
|
console.log(chalk.dim(` rule=${entry.rulePattern} file=${entry.filePattern}`));
|
|
647
941
|
console.log(chalk.dim(` expires=${entry.expiresAt} active=${entry.active ? 'yes' : 'no'}`));
|
|
648
|
-
console.log(chalk.dim(` approvals=${entry.approvals.length}
|
|
942
|
+
console.log(chalk.dim(` approvals=${entry.approvals.length}` +
|
|
943
|
+
`${typeof entry.effectiveApprovals === 'number' ? ` (effective=${entry.effectiveApprovals})` : ''}` +
|
|
944
|
+
`${typeof entry.requiredApprovals === 'number' && entry.requiredApprovals > 0 ? ` required=${entry.requiredApprovals}` : ''}` +
|
|
945
|
+
`${entry.criticalRule ? ' critical=yes' : ''}`));
|
|
649
946
|
console.log(chalk.dim(` reason=${entry.reason}`));
|
|
650
947
|
console.log(chalk.dim(` requestedBy=${entry.requestedBy || entry.createdBy || 'unknown'}`));
|
|
651
948
|
if (entry.ticket) {
|
|
@@ -661,6 +958,7 @@ function policyCommand(program) {
|
|
|
661
958
|
exception
|
|
662
959
|
.command('add')
|
|
663
960
|
.description('Add a policy exception entry')
|
|
961
|
+
.option('--org', 'Create centralized organization policy exception in Neurcode Cloud')
|
|
664
962
|
.requiredOption('--rule <pattern>', 'Rule pattern (exact, wildcard, or /regex/)')
|
|
665
963
|
.requiredOption('--file <pattern>', 'File pattern (exact, wildcard, or /regex/)')
|
|
666
964
|
.requiredOption('--reason <text>', 'Business justification for this exception')
|
|
@@ -669,7 +967,7 @@ function policyCommand(program) {
|
|
|
669
967
|
.option('--expires-at <iso>', 'Expiry timestamp in ISO-8601')
|
|
670
968
|
.option('--expires-in-days <n>', 'Expiry offset in days (default: 30)', (value) => parseInt(value, 10))
|
|
671
969
|
.option('--json', 'Output as JSON')
|
|
672
|
-
.action((options) => {
|
|
970
|
+
.action(async (options) => {
|
|
673
971
|
const cwd = (0, project_root_1.resolveNeurcodeProjectRoot)(process.cwd());
|
|
674
972
|
try {
|
|
675
973
|
const severity = options.severity === 'allow' || options.severity === 'warn' || options.severity === 'block'
|
|
@@ -682,6 +980,43 @@ function policyCommand(program) {
|
|
|
682
980
|
expiresAt: options.expiresAt,
|
|
683
981
|
expiresInDays: options.expiresInDays,
|
|
684
982
|
});
|
|
983
|
+
if (options.org) {
|
|
984
|
+
const config = loadPolicyRuntimeConfig();
|
|
985
|
+
const client = new api_client_1.ApiClient(config);
|
|
986
|
+
const created = await client.createOrgPolicyException({
|
|
987
|
+
rulePattern: options.rule,
|
|
988
|
+
filePattern: options.file,
|
|
989
|
+
reason: options.reason,
|
|
990
|
+
ticket: options.ticket,
|
|
991
|
+
severity,
|
|
992
|
+
expiresAt,
|
|
993
|
+
});
|
|
994
|
+
if (options.json) {
|
|
995
|
+
console.log(JSON.stringify({
|
|
996
|
+
source: 'org',
|
|
997
|
+
exception: created,
|
|
998
|
+
}, null, 2));
|
|
999
|
+
return;
|
|
1000
|
+
}
|
|
1001
|
+
console.log(chalk.green('\n✅ Organization policy exception created\n'));
|
|
1002
|
+
console.log(chalk.cyan(`ID: ${created.id}`));
|
|
1003
|
+
console.log(chalk.dim(`State: ${created.effectiveState}`));
|
|
1004
|
+
console.log(chalk.dim(`Rule: ${created.rulePattern}`));
|
|
1005
|
+
console.log(chalk.dim(`File: ${created.filePattern}`));
|
|
1006
|
+
console.log(chalk.dim(`Expires: ${created.expiresAt}`));
|
|
1007
|
+
console.log(chalk.dim(`Approvals: ${created.approvalCount}/${created.requiredApprovals}`));
|
|
1008
|
+
if (created.ticket) {
|
|
1009
|
+
console.log(chalk.dim(`Ticket: ${created.ticket}`));
|
|
1010
|
+
}
|
|
1011
|
+
console.log(chalk.dim('Source: Neurcode Cloud (/api/v1/org/policy-exceptions)\n'));
|
|
1012
|
+
return;
|
|
1013
|
+
}
|
|
1014
|
+
const governance = (0, policy_governance_1.readPolicyGovernanceConfig)(cwd);
|
|
1015
|
+
if (governance.exceptionApprovals.requireReason
|
|
1016
|
+
&& options.reason.trim().length < governance.exceptionApprovals.minReasonLength) {
|
|
1017
|
+
throw new Error(`reason must be at least ${governance.exceptionApprovals.minReasonLength} characters (governance policy)`);
|
|
1018
|
+
}
|
|
1019
|
+
validateExceptionWindowByGovernance(expiresAt, governance.exceptionApprovals.maxExpiryDays);
|
|
685
1020
|
const createdBy = resolveActor();
|
|
686
1021
|
const created = (0, policy_exceptions_1.addPolicyException)(cwd, {
|
|
687
1022
|
rulePattern: options.rule,
|
|
@@ -693,7 +1028,7 @@ function policyCommand(program) {
|
|
|
693
1028
|
createdBy,
|
|
694
1029
|
requestedBy: createdBy,
|
|
695
1030
|
});
|
|
696
|
-
const
|
|
1031
|
+
const approvalResolution = (0, policy_governance_1.resolveRequiredApprovalsForRule)(created.rulePattern, governance);
|
|
697
1032
|
try {
|
|
698
1033
|
(0, policy_audit_1.appendPolicyAuditEvent)(cwd, {
|
|
699
1034
|
actor: createdBy,
|
|
@@ -705,6 +1040,10 @@ function policyCommand(program) {
|
|
|
705
1040
|
filePattern: created.filePattern,
|
|
706
1041
|
expiresAt: created.expiresAt,
|
|
707
1042
|
requireApproval: governance.exceptionApprovals.required,
|
|
1043
|
+
requiredApprovals: governance.exceptionApprovals.required
|
|
1044
|
+
? approvalResolution.requiredApprovals
|
|
1045
|
+
: 0,
|
|
1046
|
+
criticalRule: approvalResolution.critical,
|
|
708
1047
|
},
|
|
709
1048
|
});
|
|
710
1049
|
}
|
|
@@ -716,6 +1055,10 @@ function policyCommand(program) {
|
|
|
716
1055
|
created,
|
|
717
1056
|
path: (0, policy_exceptions_1.getPolicyExceptionsPath)(cwd),
|
|
718
1057
|
requiresApproval: governance.exceptionApprovals.required,
|
|
1058
|
+
requiredApprovals: governance.exceptionApprovals.required
|
|
1059
|
+
? approvalResolution.requiredApprovals
|
|
1060
|
+
: 0,
|
|
1061
|
+
criticalRule: approvalResolution.critical,
|
|
719
1062
|
}, null, 2));
|
|
720
1063
|
return;
|
|
721
1064
|
}
|
|
@@ -726,7 +1069,8 @@ function policyCommand(program) {
|
|
|
726
1069
|
console.log(chalk.dim(`Expires: ${created.expiresAt}`));
|
|
727
1070
|
console.log(chalk.dim(`Reason: ${created.reason}`));
|
|
728
1071
|
if (governance.exceptionApprovals.required) {
|
|
729
|
-
|
|
1072
|
+
const requiredApprovals = approvalResolution.requiredApprovals;
|
|
1073
|
+
console.log(chalk.yellow(`Approval required: ${requiredApprovals} approver(s) before this exception is active${approvalResolution.critical ? ' (critical rule gate)' : ''}.`));
|
|
730
1074
|
}
|
|
731
1075
|
if (created.ticket) {
|
|
732
1076
|
console.log(chalk.dim(`Ticket: ${created.ticket}`));
|
|
@@ -747,13 +1091,57 @@ function policyCommand(program) {
|
|
|
747
1091
|
.command('approve')
|
|
748
1092
|
.description('Approve a policy exception by ID')
|
|
749
1093
|
.argument('<id>', 'Exception ID to approve')
|
|
1094
|
+
.option('--org', 'Approve centralized organization policy exception in Neurcode Cloud')
|
|
750
1095
|
.option('--by <actor>', 'Approver identity (defaults to NEURCODE_ACTOR/GITHUB_ACTOR/USER)')
|
|
751
1096
|
.option('--comment <text>', 'Approval comment')
|
|
752
1097
|
.option('--json', 'Output as JSON')
|
|
753
|
-
.action((id, options) => {
|
|
1098
|
+
.action(async (id, options) => {
|
|
754
1099
|
const cwd = (0, project_root_1.resolveNeurcodeProjectRoot)(process.cwd());
|
|
755
1100
|
const approver = resolveActor(options.by);
|
|
756
1101
|
try {
|
|
1102
|
+
if (options.org) {
|
|
1103
|
+
if (options.by) {
|
|
1104
|
+
throw new Error('--by is not supported with --org (identity comes from authenticated Neurcode user)');
|
|
1105
|
+
}
|
|
1106
|
+
const config = loadPolicyRuntimeConfig();
|
|
1107
|
+
const client = new api_client_1.ApiClient(config);
|
|
1108
|
+
const updated = await client.approveOrgPolicyException(String(id).trim(), {
|
|
1109
|
+
note: options.comment,
|
|
1110
|
+
});
|
|
1111
|
+
if (options.json) {
|
|
1112
|
+
console.log(JSON.stringify({
|
|
1113
|
+
source: 'org',
|
|
1114
|
+
approved: true,
|
|
1115
|
+
exception: updated,
|
|
1116
|
+
}, null, 2));
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
console.log(chalk.green('\n✅ Organization policy exception approval recorded.\n'));
|
|
1120
|
+
console.log(chalk.dim(`ID: ${updated.id}`));
|
|
1121
|
+
console.log(chalk.dim(`State: ${updated.effectiveState}`));
|
|
1122
|
+
console.log(chalk.dim(`Approvals: ${updated.approvalCount}/${updated.requiredApprovals}`));
|
|
1123
|
+
console.log(chalk.dim('Source: Neurcode Cloud (/api/v1/org/policy-exceptions)\n'));
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
const governance = (0, policy_governance_1.readPolicyGovernanceConfig)(cwd);
|
|
1127
|
+
const target = (0, policy_exceptions_1.listPolicyExceptions)(cwd).all.find((entry) => entry.id === String(id).trim());
|
|
1128
|
+
if (!target) {
|
|
1129
|
+
if (options.json) {
|
|
1130
|
+
console.log(JSON.stringify({ error: 'Exception not found' }, null, 2));
|
|
1131
|
+
process.exit(1);
|
|
1132
|
+
}
|
|
1133
|
+
console.log(chalk.yellow('\n⚠️ Exception not found.\n'));
|
|
1134
|
+
process.exit(1);
|
|
1135
|
+
}
|
|
1136
|
+
const normalizedApprover = approver.toLowerCase();
|
|
1137
|
+
const requestedBy = (target.requestedBy || target.createdBy || '').toLowerCase();
|
|
1138
|
+
if (governance.exceptionApprovals.disallowSelfApproval && requestedBy && normalizedApprover === requestedBy) {
|
|
1139
|
+
throw new Error('self-approval is disallowed by governance policy');
|
|
1140
|
+
}
|
|
1141
|
+
if (governance.exceptionApprovals.allowedApprovers.length > 0
|
|
1142
|
+
&& !governance.exceptionApprovals.allowedApprovers.map((item) => item.toLowerCase()).includes(normalizedApprover)) {
|
|
1143
|
+
throw new Error('approver is not in governance allow-list');
|
|
1144
|
+
}
|
|
757
1145
|
const updated = (0, policy_exceptions_1.approvePolicyException)(cwd, String(id).trim(), {
|
|
758
1146
|
approver,
|
|
759
1147
|
comment: options.comment,
|
|
@@ -766,6 +1154,19 @@ function policyCommand(program) {
|
|
|
766
1154
|
console.log(chalk.yellow('\n⚠️ Exception not found.\n'));
|
|
767
1155
|
process.exit(1);
|
|
768
1156
|
}
|
|
1157
|
+
const requiredApprovalResolution = (0, policy_governance_1.resolveRequiredApprovalsForRule)(updated.rulePattern, governance);
|
|
1158
|
+
const acceptedApprovals = updated.approvals.filter((item) => {
|
|
1159
|
+
const actor = item.approver.toLowerCase();
|
|
1160
|
+
if (governance.exceptionApprovals.allowedApprovers.length > 0
|
|
1161
|
+
&& !governance.exceptionApprovals.allowedApprovers.map((entry) => entry.toLowerCase()).includes(actor)) {
|
|
1162
|
+
return false;
|
|
1163
|
+
}
|
|
1164
|
+
if (governance.exceptionApprovals.disallowSelfApproval && requestedBy && actor === requestedBy) {
|
|
1165
|
+
return false;
|
|
1166
|
+
}
|
|
1167
|
+
return true;
|
|
1168
|
+
});
|
|
1169
|
+
const effectiveApprovals = acceptedApprovals.length;
|
|
769
1170
|
try {
|
|
770
1171
|
(0, policy_audit_1.appendPolicyAuditEvent)(cwd, {
|
|
771
1172
|
actor: approver,
|
|
@@ -775,6 +1176,9 @@ function policyCommand(program) {
|
|
|
775
1176
|
metadata: {
|
|
776
1177
|
comment: options.comment || null,
|
|
777
1178
|
approvals: updated.approvals.length,
|
|
1179
|
+
effectiveApprovals,
|
|
1180
|
+
requiredApprovals: requiredApprovalResolution.requiredApprovals,
|
|
1181
|
+
criticalRule: requiredApprovalResolution.critical,
|
|
778
1182
|
},
|
|
779
1183
|
});
|
|
780
1184
|
}
|
|
@@ -785,12 +1189,20 @@ function policyCommand(program) {
|
|
|
785
1189
|
console.log(JSON.stringify({
|
|
786
1190
|
approved: true,
|
|
787
1191
|
exception: updated,
|
|
1192
|
+
effectiveApprovals,
|
|
1193
|
+
requiredApprovals: requiredApprovalResolution.requiredApprovals,
|
|
1194
|
+
approvalSatisfied: !governance.exceptionApprovals.required
|
|
1195
|
+
|| effectiveApprovals >= requiredApprovalResolution.requiredApprovals,
|
|
1196
|
+
criticalRule: requiredApprovalResolution.critical,
|
|
788
1197
|
}, null, 2));
|
|
789
1198
|
return;
|
|
790
1199
|
}
|
|
791
1200
|
console.log(chalk.green('\n✅ Policy exception approval recorded.\n'));
|
|
792
1201
|
console.log(chalk.dim(`ID: ${updated.id}`));
|
|
793
|
-
console.log(chalk.dim(`Approvals: ${updated.approvals.length}`));
|
|
1202
|
+
console.log(chalk.dim(`Approvals: ${updated.approvals.length} (effective=${effectiveApprovals})`));
|
|
1203
|
+
if (governance.exceptionApprovals.required) {
|
|
1204
|
+
console.log(chalk.dim(`Required approvals: ${requiredApprovalResolution.requiredApprovals}${requiredApprovalResolution.critical ? ' (critical rule gate)' : ''}`));
|
|
1205
|
+
}
|
|
794
1206
|
console.log(chalk.dim(`Approver: ${approver}`));
|
|
795
1207
|
console.log('');
|
|
796
1208
|
}
|
|
@@ -804,12 +1216,185 @@ function policyCommand(program) {
|
|
|
804
1216
|
process.exit(1);
|
|
805
1217
|
}
|
|
806
1218
|
});
|
|
1219
|
+
exception
|
|
1220
|
+
.command('reject')
|
|
1221
|
+
.description('Reject a pending organization policy exception by ID')
|
|
1222
|
+
.argument('<id>', 'Exception ID to reject')
|
|
1223
|
+
.requiredOption('--reason <text>', 'Reason for rejecting this exception')
|
|
1224
|
+
.option('--org', 'Reject centralized organization policy exception in Neurcode Cloud')
|
|
1225
|
+
.option('--json', 'Output as JSON')
|
|
1226
|
+
.action(async (id, options) => {
|
|
1227
|
+
if (!options.org) {
|
|
1228
|
+
const message = '`policy exception reject` is only supported for --org exceptions. Use `policy exception remove` for local exceptions.';
|
|
1229
|
+
if (options.json) {
|
|
1230
|
+
console.log(JSON.stringify({ error: message }, null, 2));
|
|
1231
|
+
process.exit(1);
|
|
1232
|
+
}
|
|
1233
|
+
console.error(chalk.red(`\n❌ ${message}\n`));
|
|
1234
|
+
process.exit(1);
|
|
1235
|
+
}
|
|
1236
|
+
const reason = typeof options.reason === 'string' ? options.reason.trim() : '';
|
|
1237
|
+
if (!reason) {
|
|
1238
|
+
const message = '--reason is required';
|
|
1239
|
+
if (options.json) {
|
|
1240
|
+
console.log(JSON.stringify({ error: message }, null, 2));
|
|
1241
|
+
process.exit(1);
|
|
1242
|
+
}
|
|
1243
|
+
console.error(chalk.red(`\n❌ ${message}\n`));
|
|
1244
|
+
process.exit(1);
|
|
1245
|
+
}
|
|
1246
|
+
try {
|
|
1247
|
+
const config = loadPolicyRuntimeConfig();
|
|
1248
|
+
const client = new api_client_1.ApiClient(config);
|
|
1249
|
+
const updated = await client.rejectOrgPolicyException(String(id).trim(), { reason });
|
|
1250
|
+
if (options.json) {
|
|
1251
|
+
console.log(JSON.stringify({
|
|
1252
|
+
source: 'org',
|
|
1253
|
+
rejected: true,
|
|
1254
|
+
exception: updated,
|
|
1255
|
+
}, null, 2));
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
console.log(chalk.green('\n✅ Organization policy exception rejected.\n'));
|
|
1259
|
+
console.log(chalk.dim(`ID: ${updated.id}`));
|
|
1260
|
+
console.log(chalk.dim(`State: ${updated.effectiveState}`));
|
|
1261
|
+
if (updated.rejectionReason) {
|
|
1262
|
+
console.log(chalk.dim(`Reason: ${updated.rejectionReason}`));
|
|
1263
|
+
}
|
|
1264
|
+
console.log(chalk.dim('Source: Neurcode Cloud (/api/v1/org/policy-exceptions)\n'));
|
|
1265
|
+
}
|
|
1266
|
+
catch (error) {
|
|
1267
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
1268
|
+
if (options.json) {
|
|
1269
|
+
console.log(JSON.stringify({ error: message }, null, 2));
|
|
1270
|
+
process.exit(1);
|
|
1271
|
+
}
|
|
1272
|
+
console.error(chalk.red(`\n❌ ${message}\n`));
|
|
1273
|
+
process.exit(1);
|
|
1274
|
+
}
|
|
1275
|
+
});
|
|
1276
|
+
exception
|
|
1277
|
+
.command('events')
|
|
1278
|
+
.description('Show policy exception audit events')
|
|
1279
|
+
.argument('<id>', 'Exception ID')
|
|
1280
|
+
.option('--org', 'Read centralized organization policy exception events from Neurcode Cloud')
|
|
1281
|
+
.option('--limit <n>', 'Maximum events to return (default: 30)', (value) => parseInt(value, 10))
|
|
1282
|
+
.option('--json', 'Output as JSON')
|
|
1283
|
+
.action(async (id, options) => {
|
|
1284
|
+
const exceptionId = String(id).trim();
|
|
1285
|
+
const limit = normalizeListLimit(options.limit, 30, 1, 300);
|
|
1286
|
+
if (options.org) {
|
|
1287
|
+
try {
|
|
1288
|
+
const config = loadPolicyRuntimeConfig();
|
|
1289
|
+
const client = new api_client_1.ApiClient(config);
|
|
1290
|
+
const events = await client.listOrgPolicyExceptionEvents(exceptionId, limit);
|
|
1291
|
+
if (options.json) {
|
|
1292
|
+
console.log(JSON.stringify({
|
|
1293
|
+
source: 'org',
|
|
1294
|
+
exceptionId,
|
|
1295
|
+
total: events.length,
|
|
1296
|
+
events,
|
|
1297
|
+
}, null, 2));
|
|
1298
|
+
return;
|
|
1299
|
+
}
|
|
1300
|
+
if (events.length === 0) {
|
|
1301
|
+
console.log(chalk.yellow('\n⚠️ No organization exception events found.\n'));
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
console.log(chalk.bold('\n🧾 Organization Exception Events\n'));
|
|
1305
|
+
events.forEach((event) => {
|
|
1306
|
+
const actor = event.actorEmail ||
|
|
1307
|
+
[event.actorFirstName, event.actorLastName].filter(Boolean).join(' ').trim() ||
|
|
1308
|
+
event.actorUserId ||
|
|
1309
|
+
'unknown';
|
|
1310
|
+
console.log(chalk.cyan(`• ${event.createdAt} ${event.action}`));
|
|
1311
|
+
console.log(chalk.dim(` actor=${actor}`));
|
|
1312
|
+
if (event.note) {
|
|
1313
|
+
console.log(chalk.dim(` note=${event.note}`));
|
|
1314
|
+
}
|
|
1315
|
+
console.log(chalk.dim(` eventId=${event.id}`));
|
|
1316
|
+
console.log('');
|
|
1317
|
+
});
|
|
1318
|
+
console.log(chalk.dim('Source: Neurcode Cloud (/api/v1/org/policy-exceptions/:id/events)\n'));
|
|
1319
|
+
return;
|
|
1320
|
+
}
|
|
1321
|
+
catch (error) {
|
|
1322
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
1323
|
+
if (options.json) {
|
|
1324
|
+
console.log(JSON.stringify({ error: message }, null, 2));
|
|
1325
|
+
process.exit(1);
|
|
1326
|
+
}
|
|
1327
|
+
console.error(chalk.red(`\n❌ ${message}\n`));
|
|
1328
|
+
process.exit(1);
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
const cwd = (0, project_root_1.resolveNeurcodeProjectRoot)(process.cwd());
|
|
1332
|
+
const events = (0, policy_audit_1.readPolicyAuditEvents)(cwd)
|
|
1333
|
+
.filter((event) => event.entityType === 'policy_exception' && event.entityId === exceptionId)
|
|
1334
|
+
.sort((left, right) => right.timestamp.localeCompare(left.timestamp))
|
|
1335
|
+
.slice(0, limit);
|
|
1336
|
+
if (options.json) {
|
|
1337
|
+
console.log(JSON.stringify({
|
|
1338
|
+
source: 'local',
|
|
1339
|
+
exceptionId,
|
|
1340
|
+
total: events.length,
|
|
1341
|
+
events,
|
|
1342
|
+
path: (0, policy_audit_1.getPolicyAuditPath)(cwd),
|
|
1343
|
+
}, null, 2));
|
|
1344
|
+
return;
|
|
1345
|
+
}
|
|
1346
|
+
if (events.length === 0) {
|
|
1347
|
+
console.log(chalk.yellow('\n⚠️ No local exception audit events found.\n'));
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
console.log(chalk.bold('\n🧾 Local Exception Events\n'));
|
|
1351
|
+
events.forEach((event) => {
|
|
1352
|
+
console.log(chalk.cyan(`• ${event.timestamp} ${event.action}`));
|
|
1353
|
+
console.log(chalk.dim(` actor=${event.actor}`));
|
|
1354
|
+
if (event.metadata && Object.keys(event.metadata).length > 0) {
|
|
1355
|
+
console.log(chalk.dim(` metadata=${JSON.stringify(event.metadata)}`));
|
|
1356
|
+
}
|
|
1357
|
+
console.log(chalk.dim(` hash=${event.hash.slice(0, 12)}...`));
|
|
1358
|
+
console.log('');
|
|
1359
|
+
});
|
|
1360
|
+
console.log(chalk.dim(`Source: ${(0, policy_audit_1.getPolicyAuditPath)(cwd)}\n`));
|
|
1361
|
+
});
|
|
807
1362
|
exception
|
|
808
1363
|
.command('remove')
|
|
809
1364
|
.description('Deactivate a policy exception by ID')
|
|
810
1365
|
.argument('<id>', 'Exception ID to deactivate')
|
|
1366
|
+
.option('--org', 'Revoke centralized organization policy exception in Neurcode Cloud')
|
|
811
1367
|
.option('--json', 'Output as JSON')
|
|
812
|
-
.action((id, options) => {
|
|
1368
|
+
.action(async (id, options) => {
|
|
1369
|
+
if (options.org) {
|
|
1370
|
+
try {
|
|
1371
|
+
const config = loadPolicyRuntimeConfig();
|
|
1372
|
+
const client = new api_client_1.ApiClient(config);
|
|
1373
|
+
const updated = await client.revokeOrgPolicyException(String(id).trim());
|
|
1374
|
+
if (options.json) {
|
|
1375
|
+
console.log(JSON.stringify({
|
|
1376
|
+
source: 'org',
|
|
1377
|
+
removed: true,
|
|
1378
|
+
exception: updated,
|
|
1379
|
+
}, null, 2));
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
console.log(chalk.green('\n✅ Organization policy exception revoked.\n'));
|
|
1383
|
+
console.log(chalk.dim(`ID: ${updated.id}`));
|
|
1384
|
+
console.log(chalk.dim(`State: ${updated.effectiveState}`));
|
|
1385
|
+
console.log(chalk.dim('Source: Neurcode Cloud (/api/v1/org/policy-exceptions)\n'));
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
catch (error) {
|
|
1389
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
1390
|
+
if (options.json) {
|
|
1391
|
+
console.log(JSON.stringify({ error: message }, null, 2));
|
|
1392
|
+
process.exit(1);
|
|
1393
|
+
}
|
|
1394
|
+
console.error(chalk.red(`\n❌ ${message}\n`));
|
|
1395
|
+
process.exit(1);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
813
1398
|
const cwd = (0, project_root_1.resolveNeurcodeProjectRoot)(process.cwd());
|
|
814
1399
|
const removed = (0, policy_exceptions_1.revokePolicyException)(cwd, String(id).trim());
|
|
815
1400
|
if (removed) {
|