@neurcode-ai/cli 0.9.26 → 0.9.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/allow.d.ts.map +1 -1
- package/dist/commands/allow.js +5 -19
- package/dist/commands/allow.js.map +1 -1
- package/dist/commands/apply.d.ts +1 -0
- package/dist/commands/apply.d.ts.map +1 -1
- package/dist/commands/apply.js +105 -46
- package/dist/commands/apply.js.map +1 -1
- package/dist/commands/ask.d.ts.map +1 -1
- package/dist/commands/ask.js +1849 -1783
- package/dist/commands/ask.js.map +1 -1
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -1
- package/dist/commands/init.js +83 -24
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/plan.d.ts +4 -0
- package/dist/commands/plan.d.ts.map +1 -1
- package/dist/commands/plan.js +344 -48
- package/dist/commands/plan.js.map +1 -1
- package/dist/commands/policy.d.ts.map +1 -1
- package/dist/commands/policy.js +629 -0
- package/dist/commands/policy.js.map +1 -1
- package/dist/commands/prompt.d.ts +7 -1
- package/dist/commands/prompt.d.ts.map +1 -1
- package/dist/commands/prompt.js +106 -25
- package/dist/commands/prompt.js.map +1 -1
- package/dist/commands/ship.d.ts +32 -0
- package/dist/commands/ship.d.ts.map +1 -1
- package/dist/commands/ship.js +1404 -75
- package/dist/commands/ship.js.map +1 -1
- package/dist/commands/verify.d.ts +6 -0
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +527 -102
- package/dist/commands/verify.js.map +1 -1
- package/dist/index.js +89 -3
- package/dist/index.js.map +1 -1
- package/dist/utils/custom-policy-rules.d.ts +21 -0
- package/dist/utils/custom-policy-rules.d.ts.map +1 -0
- package/dist/utils/custom-policy-rules.js +71 -0
- package/dist/utils/custom-policy-rules.js.map +1 -0
- package/dist/utils/plan-cache.d.ts.map +1 -1
- package/dist/utils/plan-cache.js +4 -0
- package/dist/utils/plan-cache.js.map +1 -1
- package/dist/utils/policy-audit.d.ts +29 -0
- package/dist/utils/policy-audit.d.ts.map +1 -0
- package/dist/utils/policy-audit.js +208 -0
- package/dist/utils/policy-audit.js.map +1 -0
- package/dist/utils/policy-exceptions.d.ts +96 -0
- package/dist/utils/policy-exceptions.d.ts.map +1 -0
- package/dist/utils/policy-exceptions.js +389 -0
- package/dist/utils/policy-exceptions.js.map +1 -0
- package/dist/utils/policy-governance.d.ts +24 -0
- package/dist/utils/policy-governance.d.ts.map +1 -0
- package/dist/utils/policy-governance.js +124 -0
- package/dist/utils/policy-governance.js.map +1 -0
- package/dist/utils/policy-packs.d.ts +72 -1
- package/dist/utils/policy-packs.d.ts.map +1 -1
- package/dist/utils/policy-packs.js +285 -0
- package/dist/utils/policy-packs.js.map +1 -1
- package/package.json +1 -1
package/dist/commands/policy.js
CHANGED
|
@@ -2,6 +2,12 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.policyCommand = policyCommand;
|
|
4
4
|
const project_root_1 = require("../utils/project-root");
|
|
5
|
+
const config_1 = require("../config");
|
|
6
|
+
const api_client_1 = require("../api-client");
|
|
7
|
+
const custom_policy_rules_1 = require("../utils/custom-policy-rules");
|
|
8
|
+
const policy_exceptions_1 = require("../utils/policy-exceptions");
|
|
9
|
+
const policy_governance_1 = require("../utils/policy-governance");
|
|
10
|
+
const policy_audit_1 = require("../utils/policy-audit");
|
|
5
11
|
const policy_packs_1 = require("../utils/policy-packs");
|
|
6
12
|
// Import chalk with fallback
|
|
7
13
|
let chalk;
|
|
@@ -28,6 +34,60 @@ function toJsonPack(pack) {
|
|
|
28
34
|
ruleCount: Array.isArray(pack.rules) ? pack.rules.length : 0,
|
|
29
35
|
};
|
|
30
36
|
}
|
|
37
|
+
function loadPolicyRuntimeConfig() {
|
|
38
|
+
const config = (0, config_1.loadConfig)();
|
|
39
|
+
if (process.env.NEURCODE_API_KEY) {
|
|
40
|
+
config.apiKey = process.env.NEURCODE_API_KEY;
|
|
41
|
+
}
|
|
42
|
+
if (process.env.NEURCODE_API_URL) {
|
|
43
|
+
config.apiUrl = process.env.NEURCODE_API_URL.replace(/\/$/, '');
|
|
44
|
+
}
|
|
45
|
+
else if (config.apiUrl) {
|
|
46
|
+
config.apiUrl = config.apiUrl.replace(/\/$/, '');
|
|
47
|
+
}
|
|
48
|
+
return config;
|
|
49
|
+
}
|
|
50
|
+
function resolveExpiresAt(input) {
|
|
51
|
+
if (input.expiresAt && input.expiresAt.trim()) {
|
|
52
|
+
return new Date(input.expiresAt.trim()).toISOString();
|
|
53
|
+
}
|
|
54
|
+
if (Number.isFinite(input.expiresInDays) && input.expiresInDays > 0) {
|
|
55
|
+
const ms = input.expiresInDays * 24 * 60 * 60 * 1000;
|
|
56
|
+
return new Date(Date.now() + ms).toISOString();
|
|
57
|
+
}
|
|
58
|
+
return new Date(Date.now() + 30 * 24 * 60 * 60 * 1000).toISOString();
|
|
59
|
+
}
|
|
60
|
+
function resolveActor(explicit) {
|
|
61
|
+
if (explicit && explicit.trim())
|
|
62
|
+
return explicit.trim();
|
|
63
|
+
return process.env.NEURCODE_ACTOR || process.env.GITHUB_ACTOR || process.env.USER || 'unknown';
|
|
64
|
+
}
|
|
65
|
+
async function resolveCustomPolicies(client, includeDashboardPolicies, requireDashboardPolicies) {
|
|
66
|
+
if (!includeDashboardPolicies) {
|
|
67
|
+
return {
|
|
68
|
+
includeDashboardPolicies: false,
|
|
69
|
+
customPolicies: [],
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
try {
|
|
73
|
+
const customPolicies = await client.getActiveCustomPolicies();
|
|
74
|
+
return {
|
|
75
|
+
includeDashboardPolicies: true,
|
|
76
|
+
customPolicies,
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
81
|
+
if (requireDashboardPolicies) {
|
|
82
|
+
throw new Error(`Failed to load dashboard custom policies: ${message}`);
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
includeDashboardPolicies: false,
|
|
86
|
+
customPolicies: [],
|
|
87
|
+
dashboardWarning: `Dashboard policies unavailable (${message}); continuing without dashboard rules.`,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
}
|
|
31
91
|
function policyCommand(program) {
|
|
32
92
|
const policy = program
|
|
33
93
|
.command('policy')
|
|
@@ -144,5 +204,574 @@ function policyCommand(program) {
|
|
|
144
204
|
}
|
|
145
205
|
console.log(chalk.green('\n✅ Policy pack removed for this repository.\n'));
|
|
146
206
|
});
|
|
207
|
+
policy
|
|
208
|
+
.command('lock')
|
|
209
|
+
.description('Generate or update committed policy lock baseline (neurcode.policy.lock.json)')
|
|
210
|
+
.option('--no-dashboard', 'Exclude dashboard custom policies from the lock baseline')
|
|
211
|
+
.option('--require-dashboard', 'Fail if dashboard custom policies cannot be loaded')
|
|
212
|
+
.option('--json', 'Output as JSON')
|
|
213
|
+
.action(async (options) => {
|
|
214
|
+
const cwd = (0, project_root_1.resolveNeurcodeProjectRoot)(process.cwd());
|
|
215
|
+
const config = loadPolicyRuntimeConfig();
|
|
216
|
+
const client = new api_client_1.ApiClient(config);
|
|
217
|
+
const includeDashboard = options.dashboard !== false;
|
|
218
|
+
try {
|
|
219
|
+
const customPolicyResolution = await resolveCustomPolicies(client, includeDashboard, options.requireDashboard === true);
|
|
220
|
+
const installedPack = (0, policy_packs_1.getInstalledPolicyPackRules)(cwd);
|
|
221
|
+
const customRules = customPolicyResolution.includeDashboardPolicies
|
|
222
|
+
? (0, custom_policy_rules_1.mapActiveCustomPoliciesToRules)(customPolicyResolution.customPolicies)
|
|
223
|
+
: [];
|
|
224
|
+
const snapshot = (0, policy_packs_1.buildPolicyStateSnapshot)({
|
|
225
|
+
policyPack: installedPack,
|
|
226
|
+
policyPackRules: installedPack?.rules || [],
|
|
227
|
+
customPolicies: customPolicyResolution.customPolicies,
|
|
228
|
+
customRules,
|
|
229
|
+
includeDashboardPolicies: customPolicyResolution.includeDashboardPolicies,
|
|
230
|
+
});
|
|
231
|
+
const lockPath = (0, policy_packs_1.writePolicyLockFile)(cwd, snapshot);
|
|
232
|
+
try {
|
|
233
|
+
(0, policy_audit_1.appendPolicyAuditEvent)(cwd, {
|
|
234
|
+
actor: resolveActor(),
|
|
235
|
+
action: 'policy_lock_written',
|
|
236
|
+
entityType: 'policy_lock',
|
|
237
|
+
entityId: 'neurcode.policy.lock.json',
|
|
238
|
+
metadata: {
|
|
239
|
+
policyPack: snapshot.policyPack ? `${snapshot.policyPack.id}@${snapshot.policyPack.version}` : 'none',
|
|
240
|
+
mode: snapshot.customPolicies.mode,
|
|
241
|
+
effectiveRuleCount: snapshot.effective.ruleCount,
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
catch {
|
|
246
|
+
// Non-blocking audit write.
|
|
247
|
+
}
|
|
248
|
+
if (options.json) {
|
|
249
|
+
console.log(JSON.stringify({
|
|
250
|
+
lock: {
|
|
251
|
+
path: lockPath,
|
|
252
|
+
generatedAt: snapshot.generatedAt,
|
|
253
|
+
mode: snapshot.customPolicies.mode,
|
|
254
|
+
effectiveRuleCount: snapshot.effective.ruleCount,
|
|
255
|
+
effectiveFingerprint: snapshot.effective.fingerprint,
|
|
256
|
+
policyPack: snapshot.policyPack
|
|
257
|
+
? {
|
|
258
|
+
id: snapshot.policyPack.id,
|
|
259
|
+
version: snapshot.policyPack.version,
|
|
260
|
+
ruleCount: snapshot.policyPack.ruleCount,
|
|
261
|
+
}
|
|
262
|
+
: null,
|
|
263
|
+
customPolicyCount: snapshot.customPolicies.count,
|
|
264
|
+
},
|
|
265
|
+
warning: customPolicyResolution.dashboardWarning || null,
|
|
266
|
+
}, null, 2));
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
console.log(chalk.green('\n✅ Policy lock baseline updated\n'));
|
|
270
|
+
console.log(chalk.cyan(`Path: ${lockPath}`));
|
|
271
|
+
console.log(chalk.dim(`Effective policy fingerprint: ${snapshot.effective.fingerprint}`));
|
|
272
|
+
console.log(chalk.dim(`Policy pack: ${snapshot.policyPack ? `${snapshot.policyPack.id}@${snapshot.policyPack.version}` : 'none'}`));
|
|
273
|
+
console.log(chalk.dim(`Dashboard policies: ${snapshot.customPolicies.mode} (${snapshot.customPolicies.count})`));
|
|
274
|
+
if (customPolicyResolution.dashboardWarning) {
|
|
275
|
+
console.log(chalk.yellow(`\n⚠️ ${customPolicyResolution.dashboardWarning}`));
|
|
276
|
+
}
|
|
277
|
+
console.log(chalk.dim('Commit this lock file so CI can enforce deterministic policy baselines.'));
|
|
278
|
+
console.log(chalk.dim('\nRun `neurcode policy check --require-lock` in CI to enforce this baseline.\n'));
|
|
279
|
+
}
|
|
280
|
+
catch (error) {
|
|
281
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
282
|
+
if (options.json) {
|
|
283
|
+
console.log(JSON.stringify({ error: message }, null, 2));
|
|
284
|
+
process.exit(1);
|
|
285
|
+
}
|
|
286
|
+
console.error(chalk.red(`\n❌ ${message}\n`));
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
policy
|
|
291
|
+
.command('check')
|
|
292
|
+
.description('Validate current policy state against committed policy lock baseline')
|
|
293
|
+
.option('--require-lock', 'Fail if neurcode.policy.lock.json is missing')
|
|
294
|
+
.option('--json', 'Output as JSON')
|
|
295
|
+
.action(async (options) => {
|
|
296
|
+
const cwd = (0, project_root_1.resolveNeurcodeProjectRoot)(process.cwd());
|
|
297
|
+
const lockRead = (0, policy_packs_1.readPolicyLockFile)(cwd);
|
|
298
|
+
const lockMode = lockRead.lock?.customPolicies.mode;
|
|
299
|
+
const includeDashboardPolicies = lockMode === 'dashboard';
|
|
300
|
+
const config = loadPolicyRuntimeConfig();
|
|
301
|
+
const client = new api_client_1.ApiClient(config);
|
|
302
|
+
let customPolicies = [];
|
|
303
|
+
let dashboardError = null;
|
|
304
|
+
if (includeDashboardPolicies) {
|
|
305
|
+
try {
|
|
306
|
+
customPolicies = await client.getActiveCustomPolicies();
|
|
307
|
+
}
|
|
308
|
+
catch (error) {
|
|
309
|
+
dashboardError = error instanceof Error ? error.message : 'Unknown error';
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
const customRules = includeDashboardPolicies ? (0, custom_policy_rules_1.mapActiveCustomPoliciesToRules)(customPolicies) : [];
|
|
313
|
+
const installedPack = (0, policy_packs_1.getInstalledPolicyPackRules)(cwd);
|
|
314
|
+
const currentSnapshot = (0, policy_packs_1.buildPolicyStateSnapshot)({
|
|
315
|
+
policyPack: installedPack,
|
|
316
|
+
policyPackRules: installedPack?.rules || [],
|
|
317
|
+
customPolicies,
|
|
318
|
+
customRules,
|
|
319
|
+
includeDashboardPolicies,
|
|
320
|
+
});
|
|
321
|
+
const validation = (0, policy_packs_1.evaluatePolicyLock)(cwd, currentSnapshot, {
|
|
322
|
+
requireLock: options.requireLock === true,
|
|
323
|
+
});
|
|
324
|
+
if (dashboardError) {
|
|
325
|
+
validation.mismatches.unshift({
|
|
326
|
+
code: 'POLICY_LOCK_CUSTOM_POLICIES_MISMATCH',
|
|
327
|
+
message: `Failed to load dashboard custom policies while checking lock: ${dashboardError}`,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
const pass = (!validation.enforced || validation.matched) && !dashboardError;
|
|
331
|
+
if (options.json) {
|
|
332
|
+
console.log(JSON.stringify({
|
|
333
|
+
pass,
|
|
334
|
+
enforced: validation.enforced,
|
|
335
|
+
lockPath: validation.lockPath,
|
|
336
|
+
lockPresent: validation.lockPresent,
|
|
337
|
+
mismatches: validation.mismatches,
|
|
338
|
+
effectiveRuleCount: currentSnapshot.effective.ruleCount,
|
|
339
|
+
effectiveFingerprint: currentSnapshot.effective.fingerprint,
|
|
340
|
+
}, null, 2));
|
|
341
|
+
process.exit(pass ? 0 : 1);
|
|
342
|
+
}
|
|
343
|
+
if (pass) {
|
|
344
|
+
if (validation.enforced) {
|
|
345
|
+
console.log(chalk.green('\n✅ Policy lock check passed.\n'));
|
|
346
|
+
console.log(chalk.dim(`Lock: ${validation.lockPath}`));
|
|
347
|
+
console.log(chalk.dim(`Fingerprint: ${currentSnapshot.effective.fingerprint}\n`));
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
console.log(chalk.yellow('\n⚠️ No policy lock found. Nothing to enforce.\n'));
|
|
351
|
+
console.log(chalk.dim(`Expected path: ${(0, policy_packs_1.getPolicyLockPath)(cwd)}`));
|
|
352
|
+
console.log(chalk.dim('Generate baseline: neurcode policy lock\n'));
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
else {
|
|
356
|
+
console.log(chalk.red('\n❌ Policy lock mismatch detected.\n'));
|
|
357
|
+
validation.mismatches.forEach((item) => {
|
|
358
|
+
console.log(chalk.red(`• [${item.code}] ${item.message}`));
|
|
359
|
+
if (item.expected || item.actual) {
|
|
360
|
+
console.log(chalk.dim(` expected: ${item.expected || '(none)'}`));
|
|
361
|
+
console.log(chalk.dim(` actual: ${item.actual || '(none)'}`));
|
|
362
|
+
}
|
|
363
|
+
});
|
|
364
|
+
console.log(chalk.dim('\nIf this policy drift is intentional, refresh baseline: neurcode policy lock\n'));
|
|
365
|
+
}
|
|
366
|
+
process.exit(pass ? 0 : 1);
|
|
367
|
+
});
|
|
368
|
+
const governance = policy
|
|
369
|
+
.command('governance')
|
|
370
|
+
.description('Configure exception approval and policy audit governance');
|
|
371
|
+
governance
|
|
372
|
+
.command('status')
|
|
373
|
+
.description('Show policy governance settings for this repository')
|
|
374
|
+
.option('--json', 'Output as JSON')
|
|
375
|
+
.action((options) => {
|
|
376
|
+
const cwd = (0, project_root_1.resolveNeurcodeProjectRoot)(process.cwd());
|
|
377
|
+
const config = (0, policy_governance_1.readPolicyGovernanceConfig)(cwd);
|
|
378
|
+
if (options.json) {
|
|
379
|
+
console.log(JSON.stringify({
|
|
380
|
+
path: (0, policy_governance_1.getPolicyGovernancePath)(cwd),
|
|
381
|
+
config,
|
|
382
|
+
}, null, 2));
|
|
383
|
+
return;
|
|
384
|
+
}
|
|
385
|
+
console.log(chalk.bold('\n🏛️ Policy Governance\n'));
|
|
386
|
+
console.log(chalk.dim(`Path: ${(0, policy_governance_1.getPolicyGovernancePath)(cwd)}`));
|
|
387
|
+
console.log(chalk.dim(`Exception approvals required: ${config.exceptionApprovals.required ? 'yes' : 'no'}`));
|
|
388
|
+
console.log(chalk.dim(`Minimum approvals: ${config.exceptionApprovals.minApprovals}`));
|
|
389
|
+
console.log(chalk.dim(`Disallow self approval: ${config.exceptionApprovals.disallowSelfApproval ? 'yes' : 'no'}`));
|
|
390
|
+
console.log(chalk.dim(`Allowed approvers: ${config.exceptionApprovals.allowedApprovers.length > 0 ? config.exceptionApprovals.allowedApprovers.join(', ') : '(any)'}`));
|
|
391
|
+
console.log(chalk.dim(`Require audit integrity: ${config.audit.requireIntegrity ? 'yes' : 'no'}`));
|
|
392
|
+
console.log('');
|
|
393
|
+
});
|
|
394
|
+
governance
|
|
395
|
+
.command('set')
|
|
396
|
+
.description('Update policy governance settings')
|
|
397
|
+
.option('--require-approval', 'Require approvals before exceptions become effective')
|
|
398
|
+
.option('--no-require-approval', 'Do not require approvals for exceptions')
|
|
399
|
+
.option('--min-approvals <n>', 'Minimum approvals required when approval mode is enabled', (value) => parseInt(value, 10))
|
|
400
|
+
.option('--allow-self-approval', 'Allow requester to approve their own exception')
|
|
401
|
+
.option('--restrict-approvers <csv>', 'Comma-separated allow-list of approver identities')
|
|
402
|
+
.option('--clear-approvers', 'Clear approver allow-list (allow any approver)')
|
|
403
|
+
.option('--require-audit-integrity', 'Fail verify if policy audit chain integrity is broken')
|
|
404
|
+
.option('--no-require-audit-integrity', 'Do not enforce policy audit integrity in verify')
|
|
405
|
+
.option('--json', 'Output as JSON')
|
|
406
|
+
.action((options) => {
|
|
407
|
+
const cwd = (0, project_root_1.resolveNeurcodeProjectRoot)(process.cwd());
|
|
408
|
+
try {
|
|
409
|
+
const next = (0, policy_governance_1.updatePolicyGovernanceConfig)(cwd, {
|
|
410
|
+
required: typeof options.requireApproval === 'boolean' ? options.requireApproval : undefined,
|
|
411
|
+
minApprovals: Number.isFinite(options.minApprovals) ? options.minApprovals : undefined,
|
|
412
|
+
disallowSelfApproval: typeof options.allowSelfApproval === 'boolean' ? !options.allowSelfApproval : undefined,
|
|
413
|
+
allowedApprovers: options.clearApprovers
|
|
414
|
+
? []
|
|
415
|
+
: typeof options.restrictApprovers === 'string'
|
|
416
|
+
? options.restrictApprovers
|
|
417
|
+
.split(',')
|
|
418
|
+
.map((item) => item.trim())
|
|
419
|
+
.filter(Boolean)
|
|
420
|
+
: undefined,
|
|
421
|
+
requireAuditIntegrity: typeof options.requireAuditIntegrity === 'boolean' ? options.requireAuditIntegrity : undefined,
|
|
422
|
+
});
|
|
423
|
+
try {
|
|
424
|
+
(0, policy_audit_1.appendPolicyAuditEvent)(cwd, {
|
|
425
|
+
actor: resolveActor(),
|
|
426
|
+
action: 'governance_updated',
|
|
427
|
+
entityType: 'policy_governance',
|
|
428
|
+
entityId: 'neurcode.policy.governance.json',
|
|
429
|
+
metadata: {
|
|
430
|
+
requireApproval: next.exceptionApprovals.required,
|
|
431
|
+
minApprovals: next.exceptionApprovals.minApprovals,
|
|
432
|
+
disallowSelfApproval: next.exceptionApprovals.disallowSelfApproval,
|
|
433
|
+
allowedApprovers: next.exceptionApprovals.allowedApprovers,
|
|
434
|
+
requireAuditIntegrity: next.audit.requireIntegrity,
|
|
435
|
+
},
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
// Non-blocking audit write.
|
|
440
|
+
}
|
|
441
|
+
if (options.json) {
|
|
442
|
+
console.log(JSON.stringify({ path: (0, policy_governance_1.getPolicyGovernancePath)(cwd), config: next }, null, 2));
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
console.log(chalk.green('\n✅ Policy governance updated.\n'));
|
|
446
|
+
console.log(chalk.dim(`Path: ${(0, policy_governance_1.getPolicyGovernancePath)(cwd)}`));
|
|
447
|
+
console.log(chalk.dim(`Approval required: ${next.exceptionApprovals.required ? 'yes' : 'no'}`));
|
|
448
|
+
console.log(chalk.dim(`Min approvals: ${next.exceptionApprovals.minApprovals}`));
|
|
449
|
+
console.log(chalk.dim(`Disallow self approval: ${next.exceptionApprovals.disallowSelfApproval ? 'yes' : 'no'}`));
|
|
450
|
+
console.log(chalk.dim(`Require audit integrity: ${next.audit.requireIntegrity ? 'yes' : 'no'}`));
|
|
451
|
+
console.log(chalk.dim('Commit governance + audit files so CI can enforce approval and integrity rules.'));
|
|
452
|
+
console.log('');
|
|
453
|
+
}
|
|
454
|
+
catch (error) {
|
|
455
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
456
|
+
if (options.json) {
|
|
457
|
+
console.log(JSON.stringify({ error: message }, null, 2));
|
|
458
|
+
process.exit(1);
|
|
459
|
+
}
|
|
460
|
+
console.error(chalk.red(`\n❌ ${message}\n`));
|
|
461
|
+
process.exit(1);
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
const audit = policy
|
|
465
|
+
.command('audit')
|
|
466
|
+
.description('Inspect policy audit chain integrity');
|
|
467
|
+
audit
|
|
468
|
+
.command('verify')
|
|
469
|
+
.description('Verify append-only policy audit chain integrity')
|
|
470
|
+
.option('--json', 'Output as JSON')
|
|
471
|
+
.action((options) => {
|
|
472
|
+
const cwd = (0, project_root_1.resolveNeurcodeProjectRoot)(process.cwd());
|
|
473
|
+
const result = (0, policy_audit_1.verifyPolicyAuditIntegrity)(cwd);
|
|
474
|
+
if (options.json) {
|
|
475
|
+
console.log(JSON.stringify({
|
|
476
|
+
path: (0, policy_audit_1.getPolicyAuditPath)(cwd),
|
|
477
|
+
...result,
|
|
478
|
+
}, null, 2));
|
|
479
|
+
process.exit(result.valid ? 0 : 1);
|
|
480
|
+
}
|
|
481
|
+
if (result.valid) {
|
|
482
|
+
console.log(chalk.green('\n✅ Policy audit chain is valid.\n'));
|
|
483
|
+
console.log(chalk.dim(`Path: ${(0, policy_audit_1.getPolicyAuditPath)(cwd)}`));
|
|
484
|
+
console.log(chalk.dim(`Events: ${result.count}`));
|
|
485
|
+
console.log(chalk.dim(`Last hash: ${result.lastHash || '(none)'}\n`));
|
|
486
|
+
process.exit(0);
|
|
487
|
+
}
|
|
488
|
+
console.log(chalk.red('\n❌ Policy audit chain integrity check failed.\n'));
|
|
489
|
+
console.log(chalk.dim(`Path: ${(0, policy_audit_1.getPolicyAuditPath)(cwd)}`));
|
|
490
|
+
result.issues.forEach((issue) => console.log(chalk.red(`• ${issue}`)));
|
|
491
|
+
console.log('');
|
|
492
|
+
process.exit(1);
|
|
493
|
+
});
|
|
494
|
+
const exception = policy
|
|
495
|
+
.command('exception')
|
|
496
|
+
.description('Manage time-bound policy exceptions (audited allow-list)');
|
|
497
|
+
exception
|
|
498
|
+
.command('list')
|
|
499
|
+
.description('List policy exceptions for this repository')
|
|
500
|
+
.option('--all', 'Include inactive/expired exceptions')
|
|
501
|
+
.option('--json', 'Output as JSON')
|
|
502
|
+
.action((options) => {
|
|
503
|
+
const cwd = (0, project_root_1.resolveNeurcodeProjectRoot)(process.cwd());
|
|
504
|
+
const data = (0, policy_exceptions_1.listPolicyExceptions)(cwd);
|
|
505
|
+
const governance = (0, policy_governance_1.readPolicyGovernanceConfig)(cwd);
|
|
506
|
+
const allowedApprovers = new Set(governance.exceptionApprovals.allowedApprovers.map((item) => item.toLowerCase()));
|
|
507
|
+
const withStatus = data.all.map((entry) => {
|
|
508
|
+
const unexpired = new Date(entry.expiresAt).getTime() > Date.now();
|
|
509
|
+
let effectiveApprovals = entry.approvals;
|
|
510
|
+
if (governance.exceptionApprovals.disallowSelfApproval) {
|
|
511
|
+
const requested = (entry.requestedBy || entry.createdBy || '').toLowerCase();
|
|
512
|
+
effectiveApprovals = effectiveApprovals.filter((item) => item.approver.toLowerCase() !== requested);
|
|
513
|
+
}
|
|
514
|
+
if (allowedApprovers.size > 0) {
|
|
515
|
+
effectiveApprovals = effectiveApprovals.filter((item) => allowedApprovers.has(item.approver.toLowerCase()));
|
|
516
|
+
}
|
|
517
|
+
const status = !entry.active || !unexpired
|
|
518
|
+
? 'inactive'
|
|
519
|
+
: !governance.exceptionApprovals.required
|
|
520
|
+
? 'active'
|
|
521
|
+
: effectiveApprovals.length >= governance.exceptionApprovals.minApprovals
|
|
522
|
+
? 'approved'
|
|
523
|
+
: 'pending';
|
|
524
|
+
return {
|
|
525
|
+
...entry,
|
|
526
|
+
status,
|
|
527
|
+
effectiveApprovals: effectiveApprovals.length,
|
|
528
|
+
};
|
|
529
|
+
});
|
|
530
|
+
const items = options.all ? withStatus : withStatus.filter((entry) => entry.status !== 'inactive');
|
|
531
|
+
if (options.json) {
|
|
532
|
+
console.log(JSON.stringify({
|
|
533
|
+
path: (0, policy_exceptions_1.getPolicyExceptionsPath)(cwd),
|
|
534
|
+
governance,
|
|
535
|
+
total: data.all.length,
|
|
536
|
+
active: data.active.length,
|
|
537
|
+
expired: data.expired.length,
|
|
538
|
+
exceptions: items,
|
|
539
|
+
}, null, 2));
|
|
540
|
+
return;
|
|
541
|
+
}
|
|
542
|
+
if (items.length === 0) {
|
|
543
|
+
console.log(chalk.yellow('\n⚠️ No policy exceptions found.\n'));
|
|
544
|
+
console.log(chalk.dim('Add one: neurcode policy exception add --rule <pattern> --file <glob> --reason "<why>"\n'));
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
console.log(chalk.bold('\n🧾 Policy Exceptions\n'));
|
|
548
|
+
items.forEach((entry) => {
|
|
549
|
+
console.log(chalk.cyan(`• ${entry.id}`));
|
|
550
|
+
console.log(chalk.dim(` status=${entry.status || (entry.active ? 'active' : 'inactive')}`));
|
|
551
|
+
console.log(chalk.dim(` rule=${entry.rulePattern} file=${entry.filePattern}`));
|
|
552
|
+
console.log(chalk.dim(` expires=${entry.expiresAt} active=${entry.active ? 'yes' : 'no'}`));
|
|
553
|
+
console.log(chalk.dim(` approvals=${entry.approvals.length}${typeof entry.effectiveApprovals === 'number' ? ` (effective=${entry.effectiveApprovals})` : ''}`));
|
|
554
|
+
console.log(chalk.dim(` reason=${entry.reason}`));
|
|
555
|
+
console.log(chalk.dim(` requestedBy=${entry.requestedBy || entry.createdBy || 'unknown'}`));
|
|
556
|
+
if (entry.ticket) {
|
|
557
|
+
console.log(chalk.dim(` ticket=${entry.ticket}`));
|
|
558
|
+
}
|
|
559
|
+
console.log('');
|
|
560
|
+
});
|
|
561
|
+
if (!options.all && data.expired.length > 0) {
|
|
562
|
+
console.log(chalk.dim(`(${data.expired.length} expired/inactive hidden; use --all)`));
|
|
563
|
+
}
|
|
564
|
+
console.log('');
|
|
565
|
+
});
|
|
566
|
+
exception
|
|
567
|
+
.command('add')
|
|
568
|
+
.description('Add a policy exception entry')
|
|
569
|
+
.requiredOption('--rule <pattern>', 'Rule pattern (exact, wildcard, or /regex/)')
|
|
570
|
+
.requiredOption('--file <pattern>', 'File pattern (exact, wildcard, or /regex/)')
|
|
571
|
+
.requiredOption('--reason <text>', 'Business justification for this exception')
|
|
572
|
+
.option('--ticket <id>', 'Ticket or approval reference (e.g. SEC-123)')
|
|
573
|
+
.option('--severity <level>', 'Optional severity scope: allow|warn|block')
|
|
574
|
+
.option('--expires-at <iso>', 'Expiry timestamp in ISO-8601')
|
|
575
|
+
.option('--expires-in-days <n>', 'Expiry offset in days (default: 30)', (value) => parseInt(value, 10))
|
|
576
|
+
.option('--json', 'Output as JSON')
|
|
577
|
+
.action((options) => {
|
|
578
|
+
const cwd = (0, project_root_1.resolveNeurcodeProjectRoot)(process.cwd());
|
|
579
|
+
try {
|
|
580
|
+
const severity = options.severity === 'allow' || options.severity === 'warn' || options.severity === 'block'
|
|
581
|
+
? options.severity
|
|
582
|
+
: undefined;
|
|
583
|
+
if (options.severity && !severity) {
|
|
584
|
+
throw new Error('severity must be one of: allow, warn, block');
|
|
585
|
+
}
|
|
586
|
+
const expiresAt = resolveExpiresAt({
|
|
587
|
+
expiresAt: options.expiresAt,
|
|
588
|
+
expiresInDays: options.expiresInDays,
|
|
589
|
+
});
|
|
590
|
+
const createdBy = resolveActor();
|
|
591
|
+
const created = (0, policy_exceptions_1.addPolicyException)(cwd, {
|
|
592
|
+
rulePattern: options.rule,
|
|
593
|
+
filePattern: options.file,
|
|
594
|
+
reason: options.reason,
|
|
595
|
+
ticket: options.ticket,
|
|
596
|
+
severity,
|
|
597
|
+
expiresAt,
|
|
598
|
+
createdBy,
|
|
599
|
+
requestedBy: createdBy,
|
|
600
|
+
});
|
|
601
|
+
const governance = (0, policy_governance_1.readPolicyGovernanceConfig)(cwd);
|
|
602
|
+
try {
|
|
603
|
+
(0, policy_audit_1.appendPolicyAuditEvent)(cwd, {
|
|
604
|
+
actor: createdBy,
|
|
605
|
+
action: 'exception_added',
|
|
606
|
+
entityType: 'policy_exception',
|
|
607
|
+
entityId: created.id,
|
|
608
|
+
metadata: {
|
|
609
|
+
rulePattern: created.rulePattern,
|
|
610
|
+
filePattern: created.filePattern,
|
|
611
|
+
expiresAt: created.expiresAt,
|
|
612
|
+
requireApproval: governance.exceptionApprovals.required,
|
|
613
|
+
},
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
catch {
|
|
617
|
+
// Non-blocking audit write.
|
|
618
|
+
}
|
|
619
|
+
if (options.json) {
|
|
620
|
+
console.log(JSON.stringify({
|
|
621
|
+
created,
|
|
622
|
+
path: (0, policy_exceptions_1.getPolicyExceptionsPath)(cwd),
|
|
623
|
+
requiresApproval: governance.exceptionApprovals.required,
|
|
624
|
+
}, null, 2));
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
console.log(chalk.green('\n✅ Policy exception added\n'));
|
|
628
|
+
console.log(chalk.cyan(`ID: ${created.id}`));
|
|
629
|
+
console.log(chalk.dim(`Rule: ${created.rulePattern}`));
|
|
630
|
+
console.log(chalk.dim(`File: ${created.filePattern}`));
|
|
631
|
+
console.log(chalk.dim(`Expires: ${created.expiresAt}`));
|
|
632
|
+
console.log(chalk.dim(`Reason: ${created.reason}`));
|
|
633
|
+
if (governance.exceptionApprovals.required) {
|
|
634
|
+
console.log(chalk.yellow(`Approval required: ${governance.exceptionApprovals.minApprovals} approver(s) before this exception is active.`));
|
|
635
|
+
}
|
|
636
|
+
if (created.ticket) {
|
|
637
|
+
console.log(chalk.dim(`Ticket: ${created.ticket}`));
|
|
638
|
+
}
|
|
639
|
+
console.log(chalk.dim('\nAudit tip: commit policy exception changes with approval context.\n'));
|
|
640
|
+
}
|
|
641
|
+
catch (error) {
|
|
642
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
643
|
+
if (options.json) {
|
|
644
|
+
console.log(JSON.stringify({ error: message }, null, 2));
|
|
645
|
+
process.exit(1);
|
|
646
|
+
}
|
|
647
|
+
console.error(chalk.red(`\n❌ ${message}\n`));
|
|
648
|
+
process.exit(1);
|
|
649
|
+
}
|
|
650
|
+
});
|
|
651
|
+
exception
|
|
652
|
+
.command('approve')
|
|
653
|
+
.description('Approve a policy exception by ID')
|
|
654
|
+
.argument('<id>', 'Exception ID to approve')
|
|
655
|
+
.option('--by <actor>', 'Approver identity (defaults to NEURCODE_ACTOR/GITHUB_ACTOR/USER)')
|
|
656
|
+
.option('--comment <text>', 'Approval comment')
|
|
657
|
+
.option('--json', 'Output as JSON')
|
|
658
|
+
.action((id, options) => {
|
|
659
|
+
const cwd = (0, project_root_1.resolveNeurcodeProjectRoot)(process.cwd());
|
|
660
|
+
const approver = resolveActor(options.by);
|
|
661
|
+
try {
|
|
662
|
+
const updated = (0, policy_exceptions_1.approvePolicyException)(cwd, String(id).trim(), {
|
|
663
|
+
approver,
|
|
664
|
+
comment: options.comment,
|
|
665
|
+
});
|
|
666
|
+
if (!updated) {
|
|
667
|
+
if (options.json) {
|
|
668
|
+
console.log(JSON.stringify({ error: 'Exception not found' }, null, 2));
|
|
669
|
+
process.exit(1);
|
|
670
|
+
}
|
|
671
|
+
console.log(chalk.yellow('\n⚠️ Exception not found.\n'));
|
|
672
|
+
process.exit(1);
|
|
673
|
+
}
|
|
674
|
+
try {
|
|
675
|
+
(0, policy_audit_1.appendPolicyAuditEvent)(cwd, {
|
|
676
|
+
actor: approver,
|
|
677
|
+
action: 'exception_approved',
|
|
678
|
+
entityType: 'policy_exception',
|
|
679
|
+
entityId: updated.id,
|
|
680
|
+
metadata: {
|
|
681
|
+
comment: options.comment || null,
|
|
682
|
+
approvals: updated.approvals.length,
|
|
683
|
+
},
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
catch {
|
|
687
|
+
// Non-blocking audit write.
|
|
688
|
+
}
|
|
689
|
+
if (options.json) {
|
|
690
|
+
console.log(JSON.stringify({
|
|
691
|
+
approved: true,
|
|
692
|
+
exception: updated,
|
|
693
|
+
}, null, 2));
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
console.log(chalk.green('\n✅ Policy exception approval recorded.\n'));
|
|
697
|
+
console.log(chalk.dim(`ID: ${updated.id}`));
|
|
698
|
+
console.log(chalk.dim(`Approvals: ${updated.approvals.length}`));
|
|
699
|
+
console.log(chalk.dim(`Approver: ${approver}`));
|
|
700
|
+
console.log('');
|
|
701
|
+
}
|
|
702
|
+
catch (error) {
|
|
703
|
+
const message = error instanceof Error ? error.message : 'Unknown error';
|
|
704
|
+
if (options.json) {
|
|
705
|
+
console.log(JSON.stringify({ error: message }, null, 2));
|
|
706
|
+
process.exit(1);
|
|
707
|
+
}
|
|
708
|
+
console.error(chalk.red(`\n❌ ${message}\n`));
|
|
709
|
+
process.exit(1);
|
|
710
|
+
}
|
|
711
|
+
});
|
|
712
|
+
exception
|
|
713
|
+
.command('remove')
|
|
714
|
+
.description('Deactivate a policy exception by ID')
|
|
715
|
+
.argument('<id>', 'Exception ID to deactivate')
|
|
716
|
+
.option('--json', 'Output as JSON')
|
|
717
|
+
.action((id, options) => {
|
|
718
|
+
const cwd = (0, project_root_1.resolveNeurcodeProjectRoot)(process.cwd());
|
|
719
|
+
const removed = (0, policy_exceptions_1.revokePolicyException)(cwd, String(id).trim());
|
|
720
|
+
if (removed) {
|
|
721
|
+
try {
|
|
722
|
+
(0, policy_audit_1.appendPolicyAuditEvent)(cwd, {
|
|
723
|
+
actor: resolveActor(),
|
|
724
|
+
action: 'exception_revoked',
|
|
725
|
+
entityType: 'policy_exception',
|
|
726
|
+
entityId: String(id).trim(),
|
|
727
|
+
metadata: {},
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
catch {
|
|
731
|
+
// Non-blocking audit write.
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
if (options.json) {
|
|
735
|
+
console.log(JSON.stringify({ removed }, null, 2));
|
|
736
|
+
process.exit(removed ? 0 : 1);
|
|
737
|
+
}
|
|
738
|
+
if (!removed) {
|
|
739
|
+
console.log(chalk.yellow('\n⚠️ Exception not found or already inactive.\n'));
|
|
740
|
+
process.exit(1);
|
|
741
|
+
}
|
|
742
|
+
console.log(chalk.green('\n✅ Policy exception deactivated.\n'));
|
|
743
|
+
});
|
|
744
|
+
exception
|
|
745
|
+
.command('prune')
|
|
746
|
+
.description('Prune expired/inactive policy exceptions')
|
|
747
|
+
.option('--json', 'Output as JSON')
|
|
748
|
+
.action((options) => {
|
|
749
|
+
const cwd = (0, project_root_1.resolveNeurcodeProjectRoot)(process.cwd());
|
|
750
|
+
const result = (0, policy_exceptions_1.pruneExpiredPolicyExceptions)(cwd);
|
|
751
|
+
if (result.removed > 0) {
|
|
752
|
+
try {
|
|
753
|
+
(0, policy_audit_1.appendPolicyAuditEvent)(cwd, {
|
|
754
|
+
actor: resolveActor(),
|
|
755
|
+
action: 'exception_pruned',
|
|
756
|
+
entityType: 'policy_exception',
|
|
757
|
+
entityId: null,
|
|
758
|
+
metadata: {
|
|
759
|
+
removed: result.removed,
|
|
760
|
+
remaining: result.remaining,
|
|
761
|
+
},
|
|
762
|
+
});
|
|
763
|
+
}
|
|
764
|
+
catch {
|
|
765
|
+
// Non-blocking audit write.
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
if (options.json) {
|
|
769
|
+
console.log(JSON.stringify(result, null, 2));
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
console.log(chalk.green('\n✅ Policy exceptions pruned.\n'));
|
|
773
|
+
console.log(chalk.dim(`Removed: ${result.removed}`));
|
|
774
|
+
console.log(chalk.dim(`Remaining active: ${result.remaining}\n`));
|
|
775
|
+
});
|
|
147
776
|
}
|
|
148
777
|
//# sourceMappingURL=policy.js.map
|