@codapult/guard 0.1.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/LICENSE +21 -0
- package/README.md +290 -0
- package/dist/adapters/agents/agent-integration.d.ts +9 -0
- package/dist/adapters/agents/agent-integration.js +62 -0
- package/dist/adapters/command.d.ts +21 -0
- package/dist/adapters/command.js +60 -0
- package/dist/adapters/project-checks.d.ts +37 -0
- package/dist/adapters/project-checks.js +149 -0
- package/dist/cli/commands/guard.d.ts +55 -0
- package/dist/cli/commands/guard.js +510 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +75 -0
- package/dist/cli/ui.d.ts +6 -0
- package/dist/cli/ui.js +7 -0
- package/dist/commands/guard.d.ts +2 -0
- package/dist/commands/guard.js +2 -0
- package/dist/core/analysis/doctor.d.ts +12 -0
- package/dist/core/analysis/doctor.js +85 -0
- package/dist/core/analysis/packs.d.ts +21 -0
- package/dist/core/analysis/packs.js +251 -0
- package/dist/core/config.d.ts +6 -0
- package/dist/core/config.js +6 -0
- package/dist/core/discovery/discovery.d.ts +134 -0
- package/dist/core/discovery/discovery.js +816 -0
- package/dist/core/guard.d.ts +189 -0
- package/dist/core/guard.js +936 -0
- package/dist/core/history/history.d.ts +24 -0
- package/dist/core/history/history.js +65 -0
- package/dist/core/output/sarif.d.ts +35 -0
- package/dist/core/output/sarif.js +29 -0
- package/dist/core/policy/schemas.d.ts +272 -0
- package/dist/core/policy/schemas.js +72 -0
- package/dist/core/verification/verify.d.ts +29 -0
- package/dist/core/verification/verify.js +77 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +9 -0
- package/dist/mcp/prompts.d.ts +2 -0
- package/dist/mcp/prompts.js +54 -0
- package/dist/mcp/resources.d.ts +2 -0
- package/dist/mcp/resources.js +64 -0
- package/dist/mcp/server.d.ts +1 -0
- package/dist/mcp/server.js +17 -0
- package/dist/mcp/tools/guard.d.ts +2 -0
- package/dist/mcp/tools/guard.js +375 -0
- package/package.json +117 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
export function registerGuardPrompts(server) {
|
|
3
|
+
server.registerPrompt('codapult_guard_proposal_review', {
|
|
4
|
+
title: 'Guard Proposal Review',
|
|
5
|
+
description: 'Refine evidence-backed Guard rule and contract proposals before activation.',
|
|
6
|
+
argsSchema: {
|
|
7
|
+
proposals: z.string().describe('JSON proposal packet returned by codapult_guard_propose'),
|
|
8
|
+
focus: z.string().optional().describe('Optional domain focus.'),
|
|
9
|
+
},
|
|
10
|
+
}, ({ proposals, focus }) => ({
|
|
11
|
+
messages: [
|
|
12
|
+
{
|
|
13
|
+
role: 'user',
|
|
14
|
+
content: {
|
|
15
|
+
type: 'text',
|
|
16
|
+
text: `Review these Guard proposals against project evidence.${focus ? ` Focus on ${focus}.` : ''}
|
|
17
|
+
|
|
18
|
+
Keep only rules and contracts supported by repeated project evidence. For each proposal, return
|
|
19
|
+
keep/revise/reject, rationale, exact evidence, scope, severity, and unresolved questions. Never
|
|
20
|
+
activate a proposal automatically.
|
|
21
|
+
|
|
22
|
+
Proposal packet:\n\`\`\`json\n${proposals}\n\`\`\``,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
],
|
|
26
|
+
}));
|
|
27
|
+
server.registerPrompt('codapult_guard_semantic_review', {
|
|
28
|
+
title: 'Guard Semantic Review',
|
|
29
|
+
description: 'Review a Guard packet against project-specific contracts and architecture context.',
|
|
30
|
+
argsSchema: {
|
|
31
|
+
packet: z.string().describe('JSON review packet returned by codapult_guard_review'),
|
|
32
|
+
focus: z
|
|
33
|
+
.string()
|
|
34
|
+
.optional()
|
|
35
|
+
.describe('Optional focus such as auth, data access, or API behavior.'),
|
|
36
|
+
},
|
|
37
|
+
}, ({ packet, focus }) => ({
|
|
38
|
+
messages: [
|
|
39
|
+
{
|
|
40
|
+
role: 'user',
|
|
41
|
+
content: {
|
|
42
|
+
type: 'text',
|
|
43
|
+
text: `Perform an evidence-based semantic architecture review.${focus ? ` Focus on: ${focus}.` : ''}
|
|
44
|
+
|
|
45
|
+
Treat contracts as project-specific invariants. Do not repeat deterministic findings. Separate
|
|
46
|
+
violations from recommendations and include severity, file, evidence, explanation, and remediation.
|
|
47
|
+
If evidence is insufficient, say so instead of guessing. Do not infer redacted secrets.
|
|
48
|
+
|
|
49
|
+
Guard packet:\n\`\`\`json\n${packet}\n\`\`\``,
|
|
50
|
+
},
|
|
51
|
+
},
|
|
52
|
+
],
|
|
53
|
+
}));
|
|
54
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { buildArchitectureMemory, buildConventionsMemory, discoverProject, findGuardRoot, GUARD_ARCHITECTURE_FILE, GUARD_CONVENTIONS_FILE, loadGuardAgentConfig, loadGuardArtifact, loadGuardConfig, loadGuardProposals, } from '../core/guard.js';
|
|
2
|
+
function jsonResource(uri, value) {
|
|
3
|
+
return {
|
|
4
|
+
contents: [{ uri, text: JSON.stringify(value, null, 2), mimeType: 'application/json' }],
|
|
5
|
+
};
|
|
6
|
+
}
|
|
7
|
+
export function registerGuardResources(server) {
|
|
8
|
+
server.registerResource('codapult_guard_rules', 'codapult://guard/rules', {
|
|
9
|
+
title: 'Architecture Guard Rules',
|
|
10
|
+
description: 'Local architecture invariants available to CLI checks and AI agents',
|
|
11
|
+
mimeType: 'application/json',
|
|
12
|
+
}, () => {
|
|
13
|
+
const root = findGuardRoot();
|
|
14
|
+
const config = loadGuardConfig(root);
|
|
15
|
+
return jsonResource('codapult://guard/rules', {
|
|
16
|
+
configured: config !== undefined,
|
|
17
|
+
rules: config?.rules ?? [],
|
|
18
|
+
contracts: config?.contracts ?? [],
|
|
19
|
+
project: discoverProject(root),
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
server.registerResource('codapult_guard_context', 'codapult://guard/context', {
|
|
23
|
+
title: 'Guard Project Context',
|
|
24
|
+
description: 'Current project model, architecture policy, contracts, and gate settings.',
|
|
25
|
+
mimeType: 'application/json',
|
|
26
|
+
}, () => {
|
|
27
|
+
const root = findGuardRoot();
|
|
28
|
+
const project = discoverProject(root);
|
|
29
|
+
const config = loadGuardConfig(root);
|
|
30
|
+
return jsonResource('codapult://guard/context', {
|
|
31
|
+
version: 1,
|
|
32
|
+
root,
|
|
33
|
+
configured: config !== undefined,
|
|
34
|
+
project,
|
|
35
|
+
architecture: loadGuardArtifact(root, GUARD_ARCHITECTURE_FILE) ?? buildArchitectureMemory(project),
|
|
36
|
+
conventions: loadGuardArtifact(root, GUARD_CONVENTIONS_FILE) ?? buildConventionsMemory(project),
|
|
37
|
+
rules: config?.rules ?? [],
|
|
38
|
+
contracts: config?.contracts ?? [],
|
|
39
|
+
agent: loadGuardAgentConfig(root),
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
server.registerResource('codapult_guard_architecture', 'codapult://guard/architecture', {
|
|
43
|
+
title: 'Guard Architecture Map',
|
|
44
|
+
description: 'Observed architecture and conventions.',
|
|
45
|
+
mimeType: 'application/json',
|
|
46
|
+
}, () => {
|
|
47
|
+
const root = findGuardRoot();
|
|
48
|
+
const project = discoverProject(root);
|
|
49
|
+
return jsonResource('codapult://guard/architecture', {
|
|
50
|
+
version: 1,
|
|
51
|
+
root,
|
|
52
|
+
architecture: loadGuardArtifact(root, GUARD_ARCHITECTURE_FILE) ?? buildArchitectureMemory(project),
|
|
53
|
+
conventions: loadGuardArtifact(root, GUARD_CONVENTIONS_FILE) ?? buildConventionsMemory(project),
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
server.registerResource('codapult_guard_proposals', 'codapult://guard/proposals', {
|
|
57
|
+
title: 'Architecture Guard Proposals',
|
|
58
|
+
description: 'Evidence-backed rules and contracts awaiting review.',
|
|
59
|
+
mimeType: 'application/json',
|
|
60
|
+
}, () => {
|
|
61
|
+
const root = findGuardRoot();
|
|
62
|
+
return jsonResource('codapult://guard/proposals', loadGuardProposals(root) ?? { version: 1, rules: [], contracts: [], questions: [] });
|
|
63
|
+
});
|
|
64
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
|
|
3
|
+
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
|
|
4
|
+
import { registerGuardTools } from './tools/guard.js';
|
|
5
|
+
import { registerGuardPrompts } from './prompts.js';
|
|
6
|
+
import { registerGuardResources } from './resources.js';
|
|
7
|
+
const require = createRequire(import.meta.url);
|
|
8
|
+
const packageJson = require('../../package.json');
|
|
9
|
+
const server = new McpServer({
|
|
10
|
+
name: 'codapult-guard',
|
|
11
|
+
version: packageJson.version,
|
|
12
|
+
});
|
|
13
|
+
registerGuardTools(server);
|
|
14
|
+
registerGuardPrompts(server);
|
|
15
|
+
registerGuardResources(server);
|
|
16
|
+
const transport = new StdioServerTransport();
|
|
17
|
+
await server.connect(transport);
|
|
@@ -0,0 +1,375 @@
|
|
|
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, recordGuardProposalDecision, writeGuardConfig, scanGuard, validateGuardContracts, classifyGuardOutcome, } from '../../core/guard.js';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { runGuardVerification } from '../../core/verification/verify.js';
|
|
4
|
+
const rootSchema = z.string().trim().min(1).optional().describe('Project path or workspace root.');
|
|
5
|
+
function getGuardRoot(root) {
|
|
6
|
+
return root ? findGuardRoot(root) : findGuardRoot();
|
|
7
|
+
}
|
|
8
|
+
function jsonToolResult(value, isError = false) {
|
|
9
|
+
return {
|
|
10
|
+
content: [{ type: 'text', text: JSON.stringify(value, null, 2) }],
|
|
11
|
+
...(isError ? { isError: true } : {}),
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
export function registerGuardTools(server) {
|
|
15
|
+
server.registerTool('codapult_guard_context', {
|
|
16
|
+
title: 'Guard Context',
|
|
17
|
+
description: 'Read the discovered project model, architecture insights, and local rules used by Codapult Guard. Works before init by discovering the project on demand.',
|
|
18
|
+
inputSchema: {
|
|
19
|
+
root: rootSchema,
|
|
20
|
+
refresh: z
|
|
21
|
+
.boolean()
|
|
22
|
+
.default(true)
|
|
23
|
+
.describe('Rediscover the working tree instead of reading the persisted snapshot.'),
|
|
24
|
+
},
|
|
25
|
+
}, ({ root: requestedRoot, refresh }) => {
|
|
26
|
+
const root = getGuardRoot(requestedRoot);
|
|
27
|
+
const config = loadGuardConfig(root);
|
|
28
|
+
const agentConfig = loadGuardAgentConfig(root);
|
|
29
|
+
const discovery = refresh
|
|
30
|
+
? discoverProjectWithMetrics(root, { persistCache: true })
|
|
31
|
+
: { model: loadProjectModel(root) ?? discoverProject(root), metrics: undefined };
|
|
32
|
+
const project = discovery.model;
|
|
33
|
+
return {
|
|
34
|
+
content: [
|
|
35
|
+
{
|
|
36
|
+
type: 'text',
|
|
37
|
+
text: JSON.stringify({
|
|
38
|
+
version: 1,
|
|
39
|
+
status: 'ok',
|
|
40
|
+
outcome: classifyGuardOutcome({ configured: config !== undefined }),
|
|
41
|
+
configured: config !== undefined,
|
|
42
|
+
tools: agentConfig.tools,
|
|
43
|
+
tooling: agentConfig.tooling,
|
|
44
|
+
completionGate: agentConfig.completionGate,
|
|
45
|
+
rules: config?.rules ?? [],
|
|
46
|
+
contracts: config?.contracts ?? [],
|
|
47
|
+
architecture: refresh
|
|
48
|
+
? buildArchitectureMemory(project)
|
|
49
|
+
: (loadGuardArtifact(root, GUARD_ARCHITECTURE_FILE) ??
|
|
50
|
+
buildArchitectureMemory(project)),
|
|
51
|
+
conventions: refresh
|
|
52
|
+
? buildConventionsMemory(project)
|
|
53
|
+
: (loadGuardArtifact(root, GUARD_CONVENTIONS_FILE) ??
|
|
54
|
+
buildConventionsMemory(project)),
|
|
55
|
+
project,
|
|
56
|
+
discovery: discovery.metrics,
|
|
57
|
+
}, null, 2),
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
};
|
|
61
|
+
});
|
|
62
|
+
server.registerTool('codapult_guard_propose', {
|
|
63
|
+
title: 'Guard Proposals',
|
|
64
|
+
description: 'Generate evidence-based rule and contract proposals for an AI agent. Proposals are never activated automatically.',
|
|
65
|
+
inputSchema: {
|
|
66
|
+
root: rootSchema,
|
|
67
|
+
persist: z.boolean().default(false).describe('Persist proposals.json in the project.'),
|
|
68
|
+
confirm: z.boolean().default(false).describe('Required when persist is true.'),
|
|
69
|
+
},
|
|
70
|
+
}, ({ root: requestedRoot, persist, confirm }) => {
|
|
71
|
+
const root = getGuardRoot(requestedRoot);
|
|
72
|
+
if (persist && !confirm)
|
|
73
|
+
return jsonToolResult({ status: 'needs-confirmation', persist: true });
|
|
74
|
+
const project = discoverProject(root);
|
|
75
|
+
const config = loadGuardConfig(root) ?? buildGeneratedGuardConfig(project);
|
|
76
|
+
const proposals = buildGuardProposals(project, config);
|
|
77
|
+
const previous = loadGuardProposals(root);
|
|
78
|
+
if (persist) {
|
|
79
|
+
writeGuardProposals(root, {
|
|
80
|
+
...proposals,
|
|
81
|
+
...(previous?.decisions ? { decisions: previous.decisions } : {}),
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
content: [
|
|
86
|
+
{
|
|
87
|
+
type: 'text',
|
|
88
|
+
text: JSON.stringify({
|
|
89
|
+
status: 'ok',
|
|
90
|
+
...proposals,
|
|
91
|
+
freshness: getGuardProposalFreshness(project, proposals),
|
|
92
|
+
previousFreshness: getGuardProposalFreshness(project, previous),
|
|
93
|
+
persisted: persist,
|
|
94
|
+
instructions: 'Review evidence and questions with the user or host AI before activating any proposal.',
|
|
95
|
+
}, null, 2),
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
};
|
|
99
|
+
});
|
|
100
|
+
server.registerTool('codapult_guard_init', {
|
|
101
|
+
title: 'Initialize Guard',
|
|
102
|
+
description: 'Create Guard artifacts after explicit confirmation; existing baselines are protected by default.',
|
|
103
|
+
inputSchema: {
|
|
104
|
+
root: rootSchema,
|
|
105
|
+
confirm: z.boolean().default(false).describe('Required before writing Guard artifacts.'),
|
|
106
|
+
force: z
|
|
107
|
+
.boolean()
|
|
108
|
+
.default(false)
|
|
109
|
+
.describe('Replace existing Guard state; requires confirm.'),
|
|
110
|
+
},
|
|
111
|
+
}, ({ root: requestedRoot, confirm, force }) => {
|
|
112
|
+
const root = getGuardRoot(requestedRoot);
|
|
113
|
+
if (!confirm)
|
|
114
|
+
return jsonToolResult({ status: 'needs-confirmation', root, force });
|
|
115
|
+
try {
|
|
116
|
+
const result = initializeGuard(root, { force });
|
|
117
|
+
return jsonToolResult({ status: 'ok', root, report: result.report });
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
const status = error instanceof GuardAlreadyInitializedError ? 'already-initialized' : 'fail';
|
|
121
|
+
return jsonToolResult({ status, root, message: error instanceof Error ? error.message : String(error) }, true);
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
server.registerTool('codapult_guard_proposal_decide', {
|
|
125
|
+
title: 'Decide Guard Proposals',
|
|
126
|
+
description: 'Approve or reject current evidence-backed proposals; stale proposals cannot be activated.',
|
|
127
|
+
inputSchema: {
|
|
128
|
+
root: rootSchema,
|
|
129
|
+
ids: z.array(z.string().trim().min(1)).min(1),
|
|
130
|
+
decision: z.enum(['approved', 'rejected']),
|
|
131
|
+
confirm: z.boolean().default(false).describe('Required before changing Guard state.'),
|
|
132
|
+
},
|
|
133
|
+
}, ({ root: requestedRoot, ids, decision, confirm }) => {
|
|
134
|
+
const root = getGuardRoot(requestedRoot);
|
|
135
|
+
const proposals = loadGuardProposals(root);
|
|
136
|
+
if (!proposals)
|
|
137
|
+
return jsonToolResult({ status: 'not-configured', message: 'No proposals found.' }, true);
|
|
138
|
+
const project = discoverProject(root);
|
|
139
|
+
if (getGuardProposalFreshness(project, proposals) === 'stale') {
|
|
140
|
+
return jsonToolResult({ status: 'stale', message: 'Regenerate proposals before deciding on them.' }, true);
|
|
141
|
+
}
|
|
142
|
+
const selected = [...proposals.rules, ...proposals.contracts].filter((item) => ids.includes(item.id));
|
|
143
|
+
const missing = ids.filter((id) => !selected.some((item) => item.id === id));
|
|
144
|
+
if (missing.length > 0)
|
|
145
|
+
return jsonToolResult({ status: 'invalid', missing }, true);
|
|
146
|
+
if (!confirm)
|
|
147
|
+
return jsonToolResult({ status: 'needs-confirmation', decision, ids });
|
|
148
|
+
if (decision === 'approved') {
|
|
149
|
+
const config = loadGuardConfig(root) ?? { version: 1, rules: [], contracts: [] };
|
|
150
|
+
const nextRules = [...config.rules];
|
|
151
|
+
const nextContracts = [...(config.contracts ?? [])];
|
|
152
|
+
for (const item of selected) {
|
|
153
|
+
if ('statement' in item) {
|
|
154
|
+
const index = nextContracts.findIndex((contract) => contract.id === item.id);
|
|
155
|
+
const active = { ...item, status: 'active' };
|
|
156
|
+
if (index >= 0)
|
|
157
|
+
nextContracts[index] = active;
|
|
158
|
+
else
|
|
159
|
+
nextContracts.push(active);
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
const index = nextRules.findIndex((rule) => rule.id === item.id);
|
|
163
|
+
const active = { ...item, status: 'active' };
|
|
164
|
+
if (index >= 0)
|
|
165
|
+
nextRules[index] = active;
|
|
166
|
+
else
|
|
167
|
+
nextRules.push(active);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
writeGuardConfig(root, { version: 1, rules: nextRules, contracts: nextContracts });
|
|
171
|
+
}
|
|
172
|
+
recordGuardProposalDecision(root, selected.map((item) => ({
|
|
173
|
+
id: item.id,
|
|
174
|
+
type: 'statement' in item ? 'contract' : 'rule',
|
|
175
|
+
decision,
|
|
176
|
+
})));
|
|
177
|
+
return jsonToolResult({ status: 'ok', root, decision, ids });
|
|
178
|
+
});
|
|
179
|
+
server.registerTool('codapult_guard_verify', {
|
|
180
|
+
title: 'Guard Verify',
|
|
181
|
+
description: 'Run project checks, production build, and architecture regression checks as one verification gate.',
|
|
182
|
+
inputSchema: {
|
|
183
|
+
root: rootSchema,
|
|
184
|
+
checks: z
|
|
185
|
+
.array(z.enum(['lint', 'typecheck', 'test', 'build']))
|
|
186
|
+
.optional()
|
|
187
|
+
.describe('Checks to run; defaults to lint, typecheck, test, and build.'),
|
|
188
|
+
changed_only: z.boolean().optional(),
|
|
189
|
+
requirement: z.string().trim().min(1).max(20_000).optional(),
|
|
190
|
+
tools: z
|
|
191
|
+
.enum(['auto', 'on', 'off'])
|
|
192
|
+
.optional()
|
|
193
|
+
.describe('External tools mode; defaults to agent.json.'),
|
|
194
|
+
strict: z.boolean().optional(),
|
|
195
|
+
project_checks: z
|
|
196
|
+
.boolean()
|
|
197
|
+
.optional()
|
|
198
|
+
.describe('Override completionGate.projectChecks for this invocation.'),
|
|
199
|
+
iteration: z.number().int().positive().default(1),
|
|
200
|
+
},
|
|
201
|
+
}, ({ root: requestedRoot, checks, changed_only, requirement, tools, strict, project_checks, iteration, }) => {
|
|
202
|
+
const root = getGuardRoot(requestedRoot);
|
|
203
|
+
const result = runGuardVerification(root, {
|
|
204
|
+
checks,
|
|
205
|
+
changedOnly: changed_only,
|
|
206
|
+
requirement,
|
|
207
|
+
tools,
|
|
208
|
+
strict,
|
|
209
|
+
projectChecks: project_checks,
|
|
210
|
+
});
|
|
211
|
+
const completionGate = loadGuardAgentConfig(root).completionGate;
|
|
212
|
+
return jsonToolResult({
|
|
213
|
+
...result,
|
|
214
|
+
completionGate: {
|
|
215
|
+
...completionGate,
|
|
216
|
+
iteration,
|
|
217
|
+
canRetry: result.status === 'fail' && iteration < completionGate.maxIterations,
|
|
218
|
+
},
|
|
219
|
+
}, result.status === 'fail');
|
|
220
|
+
});
|
|
221
|
+
server.registerTool('codapult_guard_audit', {
|
|
222
|
+
title: 'Guard Audit',
|
|
223
|
+
description: 'Run a full Guard audit, ignoring baseline suppression and validating contracts.',
|
|
224
|
+
inputSchema: { root: rootSchema },
|
|
225
|
+
}, ({ root: requestedRoot }) => {
|
|
226
|
+
const root = getGuardRoot(requestedRoot);
|
|
227
|
+
const config = loadGuardConfig(root);
|
|
228
|
+
if (!config) {
|
|
229
|
+
return {
|
|
230
|
+
content: [
|
|
231
|
+
{
|
|
232
|
+
type: 'text',
|
|
233
|
+
text: JSON.stringify({ configured: false, outcome: 'not-configured' }, null, 2),
|
|
234
|
+
},
|
|
235
|
+
],
|
|
236
|
+
isError: true,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
const report = scanGuard(root, config, { includeArchitectureInsights: true });
|
|
240
|
+
const contractIssues = validateGuardContracts(root, config.contracts ?? []);
|
|
241
|
+
const errors = report.findings.filter((finding) => finding.severity === 'error').length;
|
|
242
|
+
const warnings = report.findings.filter((finding) => finding.severity === 'warning').length;
|
|
243
|
+
const status = errors > 0 || contractIssues.length > 0 ? 'fail' : warnings > 0 ? 'warning' : 'ok';
|
|
244
|
+
return jsonToolResult({
|
|
245
|
+
status,
|
|
246
|
+
outcome: classifyGuardOutcome({
|
|
247
|
+
errors: errors + contractIssues.length,
|
|
248
|
+
warnings,
|
|
249
|
+
}),
|
|
250
|
+
errors,
|
|
251
|
+
warnings,
|
|
252
|
+
contractIssues,
|
|
253
|
+
report,
|
|
254
|
+
}, status === 'fail');
|
|
255
|
+
});
|
|
256
|
+
server.registerTool('codapult_guard_review', {
|
|
257
|
+
title: 'Prepare Guard Review',
|
|
258
|
+
description: 'Prepare a bounded semantic-review packet containing the current diff, project model, contracts, and deterministic findings for an AI reviewer.',
|
|
259
|
+
inputSchema: {
|
|
260
|
+
root: rootSchema,
|
|
261
|
+
changed_only: z.boolean().default(true),
|
|
262
|
+
max_diff_chars: z.number().int().positive().max(500_000).default(120_000),
|
|
263
|
+
requirement: z.string().trim().min(1).max(20_000).optional(),
|
|
264
|
+
base: z
|
|
265
|
+
.string()
|
|
266
|
+
.trim()
|
|
267
|
+
.min(1)
|
|
268
|
+
.optional()
|
|
269
|
+
.describe('Git base ref for PR review, e.g. origin/main.'),
|
|
270
|
+
},
|
|
271
|
+
}, ({ root: requestedRoot, changed_only, max_diff_chars, requirement, base }) => {
|
|
272
|
+
const root = getGuardRoot(requestedRoot);
|
|
273
|
+
const config = loadGuardConfig(root);
|
|
274
|
+
if (!config) {
|
|
275
|
+
return {
|
|
276
|
+
content: [
|
|
277
|
+
{
|
|
278
|
+
type: 'text',
|
|
279
|
+
text: JSON.stringify({ configured: false, outcome: 'not-configured' }, null, 2),
|
|
280
|
+
},
|
|
281
|
+
],
|
|
282
|
+
isError: true,
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
const packet = buildGuardReviewPacket(root, config, changed_only ? loadBaseline(root) : new Set(), max_diff_chars, changed_only, requirement, base);
|
|
286
|
+
return jsonToolResult({ ...packet, status: packet.diffError ? 'fail' : 'needs-review' }, Boolean(packet.diffError));
|
|
287
|
+
});
|
|
288
|
+
server.registerTool('codapult_guard_check', {
|
|
289
|
+
title: 'Guard Check',
|
|
290
|
+
description: 'Check project architecture rules and return structured new violations.',
|
|
291
|
+
inputSchema: { root: rootSchema, changed_only: z.boolean().optional() },
|
|
292
|
+
}, ({ root: requestedRoot, changed_only }) => {
|
|
293
|
+
const root = getGuardRoot(requestedRoot);
|
|
294
|
+
const config = loadGuardConfig(root);
|
|
295
|
+
if (!config) {
|
|
296
|
+
return {
|
|
297
|
+
content: [
|
|
298
|
+
{
|
|
299
|
+
type: 'text',
|
|
300
|
+
text: JSON.stringify({ configured: false, outcome: 'not-configured' }, null, 2),
|
|
301
|
+
},
|
|
302
|
+
],
|
|
303
|
+
isError: true,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
const report = scanGuard(root, config, {
|
|
307
|
+
changedOnly: changed_only,
|
|
308
|
+
baseline: loadBaseline(root),
|
|
309
|
+
includeArchitectureInsights: true,
|
|
310
|
+
});
|
|
311
|
+
const contractIssues = validateGuardContracts(root, config.contracts ?? []);
|
|
312
|
+
const errors = report.findings.filter((finding) => finding.severity === 'error').length;
|
|
313
|
+
return jsonToolResult({
|
|
314
|
+
status: errors > 0 || contractIssues.length > 0 ? 'fail' : 'ok',
|
|
315
|
+
outcome: classifyGuardOutcome({
|
|
316
|
+
errors: errors + contractIssues.length,
|
|
317
|
+
warnings: report.findings.filter((finding) => finding.severity === 'warning').length,
|
|
318
|
+
}),
|
|
319
|
+
contractIssues,
|
|
320
|
+
...report,
|
|
321
|
+
}, errors > 0 || contractIssues.length > 0);
|
|
322
|
+
});
|
|
323
|
+
server.registerTool('codapult_guard_impact', {
|
|
324
|
+
title: 'Guard Impact Analysis',
|
|
325
|
+
description: 'Explain the project impact of one or more changed files using the discovered dependency graph, routes, capabilities, and contracts.',
|
|
326
|
+
inputSchema: {
|
|
327
|
+
root: rootSchema,
|
|
328
|
+
files: z.array(z.string().trim().min(1)).min(1),
|
|
329
|
+
},
|
|
330
|
+
}, ({ root: requestedRoot, files }) => {
|
|
331
|
+
const root = getGuardRoot(requestedRoot);
|
|
332
|
+
const model = discoverProject(root);
|
|
333
|
+
const changed = new Set(files);
|
|
334
|
+
const paths = model.insights.impactPaths.filter((impact) => impact.files.some((file) => changed.has(file)) || changed.has(impact.entrypoint));
|
|
335
|
+
const modules = model.modules.filter((module) => changed.has(module.path) || module.resolvedImports.some((file) => changed.has(file)));
|
|
336
|
+
const capabilities = Object.entries(model.capabilities)
|
|
337
|
+
.filter(([, signal]) => signal.files.some((file) => changed.has(file)))
|
|
338
|
+
.map(([id]) => id);
|
|
339
|
+
const contracts = (loadGuardConfig(root)?.contracts ?? []).filter((contract) => [...(contract.scope ?? []), ...(contract.entrypoints ?? [])].some((scope) => [...changed].some((file) => file === scope || file.startsWith(`${scope}/`))));
|
|
340
|
+
return jsonToolResult({
|
|
341
|
+
status: 'ok',
|
|
342
|
+
files,
|
|
343
|
+
affectedModules: modules.map((module) => module.path),
|
|
344
|
+
impactPaths: paths,
|
|
345
|
+
capabilities,
|
|
346
|
+
relevantContracts: contracts,
|
|
347
|
+
});
|
|
348
|
+
});
|
|
349
|
+
server.registerTool('codapult_guard_explain', {
|
|
350
|
+
title: 'Explain Guard Policy',
|
|
351
|
+
description: 'Explain a Guard rule or contract with its evidence and applicable scope.',
|
|
352
|
+
inputSchema: {
|
|
353
|
+
root: rootSchema,
|
|
354
|
+
id: z.string().trim().min(1),
|
|
355
|
+
},
|
|
356
|
+
}, ({ root: requestedRoot, id }) => {
|
|
357
|
+
const root = getGuardRoot(requestedRoot);
|
|
358
|
+
const config = loadGuardConfig(root);
|
|
359
|
+
const item = [...(config?.rules ?? []), ...(config?.contracts ?? [])].find((candidate) => candidate.id === id);
|
|
360
|
+
if (!item)
|
|
361
|
+
return jsonToolResult({ status: 'not-found', id }, true);
|
|
362
|
+
return jsonToolResult({
|
|
363
|
+
status: 'ok',
|
|
364
|
+
id,
|
|
365
|
+
policy: item,
|
|
366
|
+
evidence: 'evidence' in item ? (item.evidence ?? []) : [],
|
|
367
|
+
guidance: 'guidance' in item ? (item.guidance ?? []) : [],
|
|
368
|
+
nextSteps: [
|
|
369
|
+
'Inspect the referenced project modules and current diff.',
|
|
370
|
+
'Use an existing project boundary that satisfies the policy.',
|
|
371
|
+
'If the policy is no longer correct, update it through a reviewed project change.',
|
|
372
|
+
],
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@codapult/guard",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Local-first architecture guardrails for JavaScript and TypeScript projects, with first-class support for Next.js SaaS and AI-assisted development",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"packageManager": "pnpm@11.22.0+sha512.1ff870c4c6133dfd88fb2afc46dd13d47f09c9794b438c6fdb47ca98caf3bc16381ee0be93a091b8e3824cf01f889f46d7d9e20910fb0be1ab0fb5baa80dd621",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=20.19.0"
|
|
9
|
+
},
|
|
10
|
+
"type": "module",
|
|
11
|
+
"bin": {
|
|
12
|
+
"codapult-guard": "./dist/cli/index.js"
|
|
13
|
+
},
|
|
14
|
+
"exports": {
|
|
15
|
+
".": {
|
|
16
|
+
"types": "./dist/index.d.ts",
|
|
17
|
+
"import": "./dist/index.js"
|
|
18
|
+
},
|
|
19
|
+
"./cli": {
|
|
20
|
+
"types": "./dist/cli/index.d.ts",
|
|
21
|
+
"import": "./dist/cli/index.js"
|
|
22
|
+
},
|
|
23
|
+
"./mcp": {
|
|
24
|
+
"types": "./dist/mcp/tools/guard.d.ts",
|
|
25
|
+
"import": "./dist/mcp/tools/guard.js"
|
|
26
|
+
},
|
|
27
|
+
"./commands/guard": {
|
|
28
|
+
"types": "./dist/cli/commands/guard.d.ts",
|
|
29
|
+
"import": "./dist/cli/commands/guard.js"
|
|
30
|
+
},
|
|
31
|
+
"./mcp/prompts": {
|
|
32
|
+
"types": "./dist/mcp/prompts.d.ts",
|
|
33
|
+
"import": "./dist/mcp/prompts.js"
|
|
34
|
+
},
|
|
35
|
+
"./mcp/resources": {
|
|
36
|
+
"types": "./dist/mcp/resources.d.ts",
|
|
37
|
+
"import": "./dist/mcp/resources.js"
|
|
38
|
+
},
|
|
39
|
+
"./project-checks": {
|
|
40
|
+
"types": "./dist/adapters/project-checks.d.ts",
|
|
41
|
+
"import": "./dist/adapters/project-checks.js"
|
|
42
|
+
},
|
|
43
|
+
"./package.json": "./package.json"
|
|
44
|
+
},
|
|
45
|
+
"files": [
|
|
46
|
+
"dist"
|
|
47
|
+
],
|
|
48
|
+
"scripts": {
|
|
49
|
+
"clean": "node scripts/clean-dist.mjs",
|
|
50
|
+
"build": "pnpm clean && tsc -p tsconfig.build.json",
|
|
51
|
+
"dev": "tsc --watch",
|
|
52
|
+
"lint": "eslint .",
|
|
53
|
+
"lint:fix": "eslint --fix .",
|
|
54
|
+
"format": "prettier --write .",
|
|
55
|
+
"format:check": "prettier --check .",
|
|
56
|
+
"release:check": "pnpm lint && pnpm format:check && pnpm typecheck && pnpm test && pnpm build && npm pack --dry-run",
|
|
57
|
+
"test": "vitest run",
|
|
58
|
+
"test:coverage": "vitest run --coverage",
|
|
59
|
+
"test:guard:coverage": "vitest run src/core src/adapters/project-checks.test.ts --coverage --coverage.include='src/core/**/*.ts' --coverage.include='src/adapters/project-checks.ts'",
|
|
60
|
+
"test:guard:fixtures": "pnpm build && node scripts/guard-fixture-smoke.mjs",
|
|
61
|
+
"test:guard:fixtures:regression": "pnpm build && node scripts/guard-fixture-regression.mjs",
|
|
62
|
+
"test:guard:fixtures:native": "pnpm build && node scripts/guard-fixture-native.mjs",
|
|
63
|
+
"test:guard:pr": "pnpm build && node scripts/guard-pr-review.mjs",
|
|
64
|
+
"test:guard:golden": "pnpm build && node scripts/guard-golden-demo.mjs",
|
|
65
|
+
"test:watch": "vitest",
|
|
66
|
+
"typecheck": "tsc --noEmit",
|
|
67
|
+
"prepublishOnly": "tsc -p tsconfig.build.json",
|
|
68
|
+
"release": "release-it"
|
|
69
|
+
},
|
|
70
|
+
"keywords": [
|
|
71
|
+
"codapult",
|
|
72
|
+
"architecture",
|
|
73
|
+
"guardrails",
|
|
74
|
+
"ai-agents",
|
|
75
|
+
"cli",
|
|
76
|
+
"nextjs",
|
|
77
|
+
"javascript",
|
|
78
|
+
"typescript",
|
|
79
|
+
"react",
|
|
80
|
+
"mcp",
|
|
81
|
+
"saas"
|
|
82
|
+
],
|
|
83
|
+
"homepage": "https://github.com/codapult/codapult-guard",
|
|
84
|
+
"repository": {
|
|
85
|
+
"type": "git",
|
|
86
|
+
"url": "https://github.com/codapult/codapult-guard.git"
|
|
87
|
+
},
|
|
88
|
+
"publishConfig": {
|
|
89
|
+
"access": "public"
|
|
90
|
+
},
|
|
91
|
+
"dependencies": {
|
|
92
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
93
|
+
"commander": "^15.0.0",
|
|
94
|
+
"picocolors": "^1.1.1",
|
|
95
|
+
"semver": "^7.8.5",
|
|
96
|
+
"ts-morph": "^28.0.0",
|
|
97
|
+
"typescript": "npm:@typescript/typescript6@^6.0.2",
|
|
98
|
+
"zod": "^4.4.3"
|
|
99
|
+
},
|
|
100
|
+
"devDependencies": {
|
|
101
|
+
"@js-toolkit/config-utils": "^1.0.1",
|
|
102
|
+
"@js-toolkit/eslint-config": "^1.1.4",
|
|
103
|
+
"@js-toolkit/prettier-config": "^1.0.1",
|
|
104
|
+
"@js-toolkit/tsconfig": "^1.0.1",
|
|
105
|
+
"@release-it/conventional-changelog": "^12.0.0",
|
|
106
|
+
"@types/node": "^26.2.0",
|
|
107
|
+
"@typescript/native": "npm:typescript@^7.0.2",
|
|
108
|
+
"@vitest/coverage-v8": "^4.1.11",
|
|
109
|
+
"eslint": "^9.39.4",
|
|
110
|
+
"eslint-config-prettier": "^10.1.8",
|
|
111
|
+
"eslint-plugin-prettier": "^5.5.6",
|
|
112
|
+
"prettier": "^3.9.6",
|
|
113
|
+
"release-it": "^21.0.2",
|
|
114
|
+
"typescript-eslint": "^8.67.0",
|
|
115
|
+
"vitest": "^4.1.11"
|
|
116
|
+
}
|
|
117
|
+
}
|