@codapult/guard 0.2.0 → 0.4.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 +73 -1
- package/dist/adapters/command.d.ts +10 -8
- package/dist/adapters/command.js +23 -2
- package/dist/adapters/project-checks.d.ts +23 -20
- package/dist/cli/commands/guard.d.ts +53 -54
- package/dist/cli/commands/guard.js +155 -32
- package/dist/cli/index.js +41 -5
- package/dist/core/analysis/doctor.d.ts +1 -1
- package/dist/core/analysis/doctor.js +19 -6
- package/dist/core/discovery/discovery.d.ts +27 -13
- package/dist/core/discovery/discovery.js +182 -29
- package/dist/core/errors.d.ts +11 -0
- package/dist/core/errors.js +11 -0
- package/dist/core/guard.d.ts +134 -69
- package/dist/core/guard.js +767 -145
- package/dist/core/history/runs.d.ts +28 -0
- package/dist/core/history/runs.js +59 -0
- package/dist/core/model/types.d.ts +10 -0
- package/dist/core/model/types.js +1 -0
- package/dist/core/policy/schemas.d.ts +81 -0
- package/dist/core/policy/schemas.js +46 -17
- package/dist/core/verification/verify.d.ts +21 -12
- package/dist/core/verification/verify.js +81 -8
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -0
- package/dist/mcp/resources.js +64 -29
- package/dist/mcp/tools/guard.js +300 -52
- package/package.json +12 -7
package/dist/mcp/resources.js
CHANGED
|
@@ -1,9 +1,24 @@
|
|
|
1
|
-
import { buildArchitectureMemory, buildConventionsMemory, discoverProject, findGuardRoot, GUARD_ARCHITECTURE_FILE, GUARD_CONVENTIONS_FILE, loadGuardAgentConfig, loadGuardArtifact, loadGuardConfig, loadGuardProposals, } from '../core/guard.js';
|
|
1
|
+
import { buildArchitectureMemory, buildConventionsMemory, discoverProject, findGuardRoot, GUARD_ARCHITECTURE_FILE, GUARD_CONVENTIONS_FILE, loadGuardAgentConfig, loadGuardArtifact, loadGuardConfig, loadGuardProposals, GuardStateBusyError, GuardStateStaleError, } from '../core/guard.js';
|
|
2
|
+
import { guardErrorPayload } from '../core/errors.js';
|
|
3
|
+
import { DiscoveryStaleError } from '../core/discovery/discovery.js';
|
|
2
4
|
function jsonResource(uri, value) {
|
|
3
5
|
return {
|
|
4
6
|
contents: [{ uri, text: JSON.stringify(value, null, 2), mimeType: 'application/json' }],
|
|
5
7
|
};
|
|
6
8
|
}
|
|
9
|
+
function resourceError(uri, error) {
|
|
10
|
+
const errorCode = error instanceof GuardStateBusyError
|
|
11
|
+
? 'GUARD_STATE_BUSY'
|
|
12
|
+
: error instanceof GuardStateStaleError || error instanceof DiscoveryStaleError
|
|
13
|
+
? 'GUARD_STATE_STALE'
|
|
14
|
+
: 'GUARD_CONFIG_INVALID';
|
|
15
|
+
return jsonResource(uri, guardErrorPayload(errorCode, error instanceof Error ? error.message : String(error), {
|
|
16
|
+
configured: errorCode !== 'GUARD_CONFIG_INVALID',
|
|
17
|
+
outcome: 'error',
|
|
18
|
+
recoverable: true,
|
|
19
|
+
hint: 'Repair the invalid Guard artifact, then run codapult-guard doctor.',
|
|
20
|
+
}));
|
|
21
|
+
}
|
|
7
22
|
export function registerGuardResources(server) {
|
|
8
23
|
server.registerResource('codapult_guard_rules', 'codapult://guard/rules', {
|
|
9
24
|
title: 'Architecture Guard Rules',
|
|
@@ -11,13 +26,18 @@ export function registerGuardResources(server) {
|
|
|
11
26
|
mimeType: 'application/json',
|
|
12
27
|
}, () => {
|
|
13
28
|
const root = findGuardRoot();
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
29
|
+
try {
|
|
30
|
+
const config = loadGuardConfig(root);
|
|
31
|
+
return jsonResource('codapult://guard/rules', {
|
|
32
|
+
configured: config !== undefined,
|
|
33
|
+
rules: config?.rules ?? [],
|
|
34
|
+
contracts: config?.contracts ?? [],
|
|
35
|
+
project: discoverProject(root),
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
return resourceError('codapult://guard/rules', error);
|
|
40
|
+
}
|
|
21
41
|
});
|
|
22
42
|
server.registerResource('codapult_guard_context', 'codapult://guard/context', {
|
|
23
43
|
title: 'Guard Project Context',
|
|
@@ -25,19 +45,24 @@ export function registerGuardResources(server) {
|
|
|
25
45
|
mimeType: 'application/json',
|
|
26
46
|
}, () => {
|
|
27
47
|
const root = findGuardRoot();
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
48
|
+
try {
|
|
49
|
+
const project = discoverProject(root);
|
|
50
|
+
const config = loadGuardConfig(root);
|
|
51
|
+
return jsonResource('codapult://guard/context', {
|
|
52
|
+
version: 1,
|
|
53
|
+
root,
|
|
54
|
+
configured: config !== undefined,
|
|
55
|
+
project,
|
|
56
|
+
architecture: loadGuardArtifact(root, GUARD_ARCHITECTURE_FILE) ?? buildArchitectureMemory(project),
|
|
57
|
+
conventions: loadGuardArtifact(root, GUARD_CONVENTIONS_FILE) ?? buildConventionsMemory(project),
|
|
58
|
+
rules: config?.rules ?? [],
|
|
59
|
+
contracts: config?.contracts ?? [],
|
|
60
|
+
agent: loadGuardAgentConfig(root),
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
return resourceError('codapult://guard/context', error);
|
|
65
|
+
}
|
|
41
66
|
});
|
|
42
67
|
server.registerResource('codapult_guard_architecture', 'codapult://guard/architecture', {
|
|
43
68
|
title: 'Guard Architecture Map',
|
|
@@ -45,13 +70,18 @@ export function registerGuardResources(server) {
|
|
|
45
70
|
mimeType: 'application/json',
|
|
46
71
|
}, () => {
|
|
47
72
|
const root = findGuardRoot();
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
73
|
+
try {
|
|
74
|
+
const project = discoverProject(root);
|
|
75
|
+
return jsonResource('codapult://guard/architecture', {
|
|
76
|
+
version: 1,
|
|
77
|
+
root,
|
|
78
|
+
architecture: loadGuardArtifact(root, GUARD_ARCHITECTURE_FILE) ?? buildArchitectureMemory(project),
|
|
79
|
+
conventions: loadGuardArtifact(root, GUARD_CONVENTIONS_FILE) ?? buildConventionsMemory(project),
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
catch (error) {
|
|
83
|
+
return resourceError('codapult://guard/architecture', error);
|
|
84
|
+
}
|
|
55
85
|
});
|
|
56
86
|
server.registerResource('codapult_guard_proposals', 'codapult://guard/proposals', {
|
|
57
87
|
title: 'Architecture Guard Proposals',
|
|
@@ -59,6 +89,11 @@ export function registerGuardResources(server) {
|
|
|
59
89
|
mimeType: 'application/json',
|
|
60
90
|
}, () => {
|
|
61
91
|
const root = findGuardRoot();
|
|
62
|
-
|
|
92
|
+
try {
|
|
93
|
+
return jsonResource('codapult://guard/proposals', loadGuardProposals(root) ?? { version: 1, rules: [], contracts: [], questions: [] });
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
return resourceError('codapult://guard/proposals', error);
|
|
97
|
+
}
|
|
63
98
|
});
|
|
64
99
|
}
|
package/dist/mcp/tools/guard.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { buildArchitectureMemory, buildConventionsMemory, buildGeneratedGuardConfig, buildGuardProposals, buildGuardReviewPacket, discoverProject, discoverProjectWithMetrics, findGuardRoot, GUARD_ARCHITECTURE_FILE, GUARD_CONVENTIONS_FILE, loadBaseline, loadGuardArtifact, loadGuardConfig, loadGuardAgentConfig, loadGuardProposals, getGuardProposalFreshness, loadProjectModel, writeGuardProposals, initializeGuard, GuardAlreadyInitializedError,
|
|
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';
|
|
2
|
+
import { guardErrorPayload } from '../../core/errors.js';
|
|
2
3
|
import { analyzeProjectImpact } from '../../core/analysis/impact.js';
|
|
3
4
|
import { z } from 'zod';
|
|
4
5
|
import { runGuardVerification } from '../../core/verification/verify.js';
|
|
@@ -12,7 +13,208 @@ function jsonToolResult(value, isError = false) {
|
|
|
12
13
|
...(isError ? { isError: true } : {}),
|
|
13
14
|
};
|
|
14
15
|
}
|
|
16
|
+
function notConfiguredToolResult() {
|
|
17
|
+
return jsonToolResult({
|
|
18
|
+
status: 'error',
|
|
19
|
+
outcome: 'not-configured',
|
|
20
|
+
configured: false,
|
|
21
|
+
errorCode: 'GUARD_NOT_CONFIGURED',
|
|
22
|
+
message: 'Guard is not initialized.',
|
|
23
|
+
recoverable: true,
|
|
24
|
+
}, true);
|
|
25
|
+
}
|
|
26
|
+
function loadConfigSafely(root) {
|
|
27
|
+
try {
|
|
28
|
+
return { config: loadGuardConfig(root) };
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
return {
|
|
32
|
+
error: jsonToolResult(guardErrorPayload('GUARD_CONFIG_INVALID', error instanceof Error ? error.message : String(error), {
|
|
33
|
+
configured: false,
|
|
34
|
+
outcome: 'error',
|
|
35
|
+
recoverable: true,
|
|
36
|
+
hint: 'Repair the invalid Guard artifact, then run codapult-guard doctor.',
|
|
37
|
+
}), true),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function loadBaselineSafely(root) {
|
|
42
|
+
try {
|
|
43
|
+
return { baseline: loadBaseline(root) };
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
return {
|
|
47
|
+
error: jsonToolResult(guardErrorPayload('GUARD_CONFIG_INVALID', error instanceof Error ? error.message : String(error), {
|
|
48
|
+
configured: true,
|
|
49
|
+
outcome: 'error',
|
|
50
|
+
recoverable: true,
|
|
51
|
+
hint: 'Repair baseline.json, then run codapult-guard doctor.',
|
|
52
|
+
}), true),
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
function stateErrorResult(error) {
|
|
57
|
+
const isBusy = error instanceof GuardStateBusyError;
|
|
58
|
+
const isStale = error instanceof GuardStateStaleError;
|
|
59
|
+
if (!isBusy && !isStale)
|
|
60
|
+
return undefined;
|
|
61
|
+
return jsonToolResult(guardErrorPayload(isBusy ? 'GUARD_STATE_BUSY' : 'GUARD_STATE_STALE', String(error), {
|
|
62
|
+
configured: true,
|
|
63
|
+
outcome: 'error',
|
|
64
|
+
recoverable: true,
|
|
65
|
+
hint: 'Re-read Guard state and retry the operation.',
|
|
66
|
+
}), true);
|
|
67
|
+
}
|
|
68
|
+
function loadAgentSafely(root) {
|
|
69
|
+
try {
|
|
70
|
+
return { config: loadGuardAgentConfig(root) };
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
return {
|
|
74
|
+
error: jsonToolResult(guardErrorPayload('GUARD_CONFIG_INVALID', error instanceof Error ? error.message : String(error), {
|
|
75
|
+
configured: false,
|
|
76
|
+
outcome: 'error',
|
|
77
|
+
recoverable: true,
|
|
78
|
+
hint: 'Repair agent.json, then run codapult-guard doctor.',
|
|
79
|
+
}), true),
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
function loadProposalsSafely(root) {
|
|
84
|
+
try {
|
|
85
|
+
return { proposals: loadGuardProposals(root) };
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
return {
|
|
89
|
+
error: jsonToolResult(guardErrorPayload('GUARD_CONFIG_INVALID', error instanceof Error ? error.message : String(error), {
|
|
90
|
+
configured: false,
|
|
91
|
+
outcome: 'error',
|
|
92
|
+
recoverable: true,
|
|
93
|
+
hint: 'Repair proposals.json, then run codapult-guard doctor.',
|
|
94
|
+
}), true),
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
15
98
|
export function registerGuardTools(server) {
|
|
99
|
+
server.registerTool('codapult_guard_analyze', {
|
|
100
|
+
title: 'Refresh Guard Model',
|
|
101
|
+
description: 'Refresh persisted project facts and snapshots without changing rules, contracts, or baseline.',
|
|
102
|
+
inputSchema: {
|
|
103
|
+
root: rootSchema,
|
|
104
|
+
confirm: z
|
|
105
|
+
.boolean()
|
|
106
|
+
.default(false)
|
|
107
|
+
.describe('Required before writing refreshed Guard state.'),
|
|
108
|
+
wait_ms: z.number().int().min(0).max(120_000).default(30_000),
|
|
109
|
+
no_wait: z.boolean().default(false),
|
|
110
|
+
},
|
|
111
|
+
}, ({ root: requestedRoot, confirm, wait_ms, no_wait }) => {
|
|
112
|
+
const root = getGuardRoot(requestedRoot);
|
|
113
|
+
if (!confirm)
|
|
114
|
+
return jsonToolResult({ status: 'needs-confirmation', action: 'analyze' });
|
|
115
|
+
const discovery = discoverProjectWithMetrics(root, { persistCache: true });
|
|
116
|
+
let revision;
|
|
117
|
+
try {
|
|
118
|
+
revision = writeProjectState(root, discovery.model, {
|
|
119
|
+
waitMs: wait_ms,
|
|
120
|
+
noWait: no_wait,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
return (stateErrorResult(error) ??
|
|
125
|
+
jsonToolResult({ status: 'fail', message: String(error) }, true));
|
|
126
|
+
}
|
|
127
|
+
return jsonToolResult({
|
|
128
|
+
status: 'ok',
|
|
129
|
+
action: 'analyze',
|
|
130
|
+
revision,
|
|
131
|
+
discovery: discovery.metrics,
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
server.registerTool('codapult_guard_next_action', {
|
|
135
|
+
title: 'Guard Next Action',
|
|
136
|
+
description: 'Return the next bounded action for an AI coding-agent loop without modifying project files.',
|
|
137
|
+
inputSchema: { root: rootSchema },
|
|
138
|
+
}, ({ root: requestedRoot }) => {
|
|
139
|
+
const root = getGuardRoot(requestedRoot);
|
|
140
|
+
const loaded = loadConfigSafely(root);
|
|
141
|
+
if (loaded.error)
|
|
142
|
+
return loaded.error;
|
|
143
|
+
const config = loaded.config;
|
|
144
|
+
if (!config) {
|
|
145
|
+
return jsonToolResult({
|
|
146
|
+
status: 'needs-setup',
|
|
147
|
+
action: 'init',
|
|
148
|
+
reason: 'Guard policy is not initialized.',
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
const project = discoverProject(root);
|
|
152
|
+
const persisted = loadProjectModel(root);
|
|
153
|
+
if (persisted &&
|
|
154
|
+
'project' in persisted &&
|
|
155
|
+
fingerprintProjectModel(persisted) !== fingerprintProjectModel(project)) {
|
|
156
|
+
return jsonToolResult({
|
|
157
|
+
status: 'needs-refresh',
|
|
158
|
+
action: 'analyze',
|
|
159
|
+
reason: 'Persisted Guard project facts are stale after repository changes.',
|
|
160
|
+
canRetry: false,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
const loadedProposals = loadProposalsSafely(root);
|
|
164
|
+
if ('error' in loadedProposals)
|
|
165
|
+
return loadedProposals.error;
|
|
166
|
+
const proposals = loadedProposals.proposals;
|
|
167
|
+
const proposedCount = getPendingGuardProposals(proposals).length;
|
|
168
|
+
if (proposals && getGuardProposalFreshness(project, proposals) === 'stale') {
|
|
169
|
+
return jsonToolResult({
|
|
170
|
+
status: 'needs-decision',
|
|
171
|
+
action: 'propose',
|
|
172
|
+
reason: 'Guard proposals are stale after project changes.',
|
|
173
|
+
canRetry: false,
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
if (proposedCount > 0) {
|
|
177
|
+
return jsonToolResult({
|
|
178
|
+
status: 'needs-decision',
|
|
179
|
+
action: 'policy-review',
|
|
180
|
+
reason: `${proposedCount} proposed policy item(s) await explicit approval or rejection.`,
|
|
181
|
+
canRetry: false,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
const loadedBaseline = loadBaselineSafely(root);
|
|
185
|
+
if (loadedBaseline.error)
|
|
186
|
+
return loadedBaseline.error;
|
|
187
|
+
const report = scanGuard(root, config, {
|
|
188
|
+
changedOnly: true,
|
|
189
|
+
baseline: loadedBaseline.baseline,
|
|
190
|
+
includeArchitectureInsights: true,
|
|
191
|
+
});
|
|
192
|
+
const contractIssues = validateGuardPolicy(root, config);
|
|
193
|
+
if (report.findings.some((finding) => finding.severity === 'error')) {
|
|
194
|
+
return jsonToolResult({
|
|
195
|
+
status: 'needs-repair',
|
|
196
|
+
action: 'repair',
|
|
197
|
+
reason: 'Active architecture findings remain in changed files.',
|
|
198
|
+
findings: report.findings,
|
|
199
|
+
canRetry: true,
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
if (contractIssues.length > 0) {
|
|
203
|
+
return jsonToolResult({
|
|
204
|
+
status: 'needs-decision',
|
|
205
|
+
action: 'policy-repair',
|
|
206
|
+
reason: 'Guard contracts are stale or invalid.',
|
|
207
|
+
contractIssues,
|
|
208
|
+
canRetry: false,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
return jsonToolResult({
|
|
212
|
+
status: 'ready-for-verification',
|
|
213
|
+
action: 'verify',
|
|
214
|
+
reason: 'No changed-file architecture regressions remain.',
|
|
215
|
+
canRetry: true,
|
|
216
|
+
});
|
|
217
|
+
});
|
|
16
218
|
server.registerTool('codapult_guard_context', {
|
|
17
219
|
title: 'Guard Context',
|
|
18
220
|
description: 'Read the discovered project model, architecture insights, and local rules used by Codapult Guard. Works before init by discovering the project on demand.',
|
|
@@ -25,8 +227,14 @@ export function registerGuardTools(server) {
|
|
|
25
227
|
},
|
|
26
228
|
}, ({ root: requestedRoot, refresh }) => {
|
|
27
229
|
const root = getGuardRoot(requestedRoot);
|
|
28
|
-
const
|
|
29
|
-
|
|
230
|
+
const loaded = loadConfigSafely(root);
|
|
231
|
+
if (loaded.error)
|
|
232
|
+
return loaded.error;
|
|
233
|
+
const config = loaded.config;
|
|
234
|
+
const agent = loadAgentSafely(root);
|
|
235
|
+
if ('error' in agent)
|
|
236
|
+
return agent.error;
|
|
237
|
+
const agentConfig = agent.config;
|
|
30
238
|
const discovery = refresh
|
|
31
239
|
? discoverProjectWithMetrics(root, { persistCache: true })
|
|
32
240
|
: { model: loadProjectModel(root) ?? discoverProject(root), metrics: undefined };
|
|
@@ -73,9 +281,15 @@ export function registerGuardTools(server) {
|
|
|
73
281
|
if (persist && !confirm)
|
|
74
282
|
return jsonToolResult({ status: 'needs-confirmation', persist: true });
|
|
75
283
|
const project = discoverProject(root);
|
|
76
|
-
const
|
|
284
|
+
const loaded = loadConfigSafely(root);
|
|
285
|
+
if (loaded.error)
|
|
286
|
+
return loaded.error;
|
|
287
|
+
const config = loaded.config ?? buildGeneratedGuardConfig(project);
|
|
77
288
|
const proposals = buildGuardProposals(project, config);
|
|
78
|
-
const
|
|
289
|
+
const loadedPrevious = loadProposalsSafely(root);
|
|
290
|
+
if ('error' in loadedPrevious)
|
|
291
|
+
return loadedPrevious.error;
|
|
292
|
+
const previous = loadedPrevious.proposals;
|
|
79
293
|
if (persist) {
|
|
80
294
|
writeGuardProposals(root, {
|
|
81
295
|
...proposals,
|
|
@@ -108,16 +322,21 @@ export function registerGuardTools(server) {
|
|
|
108
322
|
.boolean()
|
|
109
323
|
.default(false)
|
|
110
324
|
.describe('Replace existing Guard state; requires confirm.'),
|
|
325
|
+
wait_ms: z.number().int().min(0).max(120_000).default(30_000),
|
|
326
|
+
no_wait: z.boolean().default(false),
|
|
111
327
|
},
|
|
112
|
-
}, ({ root: requestedRoot, confirm, force }) => {
|
|
328
|
+
}, ({ root: requestedRoot, confirm, force, wait_ms, no_wait }) => {
|
|
113
329
|
const root = getGuardRoot(requestedRoot);
|
|
114
330
|
if (!confirm)
|
|
115
331
|
return jsonToolResult({ status: 'needs-confirmation', root, force });
|
|
116
332
|
try {
|
|
117
|
-
const result = initializeGuard(root, { force });
|
|
333
|
+
const result = initializeGuard(root, { force, waitMs: wait_ms, noWait: no_wait });
|
|
118
334
|
return jsonToolResult({ status: 'ok', root, report: result.report });
|
|
119
335
|
}
|
|
120
336
|
catch (error) {
|
|
337
|
+
const stateError = stateErrorResult(error);
|
|
338
|
+
if (stateError)
|
|
339
|
+
return stateError;
|
|
121
340
|
const status = error instanceof GuardAlreadyInitializedError ? 'already-initialized' : 'fail';
|
|
122
341
|
return jsonToolResult({ status, root, message: error instanceof Error ? error.message : String(error) }, true);
|
|
123
342
|
}
|
|
@@ -133,7 +352,10 @@ export function registerGuardTools(server) {
|
|
|
133
352
|
},
|
|
134
353
|
}, ({ root: requestedRoot, ids, decision, confirm }) => {
|
|
135
354
|
const root = getGuardRoot(requestedRoot);
|
|
136
|
-
const
|
|
355
|
+
const loadedProposals = loadProposalsSafely(root);
|
|
356
|
+
if ('error' in loadedProposals)
|
|
357
|
+
return loadedProposals.error;
|
|
358
|
+
const proposals = loadedProposals.proposals;
|
|
137
359
|
if (!proposals)
|
|
138
360
|
return jsonToolResult({ status: 'not-configured', message: 'No proposals found.' }, true);
|
|
139
361
|
const project = discoverProject(root);
|
|
@@ -144,10 +366,28 @@ export function registerGuardTools(server) {
|
|
|
144
366
|
const missing = ids.filter((id) => !selected.some((item) => item.id === id));
|
|
145
367
|
if (missing.length > 0)
|
|
146
368
|
return jsonToolResult({ status: 'invalid', missing }, true);
|
|
369
|
+
const loadedConfig = loadConfigSafely(root);
|
|
370
|
+
if (loadedConfig.error)
|
|
371
|
+
return loadedConfig.error;
|
|
372
|
+
const approval = loadedConfig.config?.approval;
|
|
373
|
+
if (decision === 'approved' && approval?.mode === 'protected' && !approval.allowMcpApproval) {
|
|
374
|
+
return jsonToolResult({
|
|
375
|
+
status: 'approval-required',
|
|
376
|
+
message: 'Protected Guard policy requires approval outside the MCP agent surface.',
|
|
377
|
+
source: 'mcp',
|
|
378
|
+
ids,
|
|
379
|
+
}, true);
|
|
380
|
+
}
|
|
381
|
+
if (decision === 'approved') {
|
|
382
|
+
const approvalError = validateGuardProposalApproval(approval, proposals);
|
|
383
|
+
if (approvalError) {
|
|
384
|
+
return jsonToolResult({ status: 'approval-required', message: approvalError, source: 'mcp', ids }, true);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
147
387
|
if (!confirm)
|
|
148
388
|
return jsonToolResult({ status: 'needs-confirmation', decision, ids });
|
|
149
389
|
if (decision === 'approved') {
|
|
150
|
-
const config =
|
|
390
|
+
const config = loadedConfig.config ?? { version: 1, rules: [], contracts: [] };
|
|
151
391
|
const nextRules = [...config.rules];
|
|
152
392
|
const nextContracts = [...(config.contracts ?? [])];
|
|
153
393
|
for (const item of selected) {
|
|
@@ -168,13 +408,19 @@ export function registerGuardTools(server) {
|
|
|
168
408
|
nextRules.push(active);
|
|
169
409
|
}
|
|
170
410
|
}
|
|
171
|
-
|
|
411
|
+
applyGuardProposalDecision(root, { version: 1, rules: nextRules, contracts: nextContracts }, selected.map((item) => ({
|
|
412
|
+
id: item.id,
|
|
413
|
+
type: 'statement' in item ? 'contract' : 'rule',
|
|
414
|
+
decision,
|
|
415
|
+
})), { source: 'mcp' });
|
|
416
|
+
}
|
|
417
|
+
else {
|
|
418
|
+
applyGuardProposalDecision(root, loadedConfig.config ?? { version: 1, rules: [], contracts: [] }, selected.map((item) => ({
|
|
419
|
+
id: item.id,
|
|
420
|
+
type: 'statement' in item ? 'contract' : 'rule',
|
|
421
|
+
decision,
|
|
422
|
+
})), { source: 'mcp' });
|
|
172
423
|
}
|
|
173
|
-
recordGuardProposalDecision(root, selected.map((item) => ({
|
|
174
|
-
id: item.id,
|
|
175
|
-
type: 'statement' in item ? 'contract' : 'rule',
|
|
176
|
-
decision,
|
|
177
|
-
})));
|
|
178
424
|
return jsonToolResult({ status: 'ok', root, decision, ids });
|
|
179
425
|
});
|
|
180
426
|
server.registerTool('codapult_guard_verify', {
|
|
@@ -201,6 +447,9 @@ export function registerGuardTools(server) {
|
|
|
201
447
|
},
|
|
202
448
|
}, ({ root: requestedRoot, checks, changed_only, requirement, tools, strict, project_checks, iteration, }) => {
|
|
203
449
|
const root = getGuardRoot(requestedRoot);
|
|
450
|
+
const agent = loadAgentSafely(root);
|
|
451
|
+
if ('error' in agent)
|
|
452
|
+
return agent.error;
|
|
204
453
|
const result = runGuardVerification(root, {
|
|
205
454
|
checks,
|
|
206
455
|
changedOnly: changed_only,
|
|
@@ -209,7 +458,7 @@ export function registerGuardTools(server) {
|
|
|
209
458
|
strict,
|
|
210
459
|
projectChecks: project_checks,
|
|
211
460
|
});
|
|
212
|
-
const completionGate =
|
|
461
|
+
const completionGate = agent.config.completionGate;
|
|
213
462
|
return jsonToolResult({
|
|
214
463
|
...result,
|
|
215
464
|
completionGate: {
|
|
@@ -225,20 +474,15 @@ export function registerGuardTools(server) {
|
|
|
225
474
|
inputSchema: { root: rootSchema },
|
|
226
475
|
}, ({ root: requestedRoot }) => {
|
|
227
476
|
const root = getGuardRoot(requestedRoot);
|
|
228
|
-
const
|
|
477
|
+
const loaded = loadConfigSafely(root);
|
|
478
|
+
if (loaded.error)
|
|
479
|
+
return loaded.error;
|
|
480
|
+
const config = loaded.config;
|
|
229
481
|
if (!config) {
|
|
230
|
-
return
|
|
231
|
-
content: [
|
|
232
|
-
{
|
|
233
|
-
type: 'text',
|
|
234
|
-
text: JSON.stringify({ configured: false, outcome: 'not-configured' }, null, 2),
|
|
235
|
-
},
|
|
236
|
-
],
|
|
237
|
-
isError: true,
|
|
238
|
-
};
|
|
482
|
+
return notConfiguredToolResult();
|
|
239
483
|
}
|
|
240
484
|
const report = scanGuard(root, config, { includeArchitectureInsights: true });
|
|
241
|
-
const contractIssues =
|
|
485
|
+
const contractIssues = validateGuardPolicy(root, config);
|
|
242
486
|
const errors = report.findings.filter((finding) => finding.severity === 'error').length;
|
|
243
487
|
const warnings = report.findings.filter((finding) => finding.severity === 'warning').length;
|
|
244
488
|
const status = errors > 0 || contractIssues.length > 0 ? 'fail' : warnings > 0 ? 'warning' : 'ok';
|
|
@@ -271,19 +515,19 @@ export function registerGuardTools(server) {
|
|
|
271
515
|
},
|
|
272
516
|
}, ({ root: requestedRoot, changed_only, max_diff_chars, requirement, base }) => {
|
|
273
517
|
const root = getGuardRoot(requestedRoot);
|
|
274
|
-
const
|
|
518
|
+
const loaded = loadConfigSafely(root);
|
|
519
|
+
if (loaded.error)
|
|
520
|
+
return loaded.error;
|
|
521
|
+
const config = loaded.config;
|
|
275
522
|
if (!config) {
|
|
276
|
-
return
|
|
277
|
-
content: [
|
|
278
|
-
{
|
|
279
|
-
type: 'text',
|
|
280
|
-
text: JSON.stringify({ configured: false, outcome: 'not-configured' }, null, 2),
|
|
281
|
-
},
|
|
282
|
-
],
|
|
283
|
-
isError: true,
|
|
284
|
-
};
|
|
523
|
+
return notConfiguredToolResult();
|
|
285
524
|
}
|
|
286
|
-
const
|
|
525
|
+
const loadedBaseline = changed_only
|
|
526
|
+
? loadBaselineSafely(root)
|
|
527
|
+
: { baseline: new Set() };
|
|
528
|
+
if (loadedBaseline.error)
|
|
529
|
+
return loadedBaseline.error;
|
|
530
|
+
const packet = buildGuardReviewPacket(root, config, loadedBaseline.baseline ?? new Set(), max_diff_chars, changed_only, requirement, base);
|
|
287
531
|
return jsonToolResult({ ...packet, status: packet.diffError ? 'fail' : 'needs-review' }, Boolean(packet.diffError));
|
|
288
532
|
});
|
|
289
533
|
server.registerTool('codapult_guard_check', {
|
|
@@ -292,24 +536,22 @@ export function registerGuardTools(server) {
|
|
|
292
536
|
inputSchema: { root: rootSchema, changed_only: z.boolean().optional() },
|
|
293
537
|
}, ({ root: requestedRoot, changed_only }) => {
|
|
294
538
|
const root = getGuardRoot(requestedRoot);
|
|
295
|
-
const
|
|
539
|
+
const loaded = loadConfigSafely(root);
|
|
540
|
+
if (loaded.error)
|
|
541
|
+
return loaded.error;
|
|
542
|
+
const config = loaded.config;
|
|
296
543
|
if (!config) {
|
|
297
|
-
return
|
|
298
|
-
content: [
|
|
299
|
-
{
|
|
300
|
-
type: 'text',
|
|
301
|
-
text: JSON.stringify({ configured: false, outcome: 'not-configured' }, null, 2),
|
|
302
|
-
},
|
|
303
|
-
],
|
|
304
|
-
isError: true,
|
|
305
|
-
};
|
|
544
|
+
return notConfiguredToolResult();
|
|
306
545
|
}
|
|
546
|
+
const loadedBaseline = loadBaselineSafely(root);
|
|
547
|
+
if (loadedBaseline.error)
|
|
548
|
+
return loadedBaseline.error;
|
|
307
549
|
const report = scanGuard(root, config, {
|
|
308
550
|
changedOnly: changed_only,
|
|
309
|
-
baseline:
|
|
551
|
+
baseline: loadedBaseline.baseline,
|
|
310
552
|
includeArchitectureInsights: true,
|
|
311
553
|
});
|
|
312
|
-
const contractIssues =
|
|
554
|
+
const contractIssues = validateGuardPolicy(root, config);
|
|
313
555
|
const errors = report.findings.filter((finding) => finding.severity === 'error').length;
|
|
314
556
|
return jsonToolResult({
|
|
315
557
|
status: errors > 0 || contractIssues.length > 0 ? 'fail' : 'ok',
|
|
@@ -331,7 +573,10 @@ export function registerGuardTools(server) {
|
|
|
331
573
|
}, ({ root: requestedRoot, files }) => {
|
|
332
574
|
const root = getGuardRoot(requestedRoot);
|
|
333
575
|
const model = discoverProject(root);
|
|
334
|
-
const
|
|
576
|
+
const loaded = loadConfigSafely(root);
|
|
577
|
+
if (loaded.error)
|
|
578
|
+
return loaded.error;
|
|
579
|
+
const impact = analyzeProjectImpact(model, files, loaded.config?.contracts ?? []);
|
|
335
580
|
return jsonToolResult({
|
|
336
581
|
status: 'ok',
|
|
337
582
|
...impact,
|
|
@@ -346,7 +591,10 @@ export function registerGuardTools(server) {
|
|
|
346
591
|
},
|
|
347
592
|
}, ({ root: requestedRoot, id }) => {
|
|
348
593
|
const root = getGuardRoot(requestedRoot);
|
|
349
|
-
const
|
|
594
|
+
const loaded = loadConfigSafely(root);
|
|
595
|
+
if (loaded.error)
|
|
596
|
+
return loaded.error;
|
|
597
|
+
const config = loaded.config;
|
|
350
598
|
const item = [...(config?.rules ?? []), ...(config?.contracts ?? [])].find((candidate) => candidate.id === id);
|
|
351
599
|
if (!item)
|
|
352
600
|
return jsonToolResult({ status: 'not-found', id }, true);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codapult/guard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Local-first architecture guardrails for JavaScript and TypeScript projects, with first-class support for Next.js SaaS and AI-assisted development",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"packageManager": "pnpm@12.4.2+sha512.08adc6613180275c7c9edada39dcf08c9c61ad4e7eaf330a4f3461f102b0f907423454d117f98e72d47fef0616070644d7bffc973a6a57f5090a6d7c368b07c9",
|
|
@@ -60,7 +60,12 @@
|
|
|
60
60
|
"test:guard:fixtures": "pnpm build && node scripts/guard-fixture-smoke.mjs",
|
|
61
61
|
"test:guard:fixtures:regression": "pnpm build && node scripts/guard-fixture-regression.mjs",
|
|
62
62
|
"test:guard:fixtures:native": "pnpm build && node scripts/guard-fixture-native.mjs",
|
|
63
|
+
"test:guard:fixtures:adversarial": "pnpm build && node scripts/guard-fixture-adversarial.mjs",
|
|
64
|
+
"test:guard:fixtures:all": "pnpm build && node scripts/guard-fixture-suite.mjs",
|
|
65
|
+
"test:guard:fixtures:full": "pnpm build && node scripts/guard-fixture-suite.mjs --all",
|
|
66
|
+
"test:guard:concurrent": "pnpm build && node scripts/guard-concurrent-state.mjs",
|
|
63
67
|
"test:guard:pr": "pnpm build && node scripts/guard-pr-review.mjs",
|
|
68
|
+
"test:guard:benchmark": "pnpm build && node scripts/guard-benchmark.mjs",
|
|
64
69
|
"test:guard:golden": "pnpm build && node scripts/guard-golden-demo.mjs",
|
|
65
70
|
"test:watch": "vitest",
|
|
66
71
|
"typecheck": "tsc --noEmit",
|
|
@@ -99,19 +104,19 @@
|
|
|
99
104
|
},
|
|
100
105
|
"devDependencies": {
|
|
101
106
|
"@js-toolkit/config-utils": "^1.0.1",
|
|
102
|
-
"@js-toolkit/eslint-config": "^1.3.
|
|
107
|
+
"@js-toolkit/eslint-config": "^1.3.2",
|
|
103
108
|
"@js-toolkit/prettier-config": "^1.0.1",
|
|
104
109
|
"@js-toolkit/tsconfig": "^1.0.1",
|
|
105
110
|
"@release-it/conventional-changelog": "^12.0.2",
|
|
106
|
-
"@types/node": "^26.6.
|
|
111
|
+
"@types/node": "^26.6.2",
|
|
107
112
|
"@typescript/native": "npm:typescript@^7.0.2",
|
|
108
113
|
"@vitest/coverage-v8": "^5.0.1",
|
|
109
|
-
"eslint": "^10.
|
|
114
|
+
"eslint": "^10.11.0",
|
|
110
115
|
"eslint-config-prettier": "^10.1.8",
|
|
111
116
|
"eslint-plugin-prettier": "^5.5.6",
|
|
112
|
-
"prettier": "^3.9.
|
|
113
|
-
"release-it": "^21.0
|
|
114
|
-
"typescript-eslint": "^8.70.
|
|
117
|
+
"prettier": "^3.9.8",
|
|
118
|
+
"release-it": "^21.1.0",
|
|
119
|
+
"typescript-eslint": "^8.70.1",
|
|
115
120
|
"vitest": "^5.0.1"
|
|
116
121
|
}
|
|
117
122
|
}
|