@codapult/guard 0.6.2 → 0.8.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.
@@ -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>;
@@ -215,6 +266,7 @@ export declare const guardContractsFileSchema: z.ZodObject<{
215
266
  }, z.core.$strip>>;
216
267
  }, z.core.$strip>;
217
268
  export declare const guardProposalDecisionSchema: z.ZodObject<{
269
+ decisionId: z.ZodOptional<z.ZodString>;
218
270
  id: z.ZodString;
219
271
  type: z.ZodEnum<{
220
272
  contract: "contract";
@@ -233,8 +285,11 @@ export declare const guardProposalDecisionSchema: z.ZodObject<{
233
285
  external: "external";
234
286
  mcp: "mcp";
235
287
  }>>;
288
+ proposalAuthor: z.ZodOptional<z.ZodString>;
236
289
  actor: z.ZodOptional<z.ZodString>;
237
290
  commit: z.ZodOptional<z.ZodString>;
291
+ policyFingerprint: z.ZodOptional<z.ZodString>;
292
+ policyRevision: z.ZodOptional<z.ZodNumber>;
238
293
  }, z.core.$strip>;
239
294
  export declare const guardProposalSchema: z.ZodObject<{
240
295
  version: z.ZodLiteral<1>;
@@ -306,6 +361,7 @@ export declare const guardProposalSchema: z.ZodObject<{
306
361
  }, z.core.$strip>>;
307
362
  questions: z.ZodArray<z.ZodString>;
308
363
  decisions: z.ZodOptional<z.ZodArray<z.ZodObject<{
364
+ decisionId: z.ZodOptional<z.ZodString>;
309
365
  id: z.ZodString;
310
366
  type: z.ZodEnum<{
311
367
  contract: "contract";
@@ -324,8 +380,11 @@ export declare const guardProposalSchema: z.ZodObject<{
324
380
  external: "external";
325
381
  mcp: "mcp";
326
382
  }>>;
383
+ proposalAuthor: z.ZodOptional<z.ZodString>;
327
384
  actor: z.ZodOptional<z.ZodString>;
328
385
  commit: z.ZodOptional<z.ZodString>;
386
+ policyFingerprint: z.ZodOptional<z.ZodString>;
387
+ policyRevision: z.ZodOptional<z.ZodNumber>;
329
388
  }, z.core.$strip>>>;
330
389
  }, z.core.$strip>;
331
390
  export declare const guardAgentConfigSchema: z.ZodObject<{
@@ -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,12 +79,23 @@ 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),
62
95
  contracts: z.array(guardContractSchema),
63
96
  });
