@codapult/guard 0.6.1 → 0.7.0
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 +39 -5
- package/dist/cli/commands/guard.d.ts +7 -0
- package/dist/cli/commands/guard.js +128 -4
- package/dist/cli/index.js +28 -3
- package/dist/core/analysis/doctor.d.ts +1 -1
- package/dist/core/analysis/doctor.js +35 -2
- package/dist/core/guard.d.ts +27 -2
- package/dist/core/guard.js +162 -2
- package/dist/core/model/types.d.ts +22 -0
- package/dist/core/output/sarif.d.ts +7 -2
- package/dist/core/output/sarif.js +23 -14
- package/dist/core/policy/schemas.d.ts +51 -0
- package/dist/core/policy/schemas.js +33 -0
- package/dist/core/verification/verify.js +2 -1
- package/dist/mcp/resources.js +18 -1
- package/dist/mcp/tools/guard.js +112 -1
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -231,12 +231,46 @@ Guard state is stored in `.codapult/guard/`:
|
|
|
231
231
|
├── proposals.json evidence and approval history
|
|
232
232
|
├── baseline.json accepted pre-existing findings
|
|
233
233
|
├── agent.json AI-host completion-gate configuration
|
|
234
|
+
├── waivers.json time-bounded exceptions to active findings
|
|
234
235
|
└── history/ project snapshots and local verification run manifests
|
|
235
236
|
```
|
|
236
237
|
|
|
237
238
|
Commit policy and baseline files when the team wants shared guardrails. Treat cache artifacts as
|
|
238
239
|
disposable according to the project’s policy, and never commit secrets.
|
|
239
240
|
|
|
241
|
+
### Temporary waivers
|
|
242
|
+
|
|
243
|
+
Baseline records legacy findings that existed when Guard was introduced. A waiver is different: it
|
|
244
|
+
temporarily suppresses one active finding and must have an owner, reason, and expiry date.
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
codapult-guard waiver add <fingerprint> \
|
|
248
|
+
--owner platform-team \
|
|
249
|
+
--reason "Migration in progress" \
|
|
250
|
+
--expires 2026-12-28
|
|
251
|
+
codapult-guard waiver list
|
|
252
|
+
codapult-guard waiver renew <id> --expires 2027-01-31 --reason "Migration continues"
|
|
253
|
+
codapult-guard waiver remove <id> --reason "Migration completed"
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
Guard warns when a waiver is within the configured warning window (14 days by default). Once it
|
|
257
|
+
expires, the original finding is active again. Protected policy can forbid waiver changes through
|
|
258
|
+
MCP so an agent can propose a waiver but cannot approve it itself. Waivers are intentionally
|
|
259
|
+
fingerprint-specific; changing the violating code invalidates the waiver.
|
|
260
|
+
|
|
261
|
+
Teams can optionally cap each new waiver or renewal with `waiverPolicy.maxDays` in `rules.json`:
|
|
262
|
+
|
|
263
|
+
```json
|
|
264
|
+
"waiverPolicy": { "warningDays": 14, "maxDays": 90 }
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
The cap applies to the requested grant from the current operation; it does not silently delete or
|
|
268
|
+
rewrite existing waivers. Boundary findings with severity `error` fail `check`; warning findings
|
|
269
|
+
and waiver-expiry reminders remain advisory.
|
|
270
|
+
|
|
271
|
+
Run `codapult-guard doctor` to detect an explicitly downgraded client or package boundary. Doctor
|
|
272
|
+
reports it as a warning and leaves the project policy unchanged.
|
|
273
|
+
|
|
240
274
|
### Optional scoped budgets
|
|
241
275
|
|
|
242
276
|
Budgets are policy, not a universal style rule. Add them only for a named risk boundary such as a
|
|
@@ -310,11 +344,11 @@ For Cursor, Claude Code, Codex, Gemini CLI, GitHub Copilot, and generic hosts, s
|
|
|
310
344
|
|
|
311
345
|
### MCP Registry
|
|
312
346
|
|
|
313
|
-
Guard is
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
347
|
+
Guard is published in the official [MCP Registry](https://registry.modelcontextprotocol.io/?q=io.github.codapult%2Fguard)
|
|
348
|
+
under the server name `io.github.codapult/guard`. The Registry is the discovery layer for the
|
|
349
|
+
published `@codapult/guard` npm package and its stdio MCP server; it does not replace npm
|
|
350
|
+
installation or the host-specific configuration above. For deterministic setup, install the npm
|
|
351
|
+
package directly and use the MCP configuration shown below.
|
|
318
352
|
|
|
319
353
|
For workflows where the authoring agent must not approve its own policy proposals, set Guard to
|
|
320
354
|
protected mode in `rules.json`:
|
|
@@ -26,6 +26,12 @@ interface GuardBaselineOptions extends GuardOutputOptions {
|
|
|
26
26
|
all?: boolean | undefined;
|
|
27
27
|
reason?: string | undefined;
|
|
28
28
|
}
|
|
29
|
+
export interface GuardWaiverOptions extends GuardOutputOptions {
|
|
30
|
+
owner?: string | undefined;
|
|
31
|
+
reason?: string | undefined;
|
|
32
|
+
expires?: string | undefined;
|
|
33
|
+
issue?: string | undefined;
|
|
34
|
+
}
|
|
29
35
|
interface GuardApprovalOptions {
|
|
30
36
|
all?: boolean | undefined;
|
|
31
37
|
}
|
|
@@ -49,6 +55,7 @@ export declare function guardVerifyCommand(options?: GuardVerifyOptions): void;
|
|
|
49
55
|
export declare function guardCheckCommand(options?: GuardCheckOptions): void;
|
|
50
56
|
export declare function guardAuditCommand(options?: GuardOutputOptions): void;
|
|
51
57
|
export declare function guardBaselineCommand(action: 'list' | 'accept' | 'remove', ids?: string, options?: GuardBaselineOptions): void;
|
|
58
|
+
export declare function guardWaiverCommand(action: 'list' | 'add' | 'remove' | 'renew', value?: string, options?: GuardWaiverOptions): void;
|
|
52
59
|
export declare function guardRulesApproveCommand(ids?: string, options?: GuardApprovalOptions): void;
|
|
53
60
|
export declare function guardPolicyExplainCommand(id: string, options?: GuardOutputOptions): void;
|
|
54
61
|
export declare function guardContractsApproveCommand(ids?: string, options?: GuardApprovalOptions): void;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
1
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
2
3
|
import { relative, resolve } from 'node:path';
|
|
3
|
-
import { GUARD_BASELINE_FILE, GUARD_BASELINE_META_FILE, GUARD_ARCHITECTURE_FILE, GUARD_CONVENTIONS_FILE, GUARD_AGENT_FILE, GUARD_CONTRACTS_FILE, GUARD_PROPOSALS_FILE, GUARD_PROJECT_FILE, GUARD_HISTORY_DIR, GUARD_RULES_FILE, findGuardRoot, discoverProjectWithMetrics, clearDiscoveryCache, buildGuardReviewPacket, buildGuardProposals, buildGeneratedGuardConfig, discoverProject, GuardAlreadyInitializedError, initializeGuard, loadBaseline, updateBaseline, loadGuardConfig, loadGuardProposals, applyGuardProposalDecision, writeGuardProposals, validateGuardProposalApproval, getGuardProposalFreshness, scanGuard, classifyGuardOutcome, validateGuardPolicy, writeProjectState, } from '../../core/guard.js';
|
|
4
|
+
import { GUARD_BASELINE_FILE, GUARD_BASELINE_META_FILE, GUARD_ARCHITECTURE_FILE, GUARD_CONVENTIONS_FILE, GUARD_AGENT_FILE, GUARD_CONTRACTS_FILE, GUARD_PROPOSALS_FILE, GUARD_PROJECT_FILE, GUARD_HISTORY_DIR, GUARD_RULES_FILE, GuardWaiverError, findGuardRoot, discoverProjectWithMetrics, clearDiscoveryCache, buildGuardReviewPacket, buildGuardProposals, buildGeneratedGuardConfig, discoverProject, GuardAlreadyInitializedError, initializeGuard, loadBaseline, loadGuardWaivers, updateBaseline, updateGuardWaivers, loadGuardConfig, loadGuardProposals, applyGuardProposalDecision, writeGuardProposals, validateGuardProposalApproval, getGuardProposalFreshness, scanGuard, classifyGuardOutcome, validateGuardPolicy, writeProjectState, } from '../../core/guard.js';
|
|
4
5
|
import { guardErrorPayload } from '../../core/errors.js';
|
|
5
6
|
import { runGuardVerification, } from '../../core/verification/verify.js';
|
|
6
7
|
import { diagnoseGuard } from '../../core/analysis/doctor.js';
|
|
@@ -19,6 +20,16 @@ function renderFindings(findings) {
|
|
|
19
20
|
dim(` resolved: ${finding.resolvedPath}`);
|
|
20
21
|
}
|
|
21
22
|
}
|
|
23
|
+
function renderWaiverStatus(report) {
|
|
24
|
+
for (const waiver of report.waiverWarnings ?? []) {
|
|
25
|
+
warn(`Waiver ${waiver.id} expires soon: ${waiver.expiresAt} (${waiver.owner})`);
|
|
26
|
+
dim(` ${waiver.fingerprint}: ${waiver.reason}`);
|
|
27
|
+
}
|
|
28
|
+
for (const waiver of report.expiredWaivers ?? []) {
|
|
29
|
+
warn(`Waiver ${waiver.id} expired: ${waiver.expiresAt} (${waiver.owner})`);
|
|
30
|
+
dim(` The original finding is active again: ${waiver.fingerprint}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
22
33
|
function getRoot() {
|
|
23
34
|
return findGuardRoot();
|
|
24
35
|
}
|
|
@@ -174,6 +185,8 @@ export function guardDoctorCommand(options = {}) {
|
|
|
174
185
|
success(`${item.path}: ok`);
|
|
175
186
|
else if (item.status === 'missing')
|
|
176
187
|
warn(`${item.path}: missing`);
|
|
188
|
+
else if (item.status === 'warning')
|
|
189
|
+
warn(`${item.path}: ${item.message}`);
|
|
177
190
|
else
|
|
178
191
|
fail(`${item.path}: invalid`);
|
|
179
192
|
}
|
|
@@ -303,6 +316,8 @@ export function guardVerifyCommand(options = {}) {
|
|
|
303
316
|
success('architecture: no new regressions');
|
|
304
317
|
else
|
|
305
318
|
renderFindings(findings);
|
|
319
|
+
if (result.architecture)
|
|
320
|
+
renderWaiverStatus(result.architecture);
|
|
306
321
|
for (const issue of result.contractIssues)
|
|
307
322
|
warn(`[${issue.contractId}] ${issue.message}`);
|
|
308
323
|
if (result.contractIssues.length > 0)
|
|
@@ -340,7 +355,8 @@ export function guardCheckCommand(options = {}) {
|
|
|
340
355
|
});
|
|
341
356
|
const contractIssues = validateGuardPolicy(root, config);
|
|
342
357
|
const errors = report.findings.filter((finding) => finding.severity === 'error').length;
|
|
343
|
-
const warnings = report.findings.filter((finding) => finding.severity === 'warning').length
|
|
358
|
+
const warnings = report.findings.filter((finding) => finding.severity === 'warning').length +
|
|
359
|
+
(report.waiverWarnings ?? []).length;
|
|
344
360
|
const contractFindings = contractIssues.map((issue) => ({
|
|
345
361
|
ruleId: `contract:${issue.contractId}`,
|
|
346
362
|
severity: 'error',
|
|
@@ -351,7 +367,19 @@ export function guardCheckCommand(options = {}) {
|
|
|
351
367
|
fingerprint: `contract-invalid|${issue.contractId}|${issue.field}|${issue.value}`,
|
|
352
368
|
}));
|
|
353
369
|
if (options.sarif) {
|
|
354
|
-
|
|
370
|
+
const waiverNotices = [
|
|
371
|
+
...(report.waiverWarnings ?? []).map((waiver) => ({
|
|
372
|
+
ruleId: 'guard-waiver-expiring',
|
|
373
|
+
level: 'warning',
|
|
374
|
+
message: `Waiver ${waiver.id} for ${waiver.ruleId} expires at ${waiver.expiresAt}. Owner: ${waiver.owner}.`,
|
|
375
|
+
})),
|
|
376
|
+
...(report.expiredWaivers ?? []).map((waiver) => ({
|
|
377
|
+
ruleId: 'guard-waiver-expired',
|
|
378
|
+
level: 'warning',
|
|
379
|
+
message: `Waiver ${waiver.id} for ${waiver.ruleId} expired at ${waiver.expiresAt}; the finding is active again.`,
|
|
380
|
+
})),
|
|
381
|
+
];
|
|
382
|
+
console.log(JSON.stringify(guardFindingsToSarif([...report.findings, ...contractFindings], waiverNotices), null, 2));
|
|
355
383
|
process.exitCode = errors > 0 || contractIssues.length > 0 ? 1 : 0;
|
|
356
384
|
return;
|
|
357
385
|
}
|
|
@@ -370,6 +398,7 @@ export function guardCheckCommand(options = {}) {
|
|
|
370
398
|
heading(`Codapult Guard${options.changed ? ' (changed files)' : ''}`);
|
|
371
399
|
dim(`Scanned ${report.scannedFiles} source file(s); suppressed ${report.suppressed} baseline finding(s).`);
|
|
372
400
|
renderFindings(report.findings);
|
|
401
|
+
renderWaiverStatus(report);
|
|
373
402
|
for (const issue of contractIssues)
|
|
374
403
|
warn(`[${issue.contractId}] ${issue.message}`);
|
|
375
404
|
if (errors > 0 || contractIssues.length > 0)
|
|
@@ -406,7 +435,8 @@ export function guardAuditCommand(options = {}) {
|
|
|
406
435
|
const report = scanGuard(root, config, { includeArchitectureInsights: true });
|
|
407
436
|
const contractIssues = validateGuardPolicy(root, config);
|
|
408
437
|
const errors = report.findings.filter((finding) => finding.severity === 'error').length;
|
|
409
|
-
const warnings = report.findings.filter((finding) => finding.severity === 'warning').length
|
|
438
|
+
const warnings = report.findings.filter((finding) => finding.severity === 'warning').length +
|
|
439
|
+
(report.waiverWarnings ?? []).length;
|
|
410
440
|
if (options.json) {
|
|
411
441
|
console.log(JSON.stringify({
|
|
412
442
|
status: errors > 0 || contractIssues.length > 0 ? 'fail' : warnings > 0 ? 'warning' : 'ok',
|
|
@@ -425,6 +455,7 @@ export function guardAuditCommand(options = {}) {
|
|
|
425
455
|
heading('Codapult Guard Audit');
|
|
426
456
|
dim(`Scanned ${report.scannedFiles} source file(s); baseline is ignored for this full audit.`);
|
|
427
457
|
renderFindings(report.findings);
|
|
458
|
+
renderWaiverStatus(report);
|
|
428
459
|
for (const issue of contractIssues)
|
|
429
460
|
warn(`[${issue.contractId}] ${issue.message}`);
|
|
430
461
|
if (errors > 0)
|
|
@@ -480,6 +511,99 @@ export function guardBaselineCommand(action, ids, options = {}) {
|
|
|
480
511
|
success(`${action === 'accept' ? 'Accepted' : 'Removed'} ${selected.length} baseline fingerprint(s).`);
|
|
481
512
|
process.exitCode = 0;
|
|
482
513
|
}
|
|
514
|
+
export function guardWaiverCommand(action, value, options = {}) {
|
|
515
|
+
const root = getRoot();
|
|
516
|
+
const waivers = loadGuardWaivers(root);
|
|
517
|
+
const reason = options.reason?.trim();
|
|
518
|
+
if (action === 'list') {
|
|
519
|
+
const result = { count: waivers.length, waivers };
|
|
520
|
+
if (options.json)
|
|
521
|
+
console.log(JSON.stringify(result, null, 2));
|
|
522
|
+
else {
|
|
523
|
+
heading('Codapult Guard Waivers');
|
|
524
|
+
for (const waiver of waivers) {
|
|
525
|
+
const status = Date.parse(waiver.expiresAt) <= Date.now() ? 'expired' : 'active';
|
|
526
|
+
info(`${waiver.id} [${status}] ${waiver.owner} → ${waiver.expiresAt}`);
|
|
527
|
+
dim(` ${waiver.fingerprint}: ${waiver.reason}`);
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
process.exitCode = 0;
|
|
531
|
+
return;
|
|
532
|
+
}
|
|
533
|
+
if (!reason)
|
|
534
|
+
throw new GuardWaiverError('A reason is required for waiver changes.');
|
|
535
|
+
const config = loadGuardConfig(root);
|
|
536
|
+
if (!config)
|
|
537
|
+
throw new GuardWaiverError('Guard is not initialized.');
|
|
538
|
+
if (config.approval?.mode === 'protected' &&
|
|
539
|
+
config.approval.requireDistinctActor &&
|
|
540
|
+
!process.env.GUARD_APPROVER?.trim()) {
|
|
541
|
+
throw new GuardWaiverError('Protected Guard policy requires GUARD_APPROVER for waiver changes.');
|
|
542
|
+
}
|
|
543
|
+
if (action === 'add') {
|
|
544
|
+
const owner = options.owner?.trim();
|
|
545
|
+
if (!value || !owner || !options.expires) {
|
|
546
|
+
throw new GuardWaiverError('Add requires <fingerprint>, --owner, and --expires.');
|
|
547
|
+
}
|
|
548
|
+
const expires = new Date(options.expires);
|
|
549
|
+
if (!Number.isFinite(expires.getTime()))
|
|
550
|
+
throw new GuardWaiverError('Invalid waiver expiry date.');
|
|
551
|
+
const expiresAt = expires.toISOString();
|
|
552
|
+
const finding = scanGuard(root, config, {
|
|
553
|
+
waivers: [],
|
|
554
|
+
includeArchitectureInsights: true,
|
|
555
|
+
}).findings.find((candidate) => candidate.fingerprint === value);
|
|
556
|
+
if (!finding)
|
|
557
|
+
throw new GuardWaiverError(`Finding fingerprint not found: ${value}`);
|
|
558
|
+
const waiver = {
|
|
559
|
+
id: `waiver-${randomUUID()}`,
|
|
560
|
+
fingerprint: value,
|
|
561
|
+
ruleId: finding.ruleId,
|
|
562
|
+
owner,
|
|
563
|
+
reason,
|
|
564
|
+
createdAt: new Date().toISOString(),
|
|
565
|
+
expiresAt,
|
|
566
|
+
...(options.issue ? { issue: options.issue } : {}),
|
|
567
|
+
...(process.env.GUARD_APPROVER?.trim()
|
|
568
|
+
? { approvedBy: process.env.GUARD_APPROVER.trim() }
|
|
569
|
+
: {}),
|
|
570
|
+
};
|
|
571
|
+
updateGuardWaivers(root, {
|
|
572
|
+
action,
|
|
573
|
+
waiverId: waiver.id,
|
|
574
|
+
waiver,
|
|
575
|
+
reason,
|
|
576
|
+
actor: process.env.GUARD_APPROVER,
|
|
577
|
+
maxLifetimeDays: config.waiverPolicy?.maxDays,
|
|
578
|
+
});
|
|
579
|
+
if (options.json)
|
|
580
|
+
console.log(JSON.stringify({ action, waiver }, null, 2));
|
|
581
|
+
else
|
|
582
|
+
success(`Added waiver ${waiver.id}; expires ${waiver.expiresAt}.`);
|
|
583
|
+
}
|
|
584
|
+
else {
|
|
585
|
+
if (!value)
|
|
586
|
+
throw new GuardWaiverError(`Waiver id is required to ${action}.`);
|
|
587
|
+
const expires = action === 'renew' ? new Date(options.expires ?? '') : undefined;
|
|
588
|
+
if (expires && !Number.isFinite(expires.getTime())) {
|
|
589
|
+
throw new GuardWaiverError('Invalid waiver expiry date.');
|
|
590
|
+
}
|
|
591
|
+
const expiresAt = expires?.toISOString();
|
|
592
|
+
updateGuardWaivers(root, {
|
|
593
|
+
action,
|
|
594
|
+
waiverId: value,
|
|
595
|
+
...(expiresAt ? { expiresAt } : {}),
|
|
596
|
+
reason,
|
|
597
|
+
actor: process.env.GUARD_APPROVER,
|
|
598
|
+
maxLifetimeDays: config.waiverPolicy?.maxDays,
|
|
599
|
+
});
|
|
600
|
+
if (options.json)
|
|
601
|
+
console.log(JSON.stringify({ action, waiverId: value, expiresAt }, null, 2));
|
|
602
|
+
else
|
|
603
|
+
success(`${action === 'renew' ? 'Renewed' : 'Removed'} waiver ${value}.`);
|
|
604
|
+
}
|
|
605
|
+
process.exitCode = 0;
|
|
606
|
+
}
|
|
483
607
|
export function guardRulesApproveCommand(ids, options = {}) {
|
|
484
608
|
const root = getRoot();
|
|
485
609
|
const loaded = loadConfigSafely(root);
|
package/dist/cli/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import pc from 'picocolors';
|
|
4
|
-
import { guardAnalyzeCommand, guardAuditCommand, guardBaselineCommand, guardCheckCommand, guardContractsApproveCommand, guardContractsRejectCommand, guardDoctorCommand, guardHistoryCommand, guardHistoryDiffCommand, guardImpactCommand, guardInitCommand, guardInstallAgentCommand, guardPolicyExplainCommand, guardProposeCommand, guardReviewCommand, guardRunsCommand, guardRulesApproveCommand, guardVerifyCommand, } from './commands/guard.js';
|
|
4
|
+
import { guardAnalyzeCommand, guardAuditCommand, guardBaselineCommand, guardCheckCommand, guardContractsApproveCommand, guardContractsRejectCommand, guardDoctorCommand, guardHistoryCommand, guardHistoryDiffCommand, guardImpactCommand, guardInitCommand, guardInstallAgentCommand, guardPolicyExplainCommand, guardProposeCommand, guardReviewCommand, guardRunsCommand, guardRulesApproveCommand, guardVerifyCommand, guardWaiverCommand, } from './commands/guard.js';
|
|
5
5
|
import { config } from '../core/config.js';
|
|
6
|
-
import { GuardBaselineReasonError, GuardConfigError, GuardStateBusyError, GuardStateStaleError, } from '../core/guard.js';
|
|
6
|
+
import { GuardBaselineReasonError, GuardConfigError, GuardStateBusyError, GuardStateStaleError, GuardWaiverError, } from '../core/guard.js';
|
|
7
7
|
import { guardErrorPayload } from '../core/errors.js';
|
|
8
8
|
const program = new Command()
|
|
9
9
|
.name(config.commandName)
|
|
@@ -72,6 +72,30 @@ baseline
|
|
|
72
72
|
.option('--reason <text>')
|
|
73
73
|
.option('--json')
|
|
74
74
|
.action((ids, options) => guardBaselineCommand('remove', ids, options));
|
|
75
|
+
const waivers = guard.command('waiver');
|
|
76
|
+
waivers
|
|
77
|
+
.command('list')
|
|
78
|
+
.option('--json')
|
|
79
|
+
.action((options) => guardWaiverCommand('list', undefined, options));
|
|
80
|
+
waivers
|
|
81
|
+
.command('add <fingerprint>')
|
|
82
|
+
.requiredOption('--owner <owner>')
|
|
83
|
+
.requiredOption('--reason <text>')
|
|
84
|
+
.requiredOption('--expires <date>')
|
|
85
|
+
.option('--issue <url>')
|
|
86
|
+
.option('--json')
|
|
87
|
+
.action((fingerprint, options) => guardWaiverCommand('add', fingerprint, options));
|
|
88
|
+
waivers
|
|
89
|
+
.command('remove <id>')
|
|
90
|
+
.requiredOption('--reason <text>')
|
|
91
|
+
.option('--json')
|
|
92
|
+
.action((id, options) => guardWaiverCommand('remove', id, options));
|
|
93
|
+
waivers
|
|
94
|
+
.command('renew <id>')
|
|
95
|
+
.requiredOption('--expires <date>')
|
|
96
|
+
.requiredOption('--reason <text>')
|
|
97
|
+
.option('--json')
|
|
98
|
+
.action((id, options) => guardWaiverCommand('renew', id, options));
|
|
75
99
|
program
|
|
76
100
|
.command('mcp-server')
|
|
77
101
|
.description('start the Guard MCP server over stdio')
|
|
@@ -85,7 +109,8 @@ catch (error) {
|
|
|
85
109
|
if (error instanceof GuardConfigError ||
|
|
86
110
|
error instanceof GuardBaselineReasonError ||
|
|
87
111
|
error instanceof GuardStateBusyError ||
|
|
88
|
-
error instanceof GuardStateStaleError
|
|
112
|
+
error instanceof GuardStateStaleError ||
|
|
113
|
+
error instanceof GuardWaiverError) {
|
|
89
114
|
const isBusy = error instanceof GuardStateBusyError;
|
|
90
115
|
const isStale = error instanceof GuardStateStaleError;
|
|
91
116
|
const isInvalidInput = error instanceof GuardBaselineReasonError;
|
|
@@ -11,6 +11,33 @@ const requiredArtifacts = [
|
|
|
11
11
|
GUARD_CONTRACTS_FILE,
|
|
12
12
|
GUARD_PROPOSALS_FILE,
|
|
13
13
|
];
|
|
14
|
+
function downgradedBoundaryItems(root) {
|
|
15
|
+
let guardConfig;
|
|
16
|
+
try {
|
|
17
|
+
guardConfig = loadGuardConfig(root);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
if (!guardConfig)
|
|
23
|
+
return [];
|
|
24
|
+
const items = guardConfig.rules
|
|
25
|
+
.filter((rule) => rule.kind === 'client-forbidden-import' && rule.severity === 'warning')
|
|
26
|
+
.map((rule) => ({
|
|
27
|
+
path: GUARD_RULES_FILE,
|
|
28
|
+
status: 'warning',
|
|
29
|
+
message: `Boundary rule '${rule.id}' is warning-only and will not block changes.`,
|
|
30
|
+
}));
|
|
31
|
+
items.push(...(guardConfig.contracts ?? [])
|
|
32
|
+
.filter((contract) => (contract.kind === 'import-boundary' || contract.kind === 'package-boundary') &&
|
|
33
|
+
contract.severity === 'warning')
|
|
34
|
+
.map((contract) => ({
|
|
35
|
+
path: GUARD_CONTRACTS_FILE,
|
|
36
|
+
status: 'warning',
|
|
37
|
+
message: `Boundary contract '${contract.id}' is warning-only and will not block changes.`,
|
|
38
|
+
})));
|
|
39
|
+
return items;
|
|
40
|
+
}
|
|
14
41
|
function parseJson(root, path) {
|
|
15
42
|
try {
|
|
16
43
|
return JSON.parse(readFileSync(resolve(root, path), 'utf8'));
|
|
@@ -64,6 +91,7 @@ export function diagnoseGuard(root) {
|
|
|
64
91
|
}
|
|
65
92
|
return { path, status: 'ok', message: 'Artifact is present and readable.' };
|
|
66
93
|
});
|
|
94
|
+
items.push(...downgradedBoundaryItems(root));
|
|
67
95
|
if (initialized && existsSync(resolve(root, GUARD_BASELINE_META_FILE))) {
|
|
68
96
|
const metadata = parseJson(root, GUARD_BASELINE_META_FILE);
|
|
69
97
|
items.push({
|
|
@@ -79,10 +107,11 @@ export function diagnoseGuard(root) {
|
|
|
79
107
|
}
|
|
80
108
|
const missing = items.filter((item) => item.status === 'missing');
|
|
81
109
|
const invalid = items.filter((item) => item.status === 'invalid');
|
|
110
|
+
const warnings = items.filter((item) => item.status === 'warning');
|
|
82
111
|
return {
|
|
83
112
|
status: invalid.length > 0 || (initialized && missing.length > 0)
|
|
84
113
|
? 'fail'
|
|
85
|
-
: missing.length > 0
|
|
114
|
+
: missing.length > 0 || warnings.length > 0
|
|
86
115
|
? 'warning'
|
|
87
116
|
: 'ok',
|
|
88
117
|
initialized,
|
|
@@ -93,6 +122,10 @@ export function diagnoseGuard(root) {
|
|
|
93
122
|
}
|
|
94
123
|
: missing.length > 0
|
|
95
124
|
? { recommendation: 'Run `codapult-guard analyze` or reinitialize Guard intentionally.' }
|
|
96
|
-
:
|
|
125
|
+
: warnings.length > 0
|
|
126
|
+
? {
|
|
127
|
+
recommendation: 'Review downgraded boundary policy before relying on Guard as a hard gate.',
|
|
128
|
+
}
|
|
129
|
+
: {}),
|
|
97
130
|
};
|
|
98
131
|
}
|
package/dist/core/guard.d.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { type ProjectModel } from './discovery/discovery.js';
|
|
2
2
|
import { clearDiscoveryCache, discoverProject, discoverProjectWithMetrics, findGuardRoot } from './discovery/discovery.js';
|
|
3
3
|
import { type GuardImpactAnalysis } from './analysis/impact.js';
|
|
4
|
-
import type { GuardAdapterName, GuardApprovalMode, GuardContractKind, GuardBudgetMetric, GuardRuleKind, GuardRuleStatus, GuardSeverity, GuardToolMode } from './model/types.js';
|
|
5
|
-
export type { GuardAdapterName, GuardApprovalMode, GuardContractKind, GuardBudgetMetric, GuardRuleKind, GuardRuleStatus, GuardSeverity, GuardToolMode, } from './model/types.js';
|
|
4
|
+
import type { GuardAdapterName, GuardApprovalMode, GuardContractKind, GuardBudgetMetric, GuardRuleKind, GuardRuleStatus, GuardSeverity, GuardToolMode, GuardWaiver, GuardWaiverPolicy } from './model/types.js';
|
|
5
|
+
export type { GuardAdapterName, GuardApprovalMode, GuardContractKind, GuardBudgetMetric, GuardRuleKind, GuardRuleStatus, GuardSeverity, GuardToolMode, GuardWaiver, GuardWaiverPolicy, GuardWaiverDecision, } from './model/types.js';
|
|
6
6
|
export type GuardOutcomeStatus = 'pass' | 'fail' | 'warning' | 'needs-review' | 'not-configured';
|
|
7
7
|
export interface GuardOutcomeInput {
|
|
8
8
|
configured?: boolean | undefined;
|
|
@@ -85,6 +85,7 @@ export interface GuardConfig {
|
|
|
85
85
|
contracts?: GuardContract[] | undefined;
|
|
86
86
|
budgets?: GuardBudget[] | undefined;
|
|
87
87
|
approval?: GuardApprovalPolicy | undefined;
|
|
88
|
+
waiverPolicy?: GuardWaiverPolicy | undefined;
|
|
88
89
|
}
|
|
89
90
|
export interface GuardApprovalPolicy {
|
|
90
91
|
mode: GuardApprovalMode;
|
|
@@ -157,12 +158,17 @@ export interface GuardReport {
|
|
|
157
158
|
findings: GuardFinding[];
|
|
158
159
|
suppressed: number;
|
|
159
160
|
scannedFiles: number;
|
|
161
|
+
waived: number;
|
|
162
|
+
waiverWarnings: GuardWaiver[];
|
|
163
|
+
expiredWaivers: GuardWaiver[];
|
|
160
164
|
}
|
|
161
165
|
interface ScanGuardOptions {
|
|
162
166
|
changedOnly?: boolean | undefined;
|
|
163
167
|
changedFiles?: Set<string> | undefined;
|
|
164
168
|
baseline?: Set<string> | undefined;
|
|
165
169
|
includeArchitectureInsights?: boolean | undefined;
|
|
170
|
+
waivers?: GuardWaiver[] | undefined;
|
|
171
|
+
now?: string | undefined;
|
|
166
172
|
}
|
|
167
173
|
export interface GuardReviewPacket {
|
|
168
174
|
version: 1;
|
|
@@ -178,6 +184,9 @@ export interface GuardReviewPacket {
|
|
|
178
184
|
contracts: GuardContract[];
|
|
179
185
|
requirement?: string | undefined;
|
|
180
186
|
deterministicFindings: GuardFinding[];
|
|
187
|
+
waived: number;
|
|
188
|
+
waiverWarnings: GuardWaiver[];
|
|
189
|
+
expiredWaivers: GuardWaiver[];
|
|
181
190
|
impact: GuardImpactAnalysis;
|
|
182
191
|
reviewInstructions: string[];
|
|
183
192
|
}
|
|
@@ -185,6 +194,7 @@ export declare const GUARD_DIR: string;
|
|
|
185
194
|
export declare const GUARD_RULES_FILE: string;
|
|
186
195
|
export declare const GUARD_BASELINE_FILE: string;
|
|
187
196
|
export declare const GUARD_BASELINE_META_FILE: string;
|
|
197
|
+
export declare const GUARD_WAIVERS_FILE: string;
|
|
188
198
|
export declare const GUARD_PROJECT_FILE: string;
|
|
189
199
|
export declare const GUARD_HISTORY_DIR: string;
|
|
190
200
|
export declare const GUARD_ARCHITECTURE_FILE: string;
|
|
@@ -212,6 +222,9 @@ export declare class GuardStateStaleError extends Error {
|
|
|
212
222
|
export declare class GuardBaselineReasonError extends Error {
|
|
213
223
|
constructor();
|
|
214
224
|
}
|
|
225
|
+
export declare class GuardWaiverError extends Error {
|
|
226
|
+
constructor(message: string);
|
|
227
|
+
}
|
|
215
228
|
export declare function withGuardStateLock<T>(root: string, callback: () => T, options?: GuardStateLockOptions): T;
|
|
216
229
|
export declare function isGuardContractsFile(value: unknown): value is {
|
|
217
230
|
version: 1;
|
|
@@ -227,6 +240,18 @@ export declare function isGuardAgentConfig(value: unknown): value is GuardAgentC
|
|
|
227
240
|
export declare function loadGuardAgentConfig(root: string): GuardAgentConfig;
|
|
228
241
|
export declare function writeGuardAgentConfig(root: string, agentConfig?: GuardAgentConfig): void;
|
|
229
242
|
export declare function loadBaseline(root: string): Set<string>;
|
|
243
|
+
export interface GuardWaiverUpdateOptions {
|
|
244
|
+
action: 'add' | 'renew' | 'remove';
|
|
245
|
+
waiver?: GuardWaiver | undefined;
|
|
246
|
+
waiverId: string;
|
|
247
|
+
reason: string;
|
|
248
|
+
expiresAt?: string | undefined;
|
|
249
|
+
actor?: string | undefined;
|
|
250
|
+
maxLifetimeDays?: number | undefined;
|
|
251
|
+
}
|
|
252
|
+
export declare function loadGuardWaivers(root: string): GuardWaiver[];
|
|
253
|
+
export declare function writeGuardWaivers(root: string): void;
|
|
254
|
+
export declare function updateGuardWaivers(root: string, options: GuardWaiverUpdateOptions): GuardWaiver[];
|
|
230
255
|
export declare function updateBaseline(root: string, options?: GuardBaselineUpdateOptions): Set<string>;
|
|
231
256
|
export declare function writeGuardConfig(root: string, guardConfig: GuardConfig, options?: GuardConfigWriteOptions): void;
|
|
232
257
|
export declare function writeGuardProposals(root: string, proposals: GuardProposalFile): void;
|
package/dist/core/guard.js
CHANGED
|
@@ -9,7 +9,7 @@ import { clearDiscoveryCache, discoverProject, discoverProjectWithMetrics, findG
|
|
|
9
9
|
import { config } from './config.js';
|
|
10
10
|
import { assessGuardPacks, detectGuardPacks } from './analysis/packs.js';
|
|
11
11
|
import { analyzeProjectImpact } from './analysis/impact.js';
|
|
12
|
-
import { guardAgentConfigSchema, guardConfigSchema, guardContractsFileSchema, guardProposalSchema, } from './policy/schemas.js';
|
|
12
|
+
import { guardAgentConfigSchema, guardConfigSchema, guardContractsFileSchema, guardProposalSchema, guardWaiversFileSchema, } from './policy/schemas.js';
|
|
13
13
|
export function classifyGuardOutcome(input) {
|
|
14
14
|
if (input.configured === false)
|
|
15
15
|
return 'not-configured';
|
|
@@ -23,6 +23,7 @@ export const GUARD_DIR = `.${config.appName}/guard`;
|
|
|
23
23
|
export const GUARD_RULES_FILE = `${GUARD_DIR}/rules.json`;
|
|
24
24
|
export const GUARD_BASELINE_FILE = `${GUARD_DIR}/baseline.json`;
|
|
25
25
|
export const GUARD_BASELINE_META_FILE = `${GUARD_DIR}/baseline-meta.json`;
|
|
26
|
+
export const GUARD_WAIVERS_FILE = `${GUARD_DIR}/waivers.json`;
|
|
26
27
|
export const GUARD_PROJECT_FILE = `${GUARD_DIR}/project.json`;
|
|
27
28
|
export const GUARD_HISTORY_DIR = `${GUARD_DIR}/history`;
|
|
28
29
|
export const GUARD_ARCHITECTURE_FILE = `${GUARD_DIR}/architecture.json`;
|
|
@@ -44,6 +45,7 @@ export const defaultGuardConfig = {
|
|
|
44
45
|
allowMcpApproval: true,
|
|
45
46
|
requireDistinctActor: false,
|
|
46
47
|
},
|
|
48
|
+
waiverPolicy: { warningDays: 14 },
|
|
47
49
|
};
|
|
48
50
|
export const defaultGuardAgentConfig = {
|
|
49
51
|
version: 1,
|
|
@@ -86,6 +88,12 @@ export class GuardBaselineReasonError extends Error {
|
|
|
86
88
|
this.name = 'GuardBaselineReasonError';
|
|
87
89
|
}
|
|
88
90
|
}
|
|
91
|
+
export class GuardWaiverError extends Error {
|
|
92
|
+
constructor(message) {
|
|
93
|
+
super(message);
|
|
94
|
+
this.name = 'GuardWaiverError';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
89
97
|
const activeStateLocks = new Map();
|
|
90
98
|
function processIsAlive(pid) {
|
|
91
99
|
try {
|
|
@@ -429,6 +437,119 @@ export function loadBaseline(root) {
|
|
|
429
437
|
throw new GuardConfigError(GUARD_BASELINE_FILE);
|
|
430
438
|
return new Set(value.filter((item) => typeof item === 'string'));
|
|
431
439
|
}
|
|
440
|
+
function validateGuardWaiversFile(value) {
|
|
441
|
+
const ids = new Set();
|
|
442
|
+
const fingerprints = new Set();
|
|
443
|
+
for (const waiver of value.waivers) {
|
|
444
|
+
if (ids.has(waiver.id))
|
|
445
|
+
throw new GuardConfigError(GUARD_WAIVERS_FILE);
|
|
446
|
+
if (fingerprints.has(waiver.fingerprint))
|
|
447
|
+
throw new GuardConfigError(GUARD_WAIVERS_FILE);
|
|
448
|
+
if (Date.parse(waiver.expiresAt) <= Date.parse(waiver.createdAt)) {
|
|
449
|
+
throw new GuardConfigError(GUARD_WAIVERS_FILE);
|
|
450
|
+
}
|
|
451
|
+
ids.add(waiver.id);
|
|
452
|
+
fingerprints.add(waiver.fingerprint);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
function assertWaiverLifetime(expiresAt, maxLifetimeDays) {
|
|
456
|
+
if (maxLifetimeDays === undefined)
|
|
457
|
+
return;
|
|
458
|
+
const lifetime = Date.parse(expiresAt) - Date.now();
|
|
459
|
+
if (lifetime > maxLifetimeDays * 24 * 60 * 60 * 1000) {
|
|
460
|
+
throw new GuardWaiverError(`A waiver may not exceed the configured maximum lifetime of ${maxLifetimeDays} day(s).`);
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
function readGuardWaiversUnlocked(root) {
|
|
464
|
+
const path = resolve(root, GUARD_WAIVERS_FILE);
|
|
465
|
+
if (!existsSync(path))
|
|
466
|
+
return { version: 1, waivers: [], decisions: [] };
|
|
467
|
+
const parsed = guardWaiversFileSchema.safeParse(readJson(path));
|
|
468
|
+
if (!parsed.success)
|
|
469
|
+
throw new GuardConfigError(GUARD_WAIVERS_FILE);
|
|
470
|
+
validateGuardWaiversFile(parsed.data);
|
|
471
|
+
return parsed.data;
|
|
472
|
+
}
|
|
473
|
+
export function loadGuardWaivers(root) {
|
|
474
|
+
if (!existsSync(resolve(root, GUARD_DIR)))
|
|
475
|
+
return readGuardWaiversUnlocked(root).waivers;
|
|
476
|
+
return withGuardStateLock(root, () => readGuardWaiversUnlocked(root).waivers);
|
|
477
|
+
}
|
|
478
|
+
export function writeGuardWaivers(root) {
|
|
479
|
+
withGuardStateLock(root, () => {
|
|
480
|
+
const file = { version: 1, waivers: [], decisions: [] };
|
|
481
|
+
atomicWriteFile(resolve(root, GUARD_WAIVERS_FILE), `${JSON.stringify(file, null, 2)}\n`);
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
export function updateGuardWaivers(root, options) {
|
|
485
|
+
if (!options.reason.trim())
|
|
486
|
+
throw new GuardWaiverError('A reason is required for waiver changes.');
|
|
487
|
+
return withGuardStateLock(root, () => {
|
|
488
|
+
const current = readGuardWaiversUnlocked(root);
|
|
489
|
+
const index = current.waivers.findIndex((waiver) => waiver.id === options.waiverId);
|
|
490
|
+
if (options.action === 'add') {
|
|
491
|
+
if (!options.waiver)
|
|
492
|
+
throw new GuardWaiverError('A waiver is required when adding one.');
|
|
493
|
+
if (index >= 0 ||
|
|
494
|
+
current.waivers.some((waiver) => waiver.fingerprint === options.waiver?.fingerprint)) {
|
|
495
|
+
throw new GuardWaiverError('A waiver already exists for this id or fingerprint.');
|
|
496
|
+
}
|
|
497
|
+
if (Date.parse(options.waiver.expiresAt) <= Date.now()) {
|
|
498
|
+
throw new GuardWaiverError('A waiver must expire in the future.');
|
|
499
|
+
}
|
|
500
|
+
if (Date.parse(options.waiver.expiresAt) <= Date.parse(options.waiver.createdAt)) {
|
|
501
|
+
throw new GuardWaiverError('A waiver must expire after it is created.');
|
|
502
|
+
}
|
|
503
|
+
assertWaiverLifetime(options.waiver.expiresAt, options.maxLifetimeDays);
|
|
504
|
+
const candidate = guardWaiversFileSchema.safeParse({
|
|
505
|
+
version: 1,
|
|
506
|
+
waivers: [...current.waivers, options.waiver],
|
|
507
|
+
decisions: current.decisions,
|
|
508
|
+
});
|
|
509
|
+
if (!candidate.success)
|
|
510
|
+
throw new GuardWaiverError('The waiver data is invalid.');
|
|
511
|
+
try {
|
|
512
|
+
validateGuardWaiversFile(candidate.data);
|
|
513
|
+
}
|
|
514
|
+
catch {
|
|
515
|
+
throw new GuardWaiverError('The waiver data is invalid.');
|
|
516
|
+
}
|
|
517
|
+
current.waivers.push(options.waiver);
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
if (index < 0)
|
|
521
|
+
throw new GuardWaiverError(`Waiver not found: ${options.waiverId}`);
|
|
522
|
+
if (options.action === 'remove')
|
|
523
|
+
current.waivers.splice(index, 1);
|
|
524
|
+
else {
|
|
525
|
+
if (!options.expiresAt || Date.parse(options.expiresAt) <= Date.now()) {
|
|
526
|
+
throw new GuardWaiverError('A renewed waiver must expire in the future.');
|
|
527
|
+
}
|
|
528
|
+
assertWaiverLifetime(options.expiresAt, options.maxLifetimeDays);
|
|
529
|
+
current.waivers[index] = { ...current.waivers[index], expiresAt: options.expiresAt };
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
const decision = {
|
|
533
|
+
at: new Date().toISOString(),
|
|
534
|
+
action: options.action,
|
|
535
|
+
waiverId: options.waiverId,
|
|
536
|
+
reason: options.reason.trim(),
|
|
537
|
+
...(options.actor?.trim() ? { actor: options.actor.trim() } : {}),
|
|
538
|
+
};
|
|
539
|
+
current.decisions = [...current.decisions, decision].slice(-500);
|
|
540
|
+
const validated = guardWaiversFileSchema.safeParse(current);
|
|
541
|
+
if (!validated.success)
|
|
542
|
+
throw new GuardWaiverError('The waiver data is invalid.');
|
|
543
|
+
try {
|
|
544
|
+
validateGuardWaiversFile(validated.data);
|
|
545
|
+
}
|
|
546
|
+
catch {
|
|
547
|
+
throw new GuardWaiverError('The waiver data is invalid.');
|
|
548
|
+
}
|
|
549
|
+
atomicWriteFile(resolve(root, GUARD_WAIVERS_FILE), `${JSON.stringify(current, null, 2)}\n`);
|
|
550
|
+
return current.waivers;
|
|
551
|
+
});
|
|
552
|
+
}
|
|
432
553
|
export function updateBaseline(root, options = {}) {
|
|
433
554
|
if ((options.add?.length ?? 0) > 0 || (options.remove?.length ?? 0) > 0) {
|
|
434
555
|
if (!options.reason?.trim())
|
|
@@ -1538,12 +1659,45 @@ export function scanGuard(root, guardConfig, options = {}) {
|
|
|
1538
1659
|
...(options.includeArchitectureInsights ? architectureInsightFindings(root, scope) : []),
|
|
1539
1660
|
];
|
|
1540
1661
|
const baseline = options.baseline ?? new Set();
|
|
1541
|
-
const
|
|
1662
|
+
const waivers = options.waivers ?? loadGuardWaivers(root);
|
|
1663
|
+
if (options.waivers) {
|
|
1664
|
+
const parsedWaivers = guardWaiversFileSchema.safeParse({
|
|
1665
|
+
version: 1,
|
|
1666
|
+
waivers,
|
|
1667
|
+
decisions: [],
|
|
1668
|
+
});
|
|
1669
|
+
if (!parsedWaivers.success)
|
|
1670
|
+
throw new GuardWaiverError('The waiver data is invalid.');
|
|
1671
|
+
try {
|
|
1672
|
+
validateGuardWaiversFile(parsedWaivers.data);
|
|
1673
|
+
}
|
|
1674
|
+
catch {
|
|
1675
|
+
throw new GuardWaiverError('The waiver data is invalid.');
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
const nowValue = options.now ?? new Date().toISOString();
|
|
1679
|
+
const now = Date.parse(nowValue);
|
|
1680
|
+
if (!Number.isFinite(now))
|
|
1681
|
+
throw new GuardWaiverError(`Invalid Guard time: ${nowValue}`);
|
|
1682
|
+
const activeWaivers = new Map(waivers
|
|
1683
|
+
.filter((waiver) => Date.parse(waiver.expiresAt) > now)
|
|
1684
|
+
.map((waiver) => [waiver.fingerprint, waiver]));
|
|
1685
|
+
const warningDays = guardConfig.waiverPolicy?.warningDays ?? 14;
|
|
1686
|
+
const warningLimit = now + warningDays * 24 * 60 * 60 * 1000;
|
|
1687
|
+
const currentFingerprints = new Set(allFindings.map((finding) => finding.fingerprint));
|
|
1688
|
+
const relevantWaivers = waivers.filter((waiver) => currentFingerprints.has(waiver.fingerprint));
|
|
1689
|
+
const waiverWarnings = relevantWaivers.filter((waiver) => Date.parse(waiver.expiresAt) > now && Date.parse(waiver.expiresAt) <= warningLimit);
|
|
1690
|
+
const expiredWaivers = relevantWaivers.filter((waiver) => Date.parse(waiver.expiresAt) <= now);
|
|
1691
|
+
const baselineFindings = allFindings.filter((finding) => !baseline.has(finding.fingerprint));
|
|
1692
|
+
const findings = baselineFindings.filter((finding) => !activeWaivers.has(finding.fingerprint));
|
|
1542
1693
|
return {
|
|
1543
1694
|
configured: true,
|
|
1544
1695
|
findings,
|
|
1545
1696
|
suppressed: allFindings.length - findings.length,
|
|
1546
1697
|
scannedFiles: files.length,
|
|
1698
|
+
waived: baselineFindings.length - findings.length,
|
|
1699
|
+
waiverWarnings,
|
|
1700
|
+
expiredWaivers,
|
|
1547
1701
|
};
|
|
1548
1702
|
}
|
|
1549
1703
|
export function buildGuardReviewPacket(root, guardConfig, baseline = new Set(), maxDiffChars = 120_000, changedOnly = true, requirement, base) {
|
|
@@ -1570,6 +1724,9 @@ export function buildGuardReviewPacket(root, guardConfig, baseline = new Set(),
|
|
|
1570
1724
|
contracts: guardConfig.contracts ?? [],
|
|
1571
1725
|
...(requirement ? { requirement } : {}),
|
|
1572
1726
|
deterministicFindings: report.findings,
|
|
1727
|
+
waived: report.waived,
|
|
1728
|
+
waiverWarnings: report.waiverWarnings,
|
|
1729
|
+
expiredWaivers: report.expiredWaivers,
|
|
1573
1730
|
impact,
|
|
1574
1731
|
reviewInstructions: [
|
|
1575
1732
|
...(requirement
|
|
@@ -1595,6 +1752,9 @@ export function initializeGuard(root, options = {}) {
|
|
|
1595
1752
|
const guardConfig = loadGuardConfig(root) ?? buildGeneratedGuardConfig(projectModel);
|
|
1596
1753
|
ensureGeneratedStateIgnored(root, projectModel);
|
|
1597
1754
|
writeGuardConfig(root, guardConfig);
|
|
1755
|
+
if (options.force || !existsSync(resolve(root, GUARD_WAIVERS_FILE))) {
|
|
1756
|
+
writeGuardWaivers(root);
|
|
1757
|
+
}
|
|
1598
1758
|
if (options.force || !existsSync(resolve(root, GUARD_AGENT_FILE))) {
|
|
1599
1759
|
writeGuardAgentConfig(root);
|
|
1600
1760
|
}
|
|
@@ -5,6 +5,28 @@ export type GuardRuleStatus = 'active' | 'proposed';
|
|
|
5
5
|
export type GuardContractKind = 'guidance' | 'import-boundary' | 'required-call' | 'package-boundary';
|
|
6
6
|
export type GuardBudgetMetric = 'lines' | 'bytes' | 'imports';
|
|
7
7
|
export type GuardToolMode = 'auto' | 'on' | 'off';
|
|
8
|
+
export interface GuardWaiver {
|
|
9
|
+
id: string;
|
|
10
|
+
fingerprint: string;
|
|
11
|
+
ruleId: string;
|
|
12
|
+
owner: string;
|
|
13
|
+
reason: string;
|
|
14
|
+
createdAt: string;
|
|
15
|
+
expiresAt: string;
|
|
16
|
+
issue?: string | undefined;
|
|
17
|
+
approvedBy?: string | undefined;
|
|
18
|
+
}
|
|
19
|
+
export interface GuardWaiverPolicy {
|
|
20
|
+
warningDays: number;
|
|
21
|
+
maxDays?: number | undefined;
|
|
22
|
+
}
|
|
23
|
+
export interface GuardWaiverDecision {
|
|
24
|
+
at: string;
|
|
25
|
+
action: 'add' | 'renew' | 'remove';
|
|
26
|
+
waiverId: string;
|
|
27
|
+
reason: string;
|
|
28
|
+
actor?: string | undefined;
|
|
29
|
+
}
|
|
8
30
|
export type GuardApprovalMode = 'local' | 'protected';
|
|
9
31
|
export type GuardAdapterName = 'dependency-graph' | 'security' | 'dependency-hygiene' | 'sast' | 'secret-scanning' | 'dependency-audit';
|
|
10
32
|
export type ProjectCheck = 'lint' | 'typecheck' | 'test' | 'build';
|
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import type { GuardFinding } from '../guard.js';
|
|
2
|
+
export interface GuardSarifNotice {
|
|
3
|
+
ruleId: string;
|
|
4
|
+
level: 'error' | 'warning' | 'note';
|
|
5
|
+
message: string;
|
|
6
|
+
}
|
|
2
7
|
interface SarifResult {
|
|
3
8
|
ruleId: string;
|
|
4
9
|
level: 'error' | 'warning' | 'note';
|
|
5
10
|
message: {
|
|
6
11
|
text: string;
|
|
7
12
|
};
|
|
8
|
-
locations
|
|
13
|
+
locations?: [
|
|
9
14
|
{
|
|
10
15
|
physicalLocation: {
|
|
11
16
|
artifactLocation: {
|
|
@@ -31,5 +36,5 @@ export interface SarifLog {
|
|
|
31
36
|
results: SarifResult[];
|
|
32
37
|
}];
|
|
33
38
|
}
|
|
34
|
-
export declare function guardFindingsToSarif(findings: GuardFinding[]): SarifLog;
|
|
39
|
+
export declare function guardFindingsToSarif(findings: GuardFinding[], notices?: GuardSarifNotice[]): SarifLog;
|
|
35
40
|
export {};
|
|
@@ -1,4 +1,25 @@
|
|
|
1
|
-
export function guardFindingsToSarif(findings) {
|
|
1
|
+
export function guardFindingsToSarif(findings, notices = []) {
|
|
2
|
+
const findingResults = findings.map((finding) => ({
|
|
3
|
+
ruleId: finding.ruleId,
|
|
4
|
+
level: finding.severity === 'info' ? 'note' : finding.severity,
|
|
5
|
+
message: { text: finding.message },
|
|
6
|
+
locations: [
|
|
7
|
+
{
|
|
8
|
+
physicalLocation: {
|
|
9
|
+
artifactLocation: { uri: finding.file },
|
|
10
|
+
region: { startLine: finding.line },
|
|
11
|
+
},
|
|
12
|
+
},
|
|
13
|
+
],
|
|
14
|
+
}));
|
|
15
|
+
const results = [
|
|
16
|
+
...findingResults,
|
|
17
|
+
...notices.map((notice) => ({
|
|
18
|
+
ruleId: notice.ruleId,
|
|
19
|
+
level: notice.level,
|
|
20
|
+
message: { text: notice.message },
|
|
21
|
+
})),
|
|
22
|
+
];
|
|
2
23
|
return {
|
|
3
24
|
version: '2.1.0',
|
|
4
25
|
$schema: 'https://json.schemastore.org/sarif-2.1.0.json',
|
|
@@ -10,19 +31,7 @@ export function guardFindingsToSarif(findings) {
|
|
|
10
31
|
informationUri: 'https://codapult.dev/docs/developer-tools/guard',
|
|
11
32
|
},
|
|
12
33
|
},
|
|
13
|
-
results
|
|
14
|
-
ruleId: finding.ruleId,
|
|
15
|
-
level: finding.severity === 'info' ? 'note' : finding.severity,
|
|
16
|
-
message: { text: finding.message },
|
|
17
|
-
locations: [
|
|
18
|
-
{
|
|
19
|
-
physicalLocation: {
|
|
20
|
-
artifactLocation: { uri: finding.file },
|
|
21
|
-
region: { startLine: finding.line },
|
|
22
|
-
},
|
|
23
|
-
},
|
|
24
|
-
],
|
|
25
|
-
})),
|
|
34
|
+
results,
|
|
26
35
|
},
|
|
27
36
|
],
|
|
28
37
|
};
|
|
@@ -1,4 +1,51 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
|
+
export declare const guardWaiverSchema: z.ZodObject<{
|
|
3
|
+
id: z.ZodString;
|
|
4
|
+
fingerprint: z.ZodString;
|
|
5
|
+
ruleId: z.ZodString;
|
|
6
|
+
owner: z.ZodString;
|
|
7
|
+
reason: z.ZodString;
|
|
8
|
+
createdAt: z.ZodISODateTime;
|
|
9
|
+
expiresAt: z.ZodISODateTime;
|
|
10
|
+
issue: z.ZodOptional<z.ZodURL>;
|
|
11
|
+
approvedBy: z.ZodOptional<z.ZodString>;
|
|
12
|
+
}, z.core.$strip>;
|
|
13
|
+
export declare const guardWaiverDecisionSchema: z.ZodObject<{
|
|
14
|
+
at: z.ZodISODateTime;
|
|
15
|
+
action: z.ZodEnum<{
|
|
16
|
+
add: "add";
|
|
17
|
+
remove: "remove";
|
|
18
|
+
renew: "renew";
|
|
19
|
+
}>;
|
|
20
|
+
waiverId: z.ZodString;
|
|
21
|
+
reason: z.ZodString;
|
|
22
|
+
actor: z.ZodOptional<z.ZodString>;
|
|
23
|
+
}, z.core.$strip>;
|
|
24
|
+
export declare const guardWaiversFileSchema: z.ZodObject<{
|
|
25
|
+
version: z.ZodLiteral<1>;
|
|
26
|
+
waivers: z.ZodArray<z.ZodObject<{
|
|
27
|
+
id: z.ZodString;
|
|
28
|
+
fingerprint: z.ZodString;
|
|
29
|
+
ruleId: z.ZodString;
|
|
30
|
+
owner: z.ZodString;
|
|
31
|
+
reason: z.ZodString;
|
|
32
|
+
createdAt: z.ZodISODateTime;
|
|
33
|
+
expiresAt: z.ZodISODateTime;
|
|
34
|
+
issue: z.ZodOptional<z.ZodURL>;
|
|
35
|
+
approvedBy: z.ZodOptional<z.ZodString>;
|
|
36
|
+
}, z.core.$strip>>;
|
|
37
|
+
decisions: z.ZodArray<z.ZodObject<{
|
|
38
|
+
at: z.ZodISODateTime;
|
|
39
|
+
action: z.ZodEnum<{
|
|
40
|
+
add: "add";
|
|
41
|
+
remove: "remove";
|
|
42
|
+
renew: "renew";
|
|
43
|
+
}>;
|
|
44
|
+
waiverId: z.ZodString;
|
|
45
|
+
reason: z.ZodString;
|
|
46
|
+
actor: z.ZodOptional<z.ZodString>;
|
|
47
|
+
}, z.core.$strip>>;
|
|
48
|
+
}, z.core.$strip>;
|
|
2
49
|
export declare const guardBudgetSchema: z.ZodObject<{
|
|
3
50
|
id: z.ZodString;
|
|
4
51
|
description: z.ZodString;
|
|
@@ -175,6 +222,10 @@ export declare const guardConfigSchema: z.ZodObject<{
|
|
|
175
222
|
allowMcpApproval: z.ZodBoolean;
|
|
176
223
|
requireDistinctActor: z.ZodBoolean;
|
|
177
224
|
}, z.core.$strip>>;
|
|
225
|
+
waiverPolicy: z.ZodOptional<z.ZodObject<{
|
|
226
|
+
warningDays: z.ZodNumber;
|
|
227
|
+
maxDays: z.ZodOptional<z.ZodNumber>;
|
|
228
|
+
}, z.core.$strip>>;
|
|
178
229
|
}, z.core.$strip>;
|
|
179
230
|
export declare const guardContractsFileSchema: z.ZodObject<{
|
|
180
231
|
version: z.ZodLiteral<1>;
|
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
const severity = z.enum(['error', 'warning', 'info']);
|
|
3
3
|
const status = z.enum(['active', 'proposed']);
|
|
4
|
+
export const guardWaiverSchema = z.object({
|
|
5
|
+
id: z.string().trim().min(1),
|
|
6
|
+
fingerprint: z.string().trim().min(1),
|
|
7
|
+
ruleId: z.string().trim().min(1),
|
|
8
|
+
owner: z.string().trim().min(1),
|
|
9
|
+
reason: z.string().trim().min(1),
|
|
10
|
+
createdAt: z.iso.datetime({ offset: true }),
|
|
11
|
+
expiresAt: z.iso.datetime({ offset: true }),
|
|
12
|
+
issue: z.url().optional(),
|
|
13
|
+
approvedBy: z.string().trim().min(1).optional(),
|
|
14
|
+
});
|
|
15
|
+
export const guardWaiverDecisionSchema = z.object({
|
|
16
|
+
at: z.iso.datetime({ offset: true }),
|
|
17
|
+
action: z.enum(['add', 'renew', 'remove']),
|
|
18
|
+
waiverId: z.string().trim().min(1),
|
|
19
|
+
reason: z.string().trim().min(1),
|
|
20
|
+
actor: z.string().trim().min(1).optional(),
|
|
21
|
+
});
|
|
22
|
+
export const guardWaiversFileSchema = z.object({
|
|
23
|
+
version: z.literal(1),
|
|
24
|
+
waivers: z.array(guardWaiverSchema),
|
|
25
|
+
decisions: z.array(guardWaiverDecisionSchema),
|
|
26
|
+
});
|
|
4
27
|
export const guardBudgetSchema = z.object({
|
|
5
28
|
id: z.string().trim().min(1),
|
|
6
29
|
description: z.string().trim().min(1),
|
|
@@ -56,6 +79,16 @@ export const guardConfigSchema = z.object({
|
|
|
56
79
|
requireDistinctActor: z.boolean(),
|
|
57
80
|
})
|
|
58
81
|
.optional(),
|
|
82
|
+
waiverPolicy: z
|
|
83
|
+
.object({
|
|
84
|
+
warningDays: z.number().int().min(0).max(3650),
|
|
85
|
+
maxDays: z.number().int().positive().max(3650).optional(),
|
|
86
|
+
})
|
|
87
|
+
.refine((policy) => policy.maxDays === undefined || policy.maxDays >= policy.warningDays, {
|
|
88
|
+
message: 'waiverPolicy.maxDays must be greater than or equal to warningDays',
|
|
89
|
+
path: ['maxDays'],
|
|
90
|
+
})
|
|
91
|
+
.optional(),
|
|
59
92
|
});
|
|
60
93
|
export const guardContractsFileSchema = z.object({
|
|
61
94
|
version: z.literal(1),
|
|
@@ -93,7 +93,8 @@ export function runGuardVerification(root, options = {}) {
|
|
|
93
93
|
const missingRequiredTools = options.strict &&
|
|
94
94
|
[...Object.values(results), ...Object.values(adapters)].some((result) => result.status === 'not-configured');
|
|
95
95
|
const architectureFailed = architecture.findings.some((finding) => finding.severity === 'error');
|
|
96
|
-
const architectureWarnings = architecture.findings.some((finding) => finding.severity === 'warning')
|
|
96
|
+
const architectureWarnings = architecture.findings.some((finding) => finding.severity === 'warning') ||
|
|
97
|
+
(architecture.waiverWarnings ?? []).length > 0;
|
|
97
98
|
const failed = (projectChecks && !runtime.compatible) ||
|
|
98
99
|
commandFailed ||
|
|
99
100
|
workspaceCommandFailed ||
|
package/dist/mcp/resources.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { buildArchitectureMemory, buildConventionsMemory, discoverProject, findGuardRoot, GUARD_ARCHITECTURE_FILE, GUARD_CONVENTIONS_FILE, loadGuardAgentConfig, loadGuardArtifact, loadGuardConfig, loadGuardProposals, GuardStateBusyError, GuardStateStaleError, } from '../core/guard.js';
|
|
1
|
+
import { buildArchitectureMemory, buildConventionsMemory, discoverProject, findGuardRoot, GUARD_ARCHITECTURE_FILE, GUARD_CONVENTIONS_FILE, loadGuardAgentConfig, loadGuardArtifact, loadGuardConfig, loadGuardProposals, loadGuardWaivers, GuardStateBusyError, GuardStateStaleError, } from '../core/guard.js';
|
|
2
2
|
import { guardErrorPayload } from '../core/errors.js';
|
|
3
3
|
import { DiscoveryStaleError } from '../core/discovery/discovery.js';
|
|
4
4
|
import { listGuardRuns, summarizeGuardRuns } from '../core/history/runs.js';
|
|
@@ -97,6 +97,23 @@ export function registerGuardResources(server) {
|
|
|
97
97
|
return resourceError('codapult://guard/architecture', error);
|
|
98
98
|
}
|
|
99
99
|
});
|
|
100
|
+
server.registerResource('codapult_guard_waivers', 'codapult://guard/waivers', {
|
|
101
|
+
title: 'Guard Waivers',
|
|
102
|
+
description: 'Active and expired time-bounded exceptions to Guard findings.',
|
|
103
|
+
mimeType: 'application/json',
|
|
104
|
+
}, () => {
|
|
105
|
+
const root = findGuardRoot();
|
|
106
|
+
try {
|
|
107
|
+
return jsonResource('codapult://guard/waivers', {
|
|
108
|
+
version: 1,
|
|
109
|
+
root,
|
|
110
|
+
waivers: loadGuardWaivers(root),
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
return resourceError('codapult://guard/waivers', error);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
100
117
|
server.registerResource('codapult_guard_proposals', 'codapult://guard/proposals', {
|
|
101
118
|
title: 'Architecture Guard Proposals',
|
|
102
119
|
description: 'Evidence-backed rules and contracts awaiting review.',
|
package/dist/mcp/tools/guard.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { buildArchitectureMemory, buildConventionsMemory, buildGeneratedGuardConfig, buildGuardProposals, buildGuardReviewPacket, discoverProject, discoverProjectWithMetrics, fingerprintProjectModel, findGuardRoot, GUARD_ARCHITECTURE_FILE, GUARD_CONVENTIONS_FILE, loadBaseline, loadGuardArtifact, loadGuardConfig, loadGuardAgentConfig, loadGuardProposals, loadGuardWaivers, getPendingGuardProposals, getGuardProposalFreshness, loadProjectModel, writeGuardProposals, initializeGuard, GuardAlreadyInitializedError, GuardStateBusyError, GuardStateStaleError, applyGuardProposalDecision, validateGuardProposalApproval, writeProjectState, scanGuard, updateGuardWaivers, validateGuardPolicy, classifyGuardOutcome, } from '../../core/guard.js';
|
|
2
3
|
import { guardErrorPayload } from '../../core/errors.js';
|
|
3
4
|
import { analyzeProjectImpact } from '../../core/analysis/impact.js';
|
|
4
5
|
import { z } from 'zod';
|
|
@@ -13,6 +14,12 @@ function jsonToolResult(value, isError = false) {
|
|
|
13
14
|
...(isError ? { isError: true } : {}),
|
|
14
15
|
};
|
|
15
16
|
}
|
|
17
|
+
function waiverToolError(error) {
|
|
18
|
+
return jsonToolResult({
|
|
19
|
+
status: 'invalid',
|
|
20
|
+
message: error instanceof Error ? error.message : 'Waiver update failed.',
|
|
21
|
+
}, true);
|
|
22
|
+
}
|
|
16
23
|
function notConfiguredToolResult() {
|
|
17
24
|
return jsonToolResult({
|
|
18
25
|
status: 'error',
|
|
@@ -563,6 +570,110 @@ export function registerGuardTools(server) {
|
|
|
563
570
|
...report,
|
|
564
571
|
}, errors > 0 || contractIssues.length > 0);
|
|
565
572
|
});
|
|
573
|
+
server.registerTool('codapult_guard_waivers', {
|
|
574
|
+
title: 'Guard Waivers',
|
|
575
|
+
description: 'List and manage time-bounded, project-approved exceptions to active Guard findings.',
|
|
576
|
+
inputSchema: {
|
|
577
|
+
root: rootSchema,
|
|
578
|
+
action: z.enum(['list', 'add', 'remove', 'renew']).default('list'),
|
|
579
|
+
fingerprint: z.string().trim().min(1).optional(),
|
|
580
|
+
id: z.string().trim().min(1).optional(),
|
|
581
|
+
owner: z.string().trim().min(1).optional(),
|
|
582
|
+
reason: z.string().trim().min(1).max(2000).optional(),
|
|
583
|
+
expires_at: z.iso.datetime({ offset: true }).optional(),
|
|
584
|
+
issue: z.url().optional(),
|
|
585
|
+
confirm: z.boolean().default(false),
|
|
586
|
+
},
|
|
587
|
+
}, ({ root: requestedRoot, action, fingerprint, id, owner, reason, expires_at, issue, confirm, }) => {
|
|
588
|
+
const root = getGuardRoot(requestedRoot);
|
|
589
|
+
let waivers;
|
|
590
|
+
try {
|
|
591
|
+
waivers = loadGuardWaivers(root);
|
|
592
|
+
}
|
|
593
|
+
catch (error) {
|
|
594
|
+
return waiverToolError(error);
|
|
595
|
+
}
|
|
596
|
+
if (action === 'list')
|
|
597
|
+
return jsonToolResult({ status: 'ok', waivers });
|
|
598
|
+
const loaded = loadConfigSafely(root);
|
|
599
|
+
if (loaded.error)
|
|
600
|
+
return loaded.error;
|
|
601
|
+
if (loaded.config?.approval?.mode === 'protected' &&
|
|
602
|
+
!loaded.config.approval.allowMcpApproval) {
|
|
603
|
+
return jsonToolResult({
|
|
604
|
+
status: 'approval-required',
|
|
605
|
+
message: 'Protected Guard policy requires waiver approval outside the MCP agent surface.',
|
|
606
|
+
action,
|
|
607
|
+
}, true);
|
|
608
|
+
}
|
|
609
|
+
if (loaded.config?.approval?.mode === 'protected' &&
|
|
610
|
+
loaded.config.approval.requireDistinctActor &&
|
|
611
|
+
!process.env.GUARD_APPROVER?.trim()) {
|
|
612
|
+
return jsonToolResult({
|
|
613
|
+
status: 'approval-required',
|
|
614
|
+
message: 'Protected Guard policy requires GUARD_APPROVER for waiver changes.',
|
|
615
|
+
action,
|
|
616
|
+
}, true);
|
|
617
|
+
}
|
|
618
|
+
if (!confirm)
|
|
619
|
+
return jsonToolResult({ status: 'needs-confirmation', action });
|
|
620
|
+
if (!reason)
|
|
621
|
+
return jsonToolResult({ status: 'invalid', message: 'reason is required.' }, true);
|
|
622
|
+
if (action === 'add') {
|
|
623
|
+
if (!fingerprint || !owner || !expires_at) {
|
|
624
|
+
return jsonToolResult({ status: 'invalid', message: 'fingerprint, owner, and expires_at are required.' }, true);
|
|
625
|
+
}
|
|
626
|
+
const finding = scanGuard(root, loaded.config ?? { version: 1, rules: [] }, {
|
|
627
|
+
waivers: [],
|
|
628
|
+
includeArchitectureInsights: true,
|
|
629
|
+
}).findings.find((candidate) => candidate.fingerprint === fingerprint);
|
|
630
|
+
if (!finding)
|
|
631
|
+
return jsonToolResult({ status: 'not-found', fingerprint }, true);
|
|
632
|
+
const waiver = {
|
|
633
|
+
id: `waiver-${randomUUID()}`,
|
|
634
|
+
fingerprint,
|
|
635
|
+
ruleId: finding.ruleId,
|
|
636
|
+
owner,
|
|
637
|
+
reason,
|
|
638
|
+
createdAt: new Date().toISOString(),
|
|
639
|
+
expiresAt: expires_at,
|
|
640
|
+
...(issue ? { issue } : {}),
|
|
641
|
+
...(process.env.GUARD_APPROVER?.trim()
|
|
642
|
+
? { approvedBy: process.env.GUARD_APPROVER.trim() }
|
|
643
|
+
: {}),
|
|
644
|
+
};
|
|
645
|
+
try {
|
|
646
|
+
updateGuardWaivers(root, {
|
|
647
|
+
action,
|
|
648
|
+
waiverId: waiver.id,
|
|
649
|
+
waiver,
|
|
650
|
+
reason,
|
|
651
|
+
actor: process.env.GUARD_APPROVER,
|
|
652
|
+
maxLifetimeDays: loaded.config?.waiverPolicy?.maxDays,
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
catch (error) {
|
|
656
|
+
return waiverToolError(error);
|
|
657
|
+
}
|
|
658
|
+
return jsonToolResult({ status: 'ok', action, waiver });
|
|
659
|
+
}
|
|
660
|
+
if (!id)
|
|
661
|
+
return jsonToolResult({ status: 'invalid', message: 'id is required.' }, true);
|
|
662
|
+
try {
|
|
663
|
+
updateGuardWaivers(root, {
|
|
664
|
+
action,
|
|
665
|
+
waiverId: id,
|
|
666
|
+
...(expires_at ? { expiresAt: expires_at } : {}),
|
|
667
|
+
reason,
|
|
668
|
+
actor: process.env.GUARD_APPROVER,
|
|
669
|
+
maxLifetimeDays: loaded.config?.waiverPolicy?.maxDays,
|
|
670
|
+
});
|
|
671
|
+
}
|
|
672
|
+
catch (error) {
|
|
673
|
+
return waiverToolError(error);
|
|
674
|
+
}
|
|
675
|
+
return jsonToolResult({ status: 'ok', action, id });
|
|
676
|
+
});
|
|
566
677
|
server.registerTool('codapult_guard_impact', {
|
|
567
678
|
title: 'Guard Impact Analysis',
|
|
568
679
|
description: 'Explain the project impact of one or more changed files using the discovered dependency graph, routes, capabilities, and contracts.',
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codapult/guard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"mcpName": "io.github.codapult/guard",
|
|
5
5
|
"description": "Local-first architecture guardrails for JavaScript and TypeScript projects, with first-class support for Next.js SaaS and AI-assisted development",
|
|
6
6
|
"license": "MIT",
|
|
@@ -57,6 +57,7 @@
|
|
|
57
57
|
"release:check": "pnpm lint && pnpm format:check && pnpm typecheck && pnpm test && pnpm build && npm pack --dry-run && pnpm mcp:validate",
|
|
58
58
|
"mcp:validate": "node scripts/mcp-registry.mjs validate",
|
|
59
59
|
"mcp:sync": "node scripts/mcp-registry.mjs sync",
|
|
60
|
+
"mcp:wait": "node scripts/mcp-registry.mjs wait",
|
|
60
61
|
"test": "vitest run",
|
|
61
62
|
"test:coverage": "vitest run --coverage",
|
|
62
63
|
"test:guard:coverage": "vitest run src/core src/adapters/project-checks.test.ts --coverage --coverage.include='src/core/**/*.ts' --coverage.include='src/adapters/project-checks.ts'",
|