@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.
- package/README.md +105 -10
- package/dist/adapters/agents/agent-integration.d.ts +3 -0
- package/dist/adapters/agents/agent-integration.js +29 -8
- package/dist/cli/commands/guard.d.ts +13 -0
- package/dist/cli/commands/guard.js +163 -4
- package/dist/cli/index.js +37 -4
- package/dist/core/analysis/doctor.d.ts +1 -1
- package/dist/core/analysis/doctor.js +35 -2
- package/dist/core/audit/governance.d.ts +37 -0
- package/dist/core/audit/governance.js +166 -0
- package/dist/core/errors.d.ts +1 -1
- package/dist/core/guard.d.ts +55 -2
- package/dist/core/guard.js +319 -8
- package/dist/core/history/runs.d.ts +10 -1
- package/dist/core/history/runs.js +18 -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 +59 -0
- package/dist/core/policy/schemas.js +37 -0
- package/dist/core/verification/verify.d.ts +6 -2
- package/dist/core/verification/verify.js +114 -8
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/mcp/resources.js +18 -1
- package/dist/mcp/tools/guard.js +138 -2
- package/package.json +1 -1
|
@@ -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
|
-
|
|
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:
|
|
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';
|
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,8 +1,10 @@
|
|
|
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';
|
|
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.
|
|
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",
|