64
97
  export const guardProposalDecisionSchema = z.object({
98
+ decisionId: z.string().trim().min(1).optional(),
65
99
  id: z.string().trim().min(1),
66
100
  type: z.enum(['rule', 'contract']),
67
101
  decision: z.enum(['approved', 'rejected']),
@@ -70,8 +104,11 @@ export const guardProposalDecisionSchema = z.object({
70
104
  proposalFingerprint: z.string().optional(),
71
105
  revision: z.number().optional(),
72
106
  source: z.enum(['cli', 'mcp', 'external']).optional(),
107
+ proposalAuthor: z.string().trim().min(1).optional(),
73
108
  actor: z.string().optional(),
74
109
  commit: z.string().optional(),
110
+ policyFingerprint: z.string().trim().min(1).optional(),
111
+ policyRevision: z.number().int().nonnegative().optional(),
75
112
  });
76
113
  export const guardProposalSchema = z.object({
77
114
  version: z.literal(1),
@@ -3,7 +3,7 @@ import type { ProjectCheck } from '../model/types.js';
3
3
  import { type GuardAdapter, type ProjectCheckResults, type ProjectRuntimeDiagnostics } from '../../adapters/project-checks.js';
4
4
  import { type GuardToolMode, type GuardContractIssue, type GuardReport } from '../guard.js';
5
5
  import { type GuardImpactAnalysis } from '../analysis/impact.js';
6
- import { type GuardRunManifest } from '../history/runs.js';
6
+ import { type GuardRunManifest, type GuardRunPolicy } from '../history/runs.js';
7
7
  export type GuardVerificationCheck = ProjectCheck;
8
8
  export interface GuardVerificationOptions {
9
9
  checks?: GuardVerificationCheck[] | undefined;
@@ -13,11 +13,13 @@ export interface GuardVerificationOptions {
13
13
  tools?: GuardToolMode | undefined;
14
14
  strict?: boolean | undefined;
15
15
  projectChecks?: boolean | undefined;
16
+ policyBase?: string | undefined;
17
+ failOnPolicyChange?: boolean | undefined;
16
18
  }
17
19
  export interface GuardVerificationResult {
18
20
  status: 'ok' | 'fail' | 'not-configured';
19
21
  outcome: 'pass' | 'fail' | 'warning' | 'needs-review' | 'not-configured';
20
- errorCode?: 'GUARD_NOT_CONFIGURED' | 'GUARD_CONFIG_INVALID' | undefined;
22
+ errorCode?: 'GUARD_NOT_CONFIGURED' | 'GUARD_CONFIG_INVALID' | 'GUARD_POLICY_BASE_INVALID' | 'GUARD_POLICY_CHANGED' | undefined;
21
23
  recoverable?: boolean | undefined;
22
24
  tools?: GuardToolMode | undefined;
23
25
  checks: ProjectCheckResults;
@@ -34,5 +36,7 @@ export interface GuardVerificationResult {
34
36
  message: string;
35
37
  };
36
38
  run: GuardRunManifest;
39
+ policy?: GuardRunPolicy | undefined;
40
+ policyChanged?: boolean | undefined;
37
41
  }
38
42
  export declare function runGuardVerification(root: string, options?: GuardVerificationOptions): GuardVerificationResult;
@@ -1,9 +1,19 @@
1
1
  import {} from '../../adapters/command.js';
2
2
  import { runProjectAdapters, runProjectChecks, runWorkspaceProjectChecks, inspectProjectRuntime, } from '../../adapters/project-checks.js';
3
- import { loadBaseline, loadGuardAgentConfig, loadGuardConfig, scanGuard, classifyGuardOutcome, validateGuardPolicy, } from '../guard.js';
3
+ import { loadBaseline, loadGuardPolicyAtRevision, createGuardPolicySnapshot, loadGuardWaivers, loadGuardAgentConfig, loadGuardConfig, loadGuardProposals, scanGuard, classifyGuardOutcome, validateGuardPolicy, } from '../guard.js';
4
4
  import { analyzeProjectImpact } from '../analysis/impact.js';
5
5
  import { discoverProject } from '../discovery/discovery.js';
6
6
  import { finishGuardRun, recordGuardRunStage, startGuardRun, } from '../history/runs.js';
7
+ function buildRunPolicy(snapshot, base, changedFromBase, decisionIds = []) {
8
+ return {
9
+ ...(snapshot.revision !== undefined ? { revision: snapshot.revision } : {}),
10
+ fingerprint: snapshot.fingerprint,
11
+ source: snapshot.source,
12
+ ...(base ? { ref: base } : {}),
13
+ ...(base ? { changedFromBase } : {}),
14
+ ...(decisionIds.length > 0 ? { decisionIds } : {}),
15
+ };
16
+ }
7
17
  export function runGuardVerification(root, options = {}) {
8
18
  const runContext = startGuardRun();
9
19
  const requirement = {
@@ -16,6 +26,23 @@ export function runGuardVerification(root, options = {}) {
16
26
  const runtimeStartedAt = Date.now();
17
27
  const runtime = inspectProjectRuntime(root);
18
28
  recordGuardRunStage(runContext, 'runtime', 'ok', runtimeStartedAt);
29
+ if (options.failOnPolicyChange && !options.policyBase) {
30
+ const run = finishGuardRun(root, runContext, 'fail', 'policy-base');
31
+ return {
32
+ status: 'fail',
33
+ outcome: 'fail',
34
+ errorCode: 'GUARD_POLICY_BASE_INVALID',
35
+ recoverable: true,
36
+ checks: {},
37
+ workspaceChecks: {},
38
+ adapters: {},
39
+ runtime,
40
+ configError: '`failOnPolicyChange` requires `policyBase`.',
41
+ contractIssues: [],
42
+ requirement,
43
+ run,
44
+ };
45
+ }
19
46
  let config;
20
47
  try {
21
48
  config = loadGuardConfig(root);
@@ -53,7 +80,82 @@ export function runGuardVerification(root, options = {}) {
53
80
  run,
54
81
  };
55
82
  }
56
- const agentConfig = loadGuardAgentConfig(root);
83
+ let policySnapshot;
84
+ let workingTreeSnapshot;
85
+ let policyChanged;
86
+ let policyDecisionIds = [];
87
+ try {
88
+ workingTreeSnapshot = createGuardPolicySnapshot(config, loadBaseline(root), loadGuardWaivers(root), 'working-tree', undefined, loadGuardAgentConfig(root));
89
+ const baseSnapshot = options.policyBase
90
+ ? loadGuardPolicyAtRevision(root, options.policyBase)
91
+ : undefined;
92
+ if (options.policyBase && !baseSnapshot) {
93
+ const run = finishGuardRun(root, runContext, 'fail', 'policy-base');
94
+ return {
95
+ status: 'fail',
96
+ outcome: 'fail',
97
+ errorCode: 'GUARD_POLICY_BASE_INVALID',
98
+ recoverable: true,
99
+ checks: {},
100
+ workspaceChecks: {},
101
+ adapters: {},
102
+ runtime,
103
+ configError: `Guard policy was not found at Git ref: ${options.policyBase}`,
104
+ contractIssues: [],
105
+ requirement,
106
+ run,
107
+ };
108
+ }
109
+ policySnapshot = baseSnapshot ?? workingTreeSnapshot;
110
+ policyChanged =
111
+ baseSnapshot !== undefined && baseSnapshot.fingerprint !== workingTreeSnapshot.fingerprint;
112
+ if (policySnapshot.source === 'working-tree') {
113
+ policyDecisionIds = (loadGuardProposals(root)?.decisions ?? [])
114
+ .filter((decision) => decision.policyFingerprint === policySnapshot.fingerprint)
115
+ .map((decision) => decision.decisionId ??
116
+ `${decision.proposalId ?? 'unknown'}:${decision.type}:${decision.id}`)
117
+ .sort();
118
+ }
119
+ if (options.failOnPolicyChange && policyChanged) {
120
+ const policy = buildRunPolicy(policySnapshot, options.policyBase, policyChanged, policyDecisionIds);
121
+ const run = finishGuardRun(root, runContext, 'fail', 'policy-change', 'verify', policy);
122
+ return {
123
+ status: 'fail',
124
+ outcome: 'fail',
125
+ errorCode: 'GUARD_POLICY_CHANGED',
126
+ recoverable: true,
127
+ checks: {},
128
+ workspaceChecks: {},
129
+ adapters: {},
130
+ runtime,
131
+ configError: 'Guard policy changed relative to the supplied Git base ref.',
132
+ contractIssues: [],
133
+ requirement,
134
+ run,
135
+ policy,
136
+ policyChanged,
137
+ };
138
+ }
139
+ }
140
+ catch (error) {
141
+ const run = finishGuardRun(root, runContext, 'fail', 'policy-base');
142
+ return {
143
+ status: 'fail',
144
+ outcome: 'fail',
145
+ errorCode: 'GUARD_POLICY_BASE_INVALID',
146
+ recoverable: true,
147
+ checks: {},
148
+ workspaceChecks: {},
149
+ adapters: {},
150
+ runtime,
151
+ configError: error instanceof Error ? error.message : String(error),
152
+ contractIssues: [],
153
+ requirement,
154
+ run,
155
+ };
156
+ }
157
+ const policy = buildRunPolicy(policySnapshot, options.policyBase, policyChanged, policyDecisionIds);
158
+ const agentConfig = policySnapshot.agentConfig;
57
159
  const projectChecks = options.projectChecks ?? agentConfig.completionGate.projectChecks;
58
160
  const checks = projectChecks ? (options.checks ?? agentConfig.completionGate.checks) : [];
59
161
  const checksStartedAt = Date.now();
@@ -63,7 +165,7 @@ export function runGuardVerification(root, options = {}) {
63
165
  const workspaceModel = discoverProject(root);
64
166
  const impact = analyzeProjectImpact(workspaceModel, options.changedOnly === false
65
167
  ? workspaceModel.modules.map((module) => module.path)
66
- : workspaceModel.git.changedFiles, config.contracts ?? []);
168
+ : workspaceModel.git.changedFiles, policySnapshot.config.contracts ?? []);
67
169
  const workspaceChecks = runWorkspaceProjectChecks(root, workspaceModel.project.workspacePackages ?? [], checks, {
68
170
  timeout: options.timeout,
69
171
  rootResults: results,
@@ -80,12 +182,13 @@ export function runGuardVerification(root, options = {}) {
80
182
  });
81
183
  recordGuardRunStage(runContext, 'adapters', toolMode === 'off' ? 'skipped' : 'ok', adaptersStartedAt);
82
184
  const architectureStartedAt = Date.now();
83
- const architecture = scanGuard(root, config, {
185
+ const architecture = scanGuard(root, policySnapshot.config, {
84
186
  changedOnly: options.changedOnly,
85
- baseline: loadBaseline(root),
187
+ baseline: policySnapshot.baseline,
86
188
  includeArchitectureInsights: true,
189
+ waivers: policySnapshot.waivers,
87
190
  });
88
- const contractIssues = validateGuardPolicy(root, config);
191
+ const contractIssues = validateGuardPolicy(root, policySnapshot.config);
89
192
  recordGuardRunStage(runContext, 'architecture-policy', 'ok', architectureStartedAt);
90
193
  const commandFailed = Object.values(results).some((result) => result.status === 'failed');
91
194
  const workspaceCommandFailed = Object.values(workspaceChecks).some((packageResults) => Object.values(packageResults).some((result) => result.status === 'failed'));
@@ -93,7 +196,8 @@ export function runGuardVerification(root, options = {}) {
93
196
  const missingRequiredTools = options.strict &&
94
197
  [...Object.values(results), ...Object.values(adapters)].some((result) => result.status === 'not-configured');
95
198
  const architectureFailed = architecture.findings.some((finding) => finding.severity === 'error');
96
- const architectureWarnings = architecture.findings.some((finding) => finding.severity === 'warning');
199
+ const architectureWarnings = architecture.findings.some((finding) => finding.severity === 'warning') ||
200
+ (architecture.waiverWarnings ?? []).length > 0;
97
201
  const failed = (projectChecks && !runtime.compatible) ||
98
202
  commandFailed ||
99
203
  workspaceCommandFailed ||
@@ -132,7 +236,7 @@ export function runGuardVerification(root, options = {}) {
132
236
  : !runtime.compatible && projectChecks
133
237
  ? 'runtime'
134
238
  : 'none';
135
- const run = finishGuardRun(root, runContext, outcome, gate);
239
+ const run = finishGuardRun(root, runContext, outcome, gate, 'verify', policy);
136
240
  return {
137
241
  status: failed ? 'fail' : 'ok',
138
242
  outcome,
@@ -146,5 +250,7 @@ export function runGuardVerification(root, options = {}) {
146
250
  contractIssues,
147
251
  requirement,
148
252
  run,
253
+ policy,
254
+ ...(policy.changedFromBase !== undefined ? { policyChanged: policy.changedFromBase } : {}),
149
255
  };
150
256
  }
package/dist/index.d.ts CHANGED
@@ -11,4 +11,5 @@ export * from './core/output/sarif.js';
11
11
  export * from './core/analysis/doctor.js';
12
12
  export * from './core/analysis/packs.js';
13
13
  export * from './core/analysis/impact.js';
14
+ export * from './core/audit/governance.js';
14
15
  export * from './adapters/project-checks.js';
package/dist/index.js CHANGED
@@ -10,4 +10,5 @@ export * from './core/output/sarif.js';
10
10
  export * from './core/analysis/doctor.js';
11
11
  export * from './core/analysis/packs.js';
12
12
  export * from './core/analysis/impact.js';
13
+ export * from './core/audit/governance.js';
13
14
  export * from './adapters/project-checks.js';
@@ -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.',
@@ -1,8 +1,10 @@
1
- import { buildArchitectureMemory, buildConventionsMemory, buildGeneratedGuardConfig, buildGuardProposals, buildGuardReviewPacket, discoverProject, discoverProjectWithMetrics, fingerprintProjectModel, findGuardRoot, GUARD_ARCHITECTURE_FILE, GUARD_CONVENTIONS_FILE, loadBaseline, loadGuardArtifact, loadGuardConfig, loadGuardAgentConfig, loadGuardProposals, getPendingGuardProposals, getGuardProposalFreshness, loadProjectModel, writeGuardProposals, initializeGuard, GuardAlreadyInitializedError, GuardStateBusyError, GuardStateStaleError, applyGuardProposalDecision, validateGuardProposalApproval, writeProjectState, scanGuard, validateGuardPolicy, classifyGuardOutcome, } from '../../core/guard.js';
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';
5
6
  import { runGuardVerification } from '../../core/verification/verify.js';
7
+ import { auditGuardGovernance } from '../../core/audit/governance.js';
6
8
  const rootSchema = z.string().trim().min(1).optional().describe('Project path or workspace root.');
7
9
  function getGuardRoot(root) {
8
10
  return root ? findGuardRoot(root) : findGuardRoot();
@@ -13,6 +15,12 @@ function jsonToolResult(value, isError = false) {
13
15
  ...(isError ? { isError: true } : {}),
14
16
  };
15
17
  }
18
+ function waiverToolError(error) {
19
+ return jsonToolResult({
20
+ status: 'invalid',
21
+ message: error instanceof Error ? error.message : 'Waiver update failed.',
22
+ }, true);
23
+ }
16
24
  function notConfiguredToolResult() {
17
25
  return jsonToolResult({
18
26
  status: 'error',
@@ -443,9 +451,19 @@ export function registerGuardTools(server) {
443
451
  .boolean()
444
452
  .optional()
445
453
  .describe('Override completionGate.projectChecks for this invocation.'),
454
+ policy_base: z
455
+ .string()
456
+ .trim()
457
+ .min(1)
458
+ .optional()
459
+ .describe('Git ref whose Guard policy should be used for this verification.'),
460
+ fail_on_policy_change: z
461
+ .boolean()
462
+ .optional()
463
+ .describe('Fail when the checked-out policy differs from policy_base.'),
446
464
  iteration: z.number().int().positive().default(1),
447
465
  },
448
- }, ({ root: requestedRoot, checks, changed_only, requirement, tools, strict, project_checks, iteration, }) => {
466
+ }, ({ root: requestedRoot, checks, changed_only, requirement, tools, strict, project_checks, policy_base, fail_on_policy_change, iteration, }) => {
449
467
  const root = getGuardRoot(requestedRoot);
450
468
  const agent = loadAgentSafely(root);
451
469
  if ('error' in agent)
@@ -457,6 +475,8 @@ export function registerGuardTools(server) {
457
475
  tools,
458
476
  strict,
459
477
  projectChecks: project_checks,
478
+ policyBase: policy_base,
479
+ failOnPolicyChange: fail_on_policy_change,
460
480
  });
461
481
  const completionGate = agent.config.completionGate;
462
482
  return jsonToolResult({
@@ -468,6 +488,18 @@ export function registerGuardTools(server) {
468
488
  },
469
489
  }, result.status === 'fail');
470
490
  });
491
+ server.registerTool('codapult_guard_governance', {
492
+ title: 'Guard Governance Audit',
493
+ description: 'Audit declared proposal and approval provenance, policy bindings, and retained verification reachability. This reports metadata; it does not prove identity or signatures.',
494
+ inputSchema: { root: rootSchema },
495
+ }, ({ root: requestedRoot }) => {
496
+ try {
497
+ return jsonToolResult(auditGuardGovernance(getGuardRoot(requestedRoot)));
498
+ }
499
+ catch (error) {
500
+ return jsonToolResult({ status: 'fail', message: error instanceof Error ? error.message : String(error) }, true);
501
+ }
502
+ });
471
503
  server.registerTool('codapult_guard_audit', {
472
504
  title: 'Guard Audit',
473
505
  description: 'Run a full Guard audit, ignoring baseline suppression and validating contracts.',
@@ -563,6 +595,110 @@ export function registerGuardTools(server) {
563
595
  ...report,
564
596
  }, errors > 0 || contractIssues.length > 0);
565
597
  });
598
+ server.registerTool('codapult_guard_waivers', {
599
+ title: 'Guard Waivers',
600
+ description: 'List and manage time-bounded, project-approved exceptions to active Guard findings.',
601
+ inputSchema: {
602
+ root: rootSchema,
603
+ action: z.enum(['list', 'add', 'remove', 'renew']).default('list'),
604
+ fingerprint: z.string().trim().min(1).optional(),
605
+ id: z.string().trim().min(1).optional(),
606
+ owner: z.string().trim().min(1).optional(),
607
+ reason: z.string().trim().min(1).max(2000).optional(),
608
+ expires_at: z.iso.datetime({ offset: true }).optional(),
609
+ issue: z.url().optional(),
610
+ confirm: z.boolean().default(false),
611
+ },
612
+ }, ({ root: requestedRoot, action, fingerprint, id, owner, reason, expires_at, issue, confirm, }) => {
613
+ const root = getGuardRoot(requestedRoot);
614
+ let waivers;
615
+ try {
616
+ waivers = loadGuardWaivers(root);
617
+ }
618
+ catch (error) {
619
+ return waiverToolError(error);
620
+ }
621
+ if (action === 'list')
622
+ return jsonToolResult({ status: 'ok', waivers });
623
+ const loaded = loadConfigSafely(root);
624
+ if (loaded.error)
625
+ return loaded.error;
626
+ if (loaded.config?.approval?.mode === 'protected' &&
627
+ !loaded.config.approval.allowMcpApproval) {
628
+ return jsonToolResult({
629
+ status: 'approval-required',
630
+ message: 'Protected Guard policy requires waiver approval outside the MCP agent surface.',
631
+ action,
632
+ }, true);
633
+ }
634
+ if (loaded.config?.approval?.mode === 'protected' &&
635
+ loaded.config.approval.requireDistinctActor &&
636
+ !process.env.GUARD_APPROVER?.trim()) {
637
+ return jsonToolResult({
638
+ status: 'approval-required',
639
+ message: 'Protected Guard policy requires GUARD_APPROVER for waiver changes.',
640
+ action,
641
+ }, true);
642
+ }
643
+ if (!confirm)
644
+ return jsonToolResult({ status: 'needs-confirmation', action });
645
+ if (!reason)
646
+ return jsonToolResult({ status: 'invalid', message: 'reason is required.' }, true);
647
+ if (action === 'add') {
648
+ if (!fingerprint || !owner || !expires_at) {
649
+ return jsonToolResult({ status: 'invalid', message: 'fingerprint, owner, and expires_at are required.' }, true);
650
+ }
651
+ const finding = scanGuard(root, loaded.config ?? { version: 1, rules: [] }, {
652
+ waivers: [],
653
+ includeArchitectureInsights: true,
654
+ }).findings.find((candidate) => candidate.fingerprint === fingerprint);
655
+ if (!finding)
656
+ return jsonToolResult({ status: 'not-found', fingerprint }, true);
657
+ const waiver = {
658
+ id: `waiver-${randomUUID()}`,
659
+ fingerprint,
660
+ ruleId: finding.ruleId,
661
+ owner,
662
+ reason,
663
+ createdAt: new Date().toISOString(),
664
+ expiresAt: expires_at,
665
+ ...(issue ? { issue } : {}),
666
+ ...(process.env.GUARD_APPROVER?.trim()
667
+ ? { approvedBy: process.env.GUARD_APPROVER.trim() }
668
+ : {}),
669
+ };
670
+ try {
671
+ updateGuardWaivers(root, {
672
+ action,
673
+ waiverId: waiver.id,
674
+ waiver,
675
+ reason,
676
+ actor: process.env.GUARD_APPROVER,
677
+ maxLifetimeDays: loaded.config?.waiverPolicy?.maxDays,
678
+ });
679
+ }
680
+ catch (error) {
681
+ return waiverToolError(error);
682
+ }
683
+ return jsonToolResult({ status: 'ok', action, waiver });
684
+ }
685
+ if (!id)
686
+ return jsonToolResult({ status: 'invalid', message: 'id is required.' }, true);
687
+ try {
688
+ updateGuardWaivers(root, {
689
+ action,
690
+ waiverId: id,
691
+ ...(expires_at ? { expiresAt: expires_at } : {}),
692
+ reason,
693
+ actor: process.env.GUARD_APPROVER,
694
+ maxLifetimeDays: loaded.config?.waiverPolicy?.maxDays,
695
+ });
696
+ }
697
+ catch (error) {
698
+ return waiverToolError(error);
699
+ }
700
+ return jsonToolResult({ status: 'ok', action, id });
701
+ });
566
702
  server.registerTool('codapult_guard_impact', {
567
703
  title: 'Guard Impact Analysis',
568
704
  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.6.2",
3
+ "version": "0.8.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",