@codapult/guard 0.8.1 → 0.9.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 +14 -3
- package/dist/cli/commands/guard.d.ts +1 -0
- package/dist/cli/commands/guard.js +21 -1
- package/dist/cli/index.js +2 -1
- package/dist/core/discovery/discovery.js +20 -2
- package/dist/core/guard.d.ts +21 -0
- package/dist/core/guard.js +107 -3
- package/dist/core/history/history.js +20 -2
- package/dist/core/history/runs.js +32 -9
- package/dist/mcp/context.d.ts +48 -0
- package/dist/mcp/context.js +49 -0
- package/dist/mcp/resources.js +9 -6
- package/dist/mcp/tools/guard.js +53 -13
- package/package.json +9 -9
package/README.md
CHANGED
|
@@ -148,10 +148,13 @@ Guard stores policy at the project level, while each policy item can target a na
|
|
|
148
148
|
| Waiver | One active finding fingerprint, with owner, reason, and expiry |
|
|
149
149
|
|
|
150
150
|
Routine checks do not change source code, rules, contracts, baseline, or waivers. Commands that
|
|
151
|
-
change policy are explicit: `init`, `rules
|
|
151
|
+
change policy are explicit: `init`, `rules approve`, `contracts approve|reject`, `baseline accept|remove`, and
|
|
152
152
|
`waiver add|renew|remove`. `analyze` refreshes derived project facts, while `verify` may run the
|
|
153
153
|
project's configured commands and records a local run manifest; neither changes approved policy.
|
|
154
154
|
|
|
155
|
+
For the complete mutation contract and host/CI controls, see
|
|
156
|
+
[Policy mutation control](docs/policy-mutations.md).
|
|
157
|
+
|
|
155
158
|
## What Guard discovers
|
|
156
159
|
|
|
157
160
|
The model is framework-aware without being framework-dependent:
|
|
@@ -325,6 +328,12 @@ task finished
|
|
|
325
328
|
→ repeat until pass or bounded iteration limit
|
|
326
329
|
```
|
|
327
330
|
|
|
331
|
+
`codapult_guard_context` keeps the project state explicit for the host: `observations` are
|
|
332
|
+
discovered facts only, `proposals.pendingRules` and `proposals.pendingContracts` are unapproved
|
|
333
|
+
policy suggestions, `policy` contains only active rules/contracts plus baseline and waiver state,
|
|
334
|
+
and `verification` reports whether a check has actually run. A context response with
|
|
335
|
+
`verification.status: "not-run"` and `findings: null` is not a passing check.
|
|
336
|
+
|
|
328
337
|
For Cursor, Claude Code, Codex, Gemini CLI, GitHub Copilot, and generic hosts, see
|
|
329
338
|
[`docs/integrations/`](docs/integrations/).
|
|
330
339
|
|
|
@@ -398,7 +407,9 @@ cp node_modules/@codapult/guard/docs/guard-ci.yml .github/workflows/guard.yml
|
|
|
398
407
|
```
|
|
399
408
|
|
|
400
409
|
Or copy it from [`docs/guard-ci.yml`](docs/guard-ci.yml). It runs project verification and exports
|
|
401
|
-
Guard findings as SARIF for GitHub code scanning.
|
|
410
|
+
Guard findings as SARIF for GitHub code scanning. Pull requests install dependencies without
|
|
411
|
+
lifecycle scripts and use deterministic Guard checks without executing PR-owned scripts; trusted
|
|
412
|
+
pushes may run the configured project checks.
|
|
402
413
|
|
|
403
414
|
The package repository separately runs its own unit, typecheck, build, and packaging checks in
|
|
404
415
|
[`.github/workflows/ci.yml`](.github/workflows/ci.yml), while the fixture workflow tests Guard on
|
|
@@ -420,7 +431,7 @@ real project shapes.
|
|
|
420
431
|
| `history` / `history-diff` | Inspect project model, module graph, and architecture-edge evolution. |
|
|
421
432
|
| `impact <files...>` | Explain dependencies, transitive dependents, capabilities, and contracts affected by files. |
|
|
422
433
|
| `policy explain <id>` | Explain a policy item, its evidence, and approval history. |
|
|
423
|
-
| `rules` / `contracts` | Approve or reject
|
|
434
|
+
| `rules` / `contracts` | Approve rules; approve or reject contracts. |
|
|
424
435
|
| `baseline` | Review or intentionally accept existing findings. |
|
|
425
436
|
| `waiver` | Manage owner-attributed, time-bounded exceptions to exact findings. |
|
|
426
437
|
|
|
@@ -64,6 +64,7 @@ export declare function guardBaselineCommand(action: 'list' | 'accept' | 'remove
|
|
|
64
64
|
export declare function guardWaiverCommand(action: 'list' | 'add' | 'remove' | 'renew', value?: string, options?: GuardWaiverOptions): void;
|
|
65
65
|
export declare function guardRulesApproveCommand(ids?: string, options?: GuardApprovalOptions): void;
|
|
66
66
|
export declare function guardPolicyExplainCommand(id: string, options?: GuardOutputOptions): void;
|
|
67
|
+
export declare function guardPolicyMutationsCommand(options?: GuardOutputOptions): void;
|
|
67
68
|
export declare function guardContractsApproveCommand(ids?: string, options?: GuardApprovalOptions): void;
|
|
68
69
|
export declare function guardContractsRejectCommand(ids?: string, options?: GuardApprovalOptions): void;
|
|
69
70
|
export declare function guardReviewCommand(options?: GuardReviewOptions): void;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { existsSync, readFileSync } from 'node:fs';
|
|
3
3
|
import { relative, resolve } from 'node:path';
|
|
4
|
-
import { GUARD_BASELINE_FILE, GUARD_BASELINE_META_FILE, GUARD_ARCHITECTURE_FILE, GUARD_CONVENTIONS_FILE, GUARD_AGENT_FILE, GUARD_CONTRACTS_FILE, GUARD_PROPOSALS_FILE, GUARD_PROJECT_FILE, GUARD_HISTORY_DIR, GUARD_RULES_FILE, GuardWaiverError, findGuardRoot, discoverProjectWithMetrics, clearDiscoveryCache, buildGuardReviewPacket, buildGuardProposals, buildGeneratedGuardConfig, discoverProject, GuardAlreadyInitializedError, initializeGuard, loadBaseline, loadGuardWaivers, updateBaseline, updateGuardWaivers, loadGuardConfig, loadGuardProposals, applyGuardProposalDecision, writeGuardProposals, validateGuardProposalApproval, getGuardProposalFreshness, scanGuard, classifyGuardOutcome, validateGuardPolicy, writeProjectState, } from '../../core/guard.js';
|
|
4
|
+
import { GUARD_BASELINE_FILE, GUARD_BASELINE_META_FILE, GUARD_ARCHITECTURE_FILE, GUARD_CONVENTIONS_FILE, GUARD_AGENT_FILE, GUARD_CONTRACTS_FILE, GUARD_PROPOSALS_FILE, GUARD_PROJECT_FILE, GUARD_HISTORY_DIR, GUARD_RULES_FILE, GuardWaiverError, findGuardRoot, discoverProjectWithMetrics, clearDiscoveryCache, buildGuardReviewPacket, buildGuardProposals, buildGeneratedGuardConfig, discoverProject, GuardAlreadyInitializedError, initializeGuard, loadBaseline, loadGuardWaivers, updateBaseline, updateGuardWaivers, loadGuardConfig, loadGuardProposals, applyGuardProposalDecision, writeGuardProposals, validateGuardProposalApproval, getGuardProposalFreshness, scanGuard, classifyGuardOutcome, getGuardPolicyMutationPolicy, validateGuardPolicy, writeProjectState, } from '../../core/guard.js';
|
|
5
5
|
import { guardErrorPayload } from '../../core/errors.js';
|
|
6
6
|
import { runGuardVerification, } from '../../core/verification/verify.js';
|
|
7
7
|
import { diagnoseGuard } from '../../core/analysis/doctor.js';
|
|
@@ -719,6 +719,26 @@ export function guardPolicyExplainCommand(id, options = {}) {
|
|
|
719
719
|
}
|
|
720
720
|
process.exitCode = 0;
|
|
721
721
|
}
|
|
722
|
+
export function guardPolicyMutationsCommand(options = {}) {
|
|
723
|
+
const root = getRoot();
|
|
724
|
+
const loaded = loadConfigSafely(root, options);
|
|
725
|
+
if (loaded.invalid)
|
|
726
|
+
return;
|
|
727
|
+
const policy = getGuardPolicyMutationPolicy(loaded.config);
|
|
728
|
+
if (options.json) {
|
|
729
|
+
console.log(JSON.stringify(policy, null, 2));
|
|
730
|
+
}
|
|
731
|
+
else {
|
|
732
|
+
heading('Guard Policy Mutations');
|
|
733
|
+
info(policy.message);
|
|
734
|
+
for (const operation of policy.operations) {
|
|
735
|
+
info(`${operation.id}: ${operation.surfaces.join(', ')}; ${operation.guardBlocksMcp === true
|
|
736
|
+
? 'MCP blocked in protected mode'
|
|
737
|
+
: 'external control required'}`);
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
process.exitCode = 0;
|
|
741
|
+
}
|
|
722
742
|
export function guardContractsApproveCommand(ids, options = {}) {
|
|
723
743
|
const root = getRoot();
|
|
724
744
|
const loaded = loadConfigSafely(root);
|
package/dist/cli/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
import pc from 'picocolors';
|
|
4
|
-
import { guardAnalyzeCommand, guardAuditCommand, guardBaselineCommand, guardCheckCommand, guardContractsApproveCommand, guardContractsRejectCommand, guardDoctorCommand, guardHistoryCommand, guardHistoryDiffCommand, guardImpactCommand, guardInitCommand, guardInstallAgentCommand, guardGovernanceCommand, guardPolicyExplainCommand, guardProposeCommand, guardReviewCommand, guardRunsCommand, guardRulesApproveCommand, guardVerifyCommand, guardWaiverCommand, } from './commands/guard.js';
|
|
4
|
+
import { guardAnalyzeCommand, guardAuditCommand, guardBaselineCommand, guardCheckCommand, guardContractsApproveCommand, guardContractsRejectCommand, guardDoctorCommand, guardHistoryCommand, guardHistoryDiffCommand, guardImpactCommand, guardInitCommand, guardInstallAgentCommand, guardGovernanceCommand, guardPolicyExplainCommand, guardPolicyMutationsCommand, guardProposeCommand, guardReviewCommand, guardRunsCommand, guardRulesApproveCommand, guardVerifyCommand, guardWaiverCommand, } from './commands/guard.js';
|
|
5
5
|
import { config } from '../core/config.js';
|
|
6
6
|
import { GuardBaselineReasonError, GuardConfigError, GuardStateBusyError, GuardStateStaleError, GuardWaiverError, } from '../core/guard.js';
|
|
7
7
|
import { GuardAgentInstallError } from '../adapters/agents/agent-integration.js';
|
|
@@ -56,6 +56,7 @@ const rules = guard.command('rules');
|
|
|
56
56
|
rules.command('approve [ids]').option('--all').action(guardRulesApproveCommand);
|
|
57
57
|
const policy = guard.command('policy');
|
|
58
58
|
policy.command('explain <id>').option('--json').action(guardPolicyExplainCommand);
|
|
59
|
+
policy.command('mutations').option('--json').action(guardPolicyMutationsCommand);
|
|
59
60
|
const contracts = guard.command('contracts');
|
|
60
61
|
contracts.command('approve [ids]').option('--all').action(guardContractsApproveCommand);
|
|
61
62
|
contracts.command('reject [ids]').option('--all').action(guardContractsRejectCommand);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, renameSync, statSync, writeFileSync, } from 'node:fs';
|
|
3
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, rmSync, renameSync, statSync, writeFileSync, } from 'node:fs';
|
|
4
4
|
import { basename, dirname, extname, join, normalize, relative, resolve } from 'node:path';
|
|
5
5
|
import { Project, SyntaxKind } from 'ts-morph';
|
|
6
6
|
import { config } from '../config.js';
|
|
@@ -61,6 +61,23 @@ const DISCOVERY_CACHE_VERSION = 3;
|
|
|
61
61
|
function discoveryCachePath(root) {
|
|
62
62
|
return resolve(root, `.${config.appName}/guard/cache.json`);
|
|
63
63
|
}
|
|
64
|
+
function canPersistDiscoveryCache(root) {
|
|
65
|
+
const parent = resolve(root, `.${config.appName}`);
|
|
66
|
+
const directory = dirname(discoveryCachePath(root));
|
|
67
|
+
for (const path of [parent, directory]) {
|
|
68
|
+
if (!existsSync(path))
|
|
69
|
+
continue;
|
|
70
|
+
try {
|
|
71
|
+
const stats = lstatSync(path);
|
|
72
|
+
if (stats.isSymbolicLink() || !stats.isDirectory())
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
64
81
|
export function clearDiscoveryCache(root) {
|
|
65
82
|
rmSync(discoveryCachePath(root), { force: true });
|
|
66
83
|
}
|
|
@@ -941,7 +958,8 @@ function discoverProjectOnce(root, options = {}, attempt = 0) {
|
|
|
941
958
|
}
|
|
942
959
|
discoveryCache.set(cacheKey, { signature, model });
|
|
943
960
|
discoveryReuse.set(cacheKey, reusedModules);
|
|
944
|
-
if (
|
|
961
|
+
if (canPersistDiscoveryCache(root) &&
|
|
962
|
+
(options.persistCache ?? existsSync(resolve(root, `.${config.appName}/guard`)))) {
|
|
945
963
|
try {
|
|
946
964
|
mkdirSync(resolve(root, `.${config.appName}/guard`), { recursive: true });
|
|
947
965
|
const cachePath = discoveryCachePath(root);
|
package/dist/core/guard.d.ts
CHANGED
|
@@ -87,11 +87,32 @@ export interface GuardConfig {
|
|
|
87
87
|
approval?: GuardApprovalPolicy | undefined;
|
|
88
88
|
waiverPolicy?: GuardWaiverPolicy | undefined;
|
|
89
89
|
}
|
|
90
|
+
export declare function getGuardPolicyMutationPolicy(guardConfig: GuardConfig | undefined): GuardPolicyMutationPolicy;
|
|
90
91
|
export interface GuardApprovalPolicy {
|
|
91
92
|
mode: GuardApprovalMode;
|
|
92
93
|
allowMcpApproval: boolean;
|
|
93
94
|
requireDistinctActor: boolean;
|
|
94
95
|
}
|
|
96
|
+
export type GuardPolicyMutationId = 'init.force' | 'proposal.approve' | 'proposal.reject' | 'rules.approve' | 'contracts.approve' | 'contracts.reject' | 'baseline.accept' | 'baseline.remove' | 'waiver.add' | 'waiver.renew' | 'waiver.remove' | 'policy-file.edit';
|
|
97
|
+
export interface GuardPolicyMutationDescriptor {
|
|
98
|
+
id: GuardPolicyMutationId;
|
|
99
|
+
surfaces: ('cli' | 'mcp' | 'file')[];
|
|
100
|
+
changesPolicy: true;
|
|
101
|
+
guardConfirmation: boolean;
|
|
102
|
+
reasonRequired: boolean;
|
|
103
|
+
guardBlocksMcp?: boolean | undefined;
|
|
104
|
+
externalApprovalRequired: true;
|
|
105
|
+
externalControl: 'host-or-ci';
|
|
106
|
+
}
|
|
107
|
+
export interface GuardPolicyMutationPolicy {
|
|
108
|
+
version: 1;
|
|
109
|
+
status: 'declared';
|
|
110
|
+
humanOnlyEnforcedByGuard: false;
|
|
111
|
+
message: string;
|
|
112
|
+
approvalMode: GuardApprovalMode;
|
|
113
|
+
requireDistinctActor: boolean;
|
|
114
|
+
operations: GuardPolicyMutationDescriptor[];
|
|
115
|
+
}
|
|
95
116
|
export interface GuardProposalFile {
|
|
96
117
|
version: 1;
|
|
97
118
|
generatedAt: string;
|
package/dist/core/guard.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
3
|
import { hostname } from 'node:os';
|
|
4
|
-
import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
4
|
+
import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
5
5
|
import { dirname, extname, relative, resolve, sep } from 'node:path';
|
|
6
6
|
import { Project, SyntaxKind } from 'ts-morph';
|
|
7
7
|
import { buildModuleTargetGraph } from './discovery/discovery.js';
|
|
@@ -19,6 +19,79 @@ export function classifyGuardOutcome(input) {
|
|
|
19
19
|
return 'needs-review';
|
|
20
20
|
return (input.warnings ?? 0) > 0 ? 'warning' : 'pass';
|
|
21
21
|
}
|
|
22
|
+
export function getGuardPolicyMutationPolicy(guardConfig) {
|
|
23
|
+
const approval = guardConfig?.approval ?? {
|
|
24
|
+
mode: 'local',
|
|
25
|
+
allowMcpApproval: true,
|
|
26
|
+
requireDistinctActor: false,
|
|
27
|
+
};
|
|
28
|
+
const protectedMode = approval.mode === 'protected';
|
|
29
|
+
const mcpApprovalBlocked = protectedMode && !approval.allowMcpApproval;
|
|
30
|
+
const descriptor = (id, surfaces, options) => ({
|
|
31
|
+
id,
|
|
32
|
+
surfaces,
|
|
33
|
+
changesPolicy: true,
|
|
34
|
+
...options,
|
|
35
|
+
...(options.guardBlocksMcp !== undefined ? { guardBlocksMcp: options.guardBlocksMcp } : {}),
|
|
36
|
+
externalApprovalRequired: true,
|
|
37
|
+
externalControl: 'host-or-ci',
|
|
38
|
+
});
|
|
39
|
+
return {
|
|
40
|
+
version: 1,
|
|
41
|
+
status: 'declared',
|
|
42
|
+
humanOnlyEnforcedByGuard: false,
|
|
43
|
+
message: 'Guard identifies policy mutations and applies its configured checks; a host or CI system must enforce human/external approval.',
|
|
44
|
+
approvalMode: approval.mode,
|
|
45
|
+
requireDistinctActor: approval.requireDistinctActor,
|
|
46
|
+
operations: [
|
|
47
|
+
descriptor('init.force', ['cli', 'mcp'], {
|
|
48
|
+
guardConfirmation: true,
|
|
49
|
+
reasonRequired: false,
|
|
50
|
+
guardBlocksMcp: mcpApprovalBlocked,
|
|
51
|
+
}),
|
|
52
|
+
descriptor('proposal.approve', ['mcp'], {
|
|
53
|
+
guardConfirmation: true,
|
|
54
|
+
reasonRequired: false,
|
|
55
|
+
guardBlocksMcp: mcpApprovalBlocked,
|
|
56
|
+
}),
|
|
57
|
+
descriptor('proposal.reject', ['mcp'], {
|
|
58
|
+
guardConfirmation: true,
|
|
59
|
+
reasonRequired: false,
|
|
60
|
+
guardBlocksMcp: false,
|
|
61
|
+
}),
|
|
62
|
+
descriptor('rules.approve', ['cli'], { guardConfirmation: false, reasonRequired: false }),
|
|
63
|
+
descriptor('contracts.approve', ['cli'], {
|
|
64
|
+
guardConfirmation: false,
|
|
65
|
+
reasonRequired: false,
|
|
66
|
+
}),
|
|
67
|
+
descriptor('contracts.reject', ['cli'], {
|
|
68
|
+
guardConfirmation: false,
|
|
69
|
+
reasonRequired: false,
|
|
70
|
+
}),
|
|
71
|
+
descriptor('baseline.accept', ['cli'], { guardConfirmation: false, reasonRequired: true }),
|
|
72
|
+
descriptor('baseline.remove', ['cli'], { guardConfirmation: false, reasonRequired: true }),
|
|
73
|
+
descriptor('waiver.add', ['cli', 'mcp'], {
|
|
74
|
+
guardConfirmation: true,
|
|
75
|
+
reasonRequired: true,
|
|
76
|
+
guardBlocksMcp: mcpApprovalBlocked,
|
|
77
|
+
}),
|
|
78
|
+
descriptor('waiver.renew', ['cli', 'mcp'], {
|
|
79
|
+
guardConfirmation: true,
|
|
80
|
+
reasonRequired: true,
|
|
81
|
+
guardBlocksMcp: mcpApprovalBlocked,
|
|
82
|
+
}),
|
|
83
|
+
descriptor('waiver.remove', ['cli', 'mcp'], {
|
|
84
|
+
guardConfirmation: true,
|
|
85
|
+
reasonRequired: true,
|
|
86
|
+
guardBlocksMcp: mcpApprovalBlocked,
|
|
87
|
+
}),
|
|
88
|
+
descriptor('policy-file.edit', ['file'], {
|
|
89
|
+
guardConfirmation: false,
|
|
90
|
+
reasonRequired: false,
|
|
91
|
+
}),
|
|
92
|
+
],
|
|
93
|
+
};
|
|
94
|
+
}
|
|
22
95
|
export const GUARD_DIR = `.${config.appName}/guard`;
|
|
23
96
|
export const GUARD_RULES_FILE = `${GUARD_DIR}/rules.json`;
|
|
24
97
|
export const GUARD_BASELINE_FILE = `${GUARD_DIR}/baseline.json`;
|
|
@@ -144,8 +217,24 @@ function getActiveStateRoot(path) {
|
|
|
144
217
|
function safeGeneration(value) {
|
|
145
218
|
return typeof value === 'string' && /^[A-Za-z0-9._-]+$/.test(value);
|
|
146
219
|
}
|
|
220
|
+
function assertGuardStateDirectory(root) {
|
|
221
|
+
const stateParent = resolve(root, dirname(GUARD_DIR));
|
|
222
|
+
const stateDirectory = resolve(root, GUARD_DIR);
|
|
223
|
+
for (const path of [stateParent, stateDirectory]) {
|
|
224
|
+
if (!existsSync(path))
|
|
225
|
+
continue;
|
|
226
|
+
const stats = lstatSync(path);
|
|
227
|
+
if (stats.isSymbolicLink()) {
|
|
228
|
+
throw new GuardConfigError(`${path} must not be a symbolic link.`);
|
|
229
|
+
}
|
|
230
|
+
if (!stats.isDirectory()) {
|
|
231
|
+
throw new GuardConfigError(`${path} must be a directory.`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
}
|
|
147
235
|
export function withGuardStateLock(root, callback, options = {}) {
|
|
148
236
|
const normalizedRoot = resolve(root);
|
|
237
|
+
assertGuardStateDirectory(normalizedRoot);
|
|
149
238
|
const activeDepth = activeStateLocks.get(normalizedRoot);
|
|
150
239
|
if (activeDepth !== undefined) {
|
|
151
240
|
activeStateLocks.set(normalizedRoot, activeDepth + 1);
|
|
@@ -222,6 +311,15 @@ function isGuardConfig(value) {
|
|
|
222
311
|
return guardConfigSchema.safeParse(value).success;
|
|
223
312
|
}
|
|
224
313
|
function readJson(filePath) {
|
|
314
|
+
let stats;
|
|
315
|
+
try {
|
|
316
|
+
stats = lstatSync(filePath);
|
|
317
|
+
}
|
|
318
|
+
catch {
|
|
319
|
+
return undefined;
|
|
320
|
+
}
|
|
321
|
+
if (stats.isSymbolicLink())
|
|
322
|
+
throw new GuardConfigError(filePath);
|
|
225
323
|
try {
|
|
226
324
|
return JSON.parse(readFileSync(filePath, 'utf8'));
|
|
227
325
|
}
|
|
@@ -555,9 +653,15 @@ export function loadBaseline(root) {
|
|
|
555
653
|
if (!existsSync(path))
|
|
556
654
|
return new Set();
|
|
557
655
|
const value = readJson(path);
|
|
558
|
-
if (!Array.isArray(value))
|
|
656
|
+
if (!Array.isArray(value)) {
|
|
559
657
|
throw new GuardConfigError(GUARD_BASELINE_FILE);
|
|
560
|
-
|
|
658
|
+
}
|
|
659
|
+
const entries = value;
|
|
660
|
+
const stringEntries = entries.filter((item) => typeof item === 'string');
|
|
661
|
+
if (stringEntries.length !== entries.length) {
|
|
662
|
+
throw new GuardConfigError(GUARD_BASELINE_FILE);
|
|
663
|
+
}
|
|
664
|
+
return new Set(stringEntries);
|
|
561
665
|
}
|
|
562
666
|
function validateGuardWaiversFile(value) {
|
|
563
667
|
const ids = new Set();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, lstatSync, readdirSync, readFileSync } from 'node:fs';
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
3
|
import { GUARD_HISTORY_DIR } from '../guard.js';
|
|
4
4
|
function snapshotInsights(model) {
|
|
@@ -18,7 +18,10 @@ function readSnapshot(root, revision) {
|
|
|
18
18
|
if (!/^[A-Za-z0-9._-]+$/.test(revision))
|
|
19
19
|
return undefined;
|
|
20
20
|
try {
|
|
21
|
-
|
|
21
|
+
const path = resolve(root, GUARD_HISTORY_DIR, `${revision}.json`);
|
|
22
|
+
if (!lstatSync(path).isFile())
|
|
23
|
+
return undefined;
|
|
24
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
22
25
|
}
|
|
23
26
|
catch {
|
|
24
27
|
return undefined;
|
|
@@ -28,8 +31,23 @@ export function listGuardSnapshots(root) {
|
|
|
28
31
|
const directory = resolve(root, GUARD_HISTORY_DIR);
|
|
29
32
|
if (!existsSync(directory))
|
|
30
33
|
return [];
|
|
34
|
+
try {
|
|
35
|
+
if (lstatSync(directory).isSymbolicLink() || !lstatSync(directory).isDirectory())
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
31
41
|
return readdirSync(directory)
|
|
32
42
|
.filter((file) => file.endsWith('.json'))
|
|
43
|
+
.filter((file) => {
|
|
44
|
+
try {
|
|
45
|
+
return lstatSync(resolve(directory, file)).isFile();
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
})
|
|
33
51
|
.sort()
|
|
34
52
|
.flatMap((file) => {
|
|
35
53
|
const revision = file.slice(0, -5);
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { randomUUID } from 'node:crypto';
|
|
2
2
|
import { execFileSync } from 'node:child_process';
|
|
3
|
-
import { mkdirSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
3
|
+
import { existsSync, mkdirSync, lstatSync, readdirSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
4
4
|
import { resolve } from 'node:path';
|
|
5
5
|
import { config } from '../config.js';
|
|
6
|
+
import { withGuardStateLock } from '../guard.js';
|
|
6
7
|
export const GUARD_RUNS_DIR = `.${config.appName}/guard/history/runs`;
|
|
7
8
|
export const GUARD_RUN_RETENTION = 200;
|
|
8
9
|
export class GuardRunManifestError extends Error {
|
|
@@ -85,11 +86,15 @@ export function recordGuardRunStage(context, name, status, startedAtMs, detail)
|
|
|
85
86
|
function pruneGuardRuns(root) {
|
|
86
87
|
const directory = resolve(root, GUARD_RUNS_DIR);
|
|
87
88
|
try {
|
|
89
|
+
if (lstatSync(directory).isSymbolicLink() || !lstatSync(directory).isDirectory())
|
|
90
|
+
return;
|
|
88
91
|
const files = readdirSync(directory)
|
|
89
92
|
.filter((file) => file.endsWith('.json'))
|
|
90
93
|
.map((file) => ({
|
|
91
94
|
file,
|
|
92
|
-
modifiedAt:
|
|
95
|
+
modifiedAt: lstatSync(resolve(directory, file)).isSymbolicLink()
|
|
96
|
+
? 0
|
|
97
|
+
: statSync(resolve(directory, file)).mtimeMs,
|
|
93
98
|
}))
|
|
94
99
|
.sort((left, right) => right.modifiedAt - left.modifiedAt);
|
|
95
100
|
for (const entry of files.slice(GUARD_RUN_RETENTION)) {
|
|
@@ -108,13 +113,20 @@ function pruneGuardRuns(root) {
|
|
|
108
113
|
export function writeGuardRun(root, manifest) {
|
|
109
114
|
if (!isGuardRunManifest(manifest))
|
|
110
115
|
throw new GuardRunManifestError();
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
116
|
+
withGuardStateLock(root, () => {
|
|
117
|
+
const directory = resolve(root, GUARD_RUNS_DIR);
|
|
118
|
+
if (existsSync(directory)) {
|
|
119
|
+
const stats = lstatSync(directory);
|
|
120
|
+
if (stats.isSymbolicLink() || !stats.isDirectory())
|
|
121
|
+
throw new GuardRunManifestError();
|
|
122
|
+
}
|
|
123
|
+
mkdirSync(directory, { recursive: true });
|
|
124
|
+
const path = resolve(directory, `${manifest.runId}.json`);
|
|
125
|
+
const temporaryPath = `${path}.tmp-${process.pid}-${manifest.runId}`;
|
|
126
|
+
writeFileSync(temporaryPath, `${JSON.stringify(manifest, null, 2)}\n`, 'utf8');
|
|
127
|
+
renameSync(temporaryPath, path);
|
|
128
|
+
pruneGuardRuns(root);
|
|
129
|
+
});
|
|
118
130
|
}
|
|
119
131
|
/** Reads locally persisted run manifests without reaching a remote service. */
|
|
120
132
|
export function listGuardRuns(root, limit = 20) {
|
|
@@ -123,8 +135,19 @@ export function listGuardRuns(root, limit = 20) {
|
|
|
123
135
|
const boundedLimit = Math.min(limit, 1_000);
|
|
124
136
|
const directory = resolve(root, GUARD_RUNS_DIR);
|
|
125
137
|
try {
|
|
138
|
+
const directoryStats = lstatSync(directory);
|
|
139
|
+
if (directoryStats.isSymbolicLink() || !directoryStats.isDirectory())
|
|
140
|
+
return [];
|
|
126
141
|
return readdirSync(directory)
|
|
127
142
|
.filter((file) => file.endsWith('.json'))
|
|
143
|
+
.filter((file) => {
|
|
144
|
+
try {
|
|
145
|
+
return lstatSync(resolve(directory, file)).isFile();
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
})
|
|
128
151
|
.map((file) => {
|
|
129
152
|
try {
|
|
130
153
|
const value = JSON.parse(readFileSync(resolve(directory, file), 'utf8'));
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { ProjectModel } from '../core/discovery/discovery.js';
|
|
2
|
+
import { getPendingGuardProposals, type GuardConfig, type GuardProposalFile, type GuardWaiver } from '../core/guard.js';
|
|
3
|
+
export interface GuardDiscoveryMetrics {
|
|
4
|
+
durationMs: number;
|
|
5
|
+
files: number;
|
|
6
|
+
modules: number;
|
|
7
|
+
changedFiles: number;
|
|
8
|
+
reusedModules: number;
|
|
9
|
+
cacheHit: boolean;
|
|
10
|
+
cacheAvailable: boolean;
|
|
11
|
+
}
|
|
12
|
+
export interface GuardContextState {
|
|
13
|
+
observations: {
|
|
14
|
+
status: 'observed-only';
|
|
15
|
+
project: ProjectModel;
|
|
16
|
+
architecture: Record<string, unknown>;
|
|
17
|
+
conventions: Record<string, unknown>;
|
|
18
|
+
discovery: GuardDiscoveryMetrics | undefined;
|
|
19
|
+
};
|
|
20
|
+
proposals: {
|
|
21
|
+
status: 'not-configured' | 'none' | 'pending-approval' | 'stale' | 'decided';
|
|
22
|
+
requiresApproval: boolean;
|
|
23
|
+
freshness: 'current' | 'stale' | 'unknown' | 'none';
|
|
24
|
+
proposalId?: string | undefined;
|
|
25
|
+
generatedBy?: string | undefined;
|
|
26
|
+
pendingRules: GuardConfig['rules'];
|
|
27
|
+
pendingContracts: NonNullable<GuardConfig['contracts']>;
|
|
28
|
+
questions: string[];
|
|
29
|
+
pending: ReturnType<typeof getPendingGuardProposals>;
|
|
30
|
+
};
|
|
31
|
+
policy: {
|
|
32
|
+
status: 'not-configured' | 'active';
|
|
33
|
+
rules: GuardConfig['rules'];
|
|
34
|
+
contracts: NonNullable<GuardConfig['contracts']>;
|
|
35
|
+
baseline: {
|
|
36
|
+
status: 'not-configured' | 'active';
|
|
37
|
+
count: number;
|
|
38
|
+
};
|
|
39
|
+
waivers: GuardWaiver[];
|
|
40
|
+
};
|
|
41
|
+
verification: {
|
|
42
|
+
status: 'not-run';
|
|
43
|
+
findings: null;
|
|
44
|
+
message: string;
|
|
45
|
+
next: 'codapult_guard_init' | 'codapult_guard_check';
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
export declare function buildGuardContextState(project: ProjectModel, discovery: GuardDiscoveryMetrics | undefined, architecture: Record<string, unknown>, conventions: Record<string, unknown>, config: GuardConfig | undefined, proposals: GuardProposalFile | undefined, baseline: Set<string>, waivers: GuardWaiver[]): GuardContextState;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { getGuardProposalFreshness, getPendingGuardProposals, } from '../core/guard.js';
|
|
2
|
+
export function buildGuardContextState(project, discovery, architecture, conventions, config, proposals, baseline, waivers) {
|
|
3
|
+
const pending = getPendingGuardProposals(proposals);
|
|
4
|
+
const freshness = proposals ? getGuardProposalFreshness(project, proposals) : 'none';
|
|
5
|
+
const pendingRuleIds = new Set(pending.filter((item) => item.type === 'rule').map((item) => item.id));
|
|
6
|
+
const pendingContractIds = new Set(pending.filter((item) => item.type === 'contract').map((item) => item.id));
|
|
7
|
+
const pendingRules = proposals?.rules.filter((rule) => rule.status === 'proposed' && pendingRuleIds.has(rule.id)) ??
|
|
8
|
+
[];
|
|
9
|
+
const pendingContracts = proposals?.contracts.filter((contract) => contract.status === 'proposed' && pendingContractIds.has(contract.id)) ?? [];
|
|
10
|
+
const proposalStatus = config === undefined
|
|
11
|
+
? 'not-configured'
|
|
12
|
+
: proposals === undefined
|
|
13
|
+
? 'none'
|
|
14
|
+
: freshness === 'stale'
|
|
15
|
+
? 'stale'
|
|
16
|
+
: pending.length > 0
|
|
17
|
+
? 'pending-approval'
|
|
18
|
+
: 'decided';
|
|
19
|
+
return {
|
|
20
|
+
observations: { status: 'observed-only', project, architecture, conventions, discovery },
|
|
21
|
+
proposals: {
|
|
22
|
+
status: proposalStatus,
|
|
23
|
+
requiresApproval: pending.length > 0 || freshness === 'stale',
|
|
24
|
+
freshness,
|
|
25
|
+
...(proposals?.proposalId ? { proposalId: proposals.proposalId } : {}),
|
|
26
|
+
...(proposals?.generatedBy ? { generatedBy: proposals.generatedBy } : {}),
|
|
27
|
+
pendingRules,
|
|
28
|
+
pendingContracts,
|
|
29
|
+
questions: proposals?.questions ?? [],
|
|
30
|
+
pending,
|
|
31
|
+
},
|
|
32
|
+
policy: {
|
|
33
|
+
status: config === undefined ? 'not-configured' : 'active',
|
|
34
|
+
rules: config?.rules.filter((rule) => rule.status === 'active') ?? [],
|
|
35
|
+
contracts: config?.contracts?.filter((contract) => contract.status === 'active') ?? [],
|
|
36
|
+
baseline: {
|
|
37
|
+
status: config === undefined ? 'not-configured' : 'active',
|
|
38
|
+
count: baseline.size,
|
|
39
|
+
},
|
|
40
|
+
waivers,
|
|
41
|
+
},
|
|
42
|
+
verification: {
|
|
43
|
+
status: 'not-run',
|
|
44
|
+
findings: null,
|
|
45
|
+
message: 'Context contains facts and policy state; it does not run verification.',
|
|
46
|
+
next: config === undefined ? 'codapult_guard_init' : 'codapult_guard_check',
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
}
|
package/dist/mcp/resources.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { buildArchitectureMemory, buildConventionsMemory, discoverProject, findGuardRoot, GUARD_ARCHITECTURE_FILE, GUARD_CONVENTIONS_FILE, loadGuardAgentConfig, loadGuardArtifact, loadGuardConfig, loadGuardProposals, loadGuardWaivers, GuardStateBusyError, GuardStateStaleError, } from '../core/guard.js';
|
|
1
|
+
import { buildArchitectureMemory, buildConventionsMemory, discoverProject, findGuardRoot, GUARD_ARCHITECTURE_FILE, GUARD_CONVENTIONS_FILE, getGuardPolicyMutationPolicy, loadBaseline, loadGuardAgentConfig, loadGuardArtifact, loadGuardConfig, loadGuardProposals, loadGuardWaivers, GuardStateBusyError, GuardStateStaleError, } from '../core/guard.js';
|
|
2
|
+
import { buildGuardContextState } from './context.js';
|
|
2
3
|
import { guardErrorPayload } from '../core/errors.js';
|
|
3
4
|
import { DiscoveryStaleError } from '../core/discovery/discovery.js';
|
|
4
5
|
import { listGuardRuns, summarizeGuardRuns } from '../core/history/runs.js';
|
|
@@ -62,15 +63,17 @@ export function registerGuardResources(server) {
|
|
|
62
63
|
try {
|
|
63
64
|
const project = discoverProject(root);
|
|
64
65
|
const config = loadGuardConfig(root);
|
|
66
|
+
const architecture = (loadGuardArtifact(root, GUARD_ARCHITECTURE_FILE) ??
|
|
67
|
+
buildArchitectureMemory(project));
|
|
68
|
+
const conventions = (loadGuardArtifact(root, GUARD_CONVENTIONS_FILE) ??
|
|
69
|
+
buildConventionsMemory(project));
|
|
70
|
+
const state = buildGuardContextState(project, undefined, architecture, conventions, config, loadGuardProposals(root), loadBaseline(root), loadGuardWaivers(root));
|
|
65
71
|
return jsonResource('codapult://guard/context', {
|
|
66
72
|
version: 1,
|
|
67
73
|
root,
|
|
68
74
|
configured: config !== undefined,
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
conventions: loadGuardArtifact(root, GUARD_CONVENTIONS_FILE) ?? buildConventionsMemory(project),
|
|
72
|
-
rules: config?.rules ?? [],
|
|
73
|
-
contracts: config?.contracts ?? [],
|
|
75
|
+
mutationPolicy: getGuardPolicyMutationPolicy(config),
|
|
76
|
+
...state,
|
|
74
77
|
agent: loadGuardAgentConfig(root),
|
|
75
78
|
});
|
|
76
79
|
}
|
package/dist/mcp/tools/guard.js
CHANGED
|
@@ -1,11 +1,17 @@
|
|
|
1
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
|
+
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, getGuardPolicyMutationPolicy, } from '../../core/guard.js';
|
|
3
3
|
import { guardErrorPayload } from '../../core/errors.js';
|
|
4
4
|
import { analyzeProjectImpact } from '../../core/analysis/impact.js';
|
|
5
5
|
import { z } from 'zod';
|
|
6
6
|
import { runGuardVerification } from '../../core/verification/verify.js';
|
|
7
7
|
import { auditGuardGovernance } from '../../core/audit/governance.js';
|
|
8
|
+
import { buildGuardContextState } from '../context.js';
|
|
8
9
|
const rootSchema = z.string().trim().min(1).optional().describe('Project path or workspace root.');
|
|
10
|
+
function asGuardContextRecord(value) {
|
|
11
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
12
|
+
? value
|
|
13
|
+
: {};
|
|
14
|
+
}
|
|
9
15
|
function getGuardRoot(root) {
|
|
10
16
|
return root ? findGuardRoot(root) : findGuardRoot();
|
|
11
17
|
}
|
|
@@ -61,6 +67,21 @@ function loadBaselineSafely(root) {
|
|
|
61
67
|
};
|
|
62
68
|
}
|
|
63
69
|
}
|
|
70
|
+
function loadWaiversSafely(root) {
|
|
71
|
+
try {
|
|
72
|
+
return { waivers: loadGuardWaivers(root) };
|
|
73
|
+
}
|
|
74
|
+
catch (error) {
|
|
75
|
+
return {
|
|
76
|
+
error: jsonToolResult(guardErrorPayload('GUARD_CONFIG_INVALID', error instanceof Error ? error.message : String(error), {
|
|
77
|
+
configured: true,
|
|
78
|
+
outcome: 'error',
|
|
79
|
+
recoverable: true,
|
|
80
|
+
hint: 'Repair waivers.json, then run codapult-guard doctor.',
|
|
81
|
+
}), true),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
}
|
|
64
85
|
function stateErrorResult(error) {
|
|
65
86
|
const isBusy = error instanceof GuardStateBusyError;
|
|
66
87
|
const isStale = error instanceof GuardStateStaleError;
|
|
@@ -247,6 +268,22 @@ export function registerGuardTools(server) {
|
|
|
247
268
|
? discoverProjectWithMetrics(root, { persistCache: true })
|
|
248
269
|
: { model: loadProjectModel(root) ?? discoverProject(root), metrics: undefined };
|
|
249
270
|
const project = discovery.model;
|
|
271
|
+
const architecture = asGuardContextRecord(refresh
|
|
272
|
+
? buildArchitectureMemory(project)
|
|
273
|
+
: (loadGuardArtifact(root, GUARD_ARCHITECTURE_FILE) ?? buildArchitectureMemory(project)));
|
|
274
|
+
const conventions = asGuardContextRecord(refresh
|
|
275
|
+
? buildConventionsMemory(project)
|
|
276
|
+
: (loadGuardArtifact(root, GUARD_CONVENTIONS_FILE) ?? buildConventionsMemory(project)));
|
|
277
|
+
const loadedProposals = loadProposalsSafely(root);
|
|
278
|
+
if ('error' in loadedProposals)
|
|
279
|
+
return loadedProposals.error;
|
|
280
|
+
const loadedBaseline = loadBaselineSafely(root);
|
|
281
|
+
if (loadedBaseline.error)
|
|
282
|
+
return loadedBaseline.error;
|
|
283
|
+
const loadedWaivers = loadWaiversSafely(root);
|
|
284
|
+
if (loadedWaivers.error)
|
|
285
|
+
return loadedWaivers.error;
|
|
286
|
+
const state = buildGuardContextState(project, discovery.metrics, architecture, conventions, config, loadedProposals.proposals, loadedBaseline.baseline ?? new Set(), loadedWaivers.waivers ?? []);
|
|
250
287
|
return {
|
|
251
288
|
content: [
|
|
252
289
|
{
|
|
@@ -259,18 +296,8 @@ export function registerGuardTools(server) {
|
|
|
259
296
|
tools: agentConfig.tools,
|
|
260
297
|
tooling: agentConfig.tooling,
|
|
261
298
|
completionGate: agentConfig.completionGate,
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
architecture: refresh
|
|
265
|
-
? buildArchitectureMemory(project)
|
|
266
|
-
: (loadGuardArtifact(root, GUARD_ARCHITECTURE_FILE) ??
|
|
267
|
-
buildArchitectureMemory(project)),
|
|
268
|
-
conventions: refresh
|
|
269
|
-
? buildConventionsMemory(project)
|
|
270
|
-
: (loadGuardArtifact(root, GUARD_CONVENTIONS_FILE) ??
|
|
271
|
-
buildConventionsMemory(project)),
|
|
272
|
-
project,
|
|
273
|
-
discovery: discovery.metrics,
|
|
299
|
+
mutationPolicy: getGuardPolicyMutationPolicy(config),
|
|
300
|
+
...state,
|
|
274
301
|
}, null, 2),
|
|
275
302
|
},
|
|
276
303
|
],
|
|
@@ -337,6 +364,19 @@ export function registerGuardTools(server) {
|
|
|
337
364
|
const root = getGuardRoot(requestedRoot);
|
|
338
365
|
if (!confirm)
|
|
339
366
|
return jsonToolResult({ status: 'needs-confirmation', root, force });
|
|
367
|
+
const loadedConfig = loadConfigSafely(root);
|
|
368
|
+
if (loadedConfig.error)
|
|
369
|
+
return loadedConfig.error;
|
|
370
|
+
if (force &&
|
|
371
|
+
loadedConfig.config?.approval?.mode === 'protected' &&
|
|
372
|
+
!loadedConfig.config.approval.allowMcpApproval) {
|
|
373
|
+
return jsonToolResult({
|
|
374
|
+
status: 'approval-required',
|
|
375
|
+
message: 'Protected Guard policy requires forced initialization outside the MCP agent surface.',
|
|
376
|
+
source: 'mcp',
|
|
377
|
+
force,
|
|
378
|
+
}, true);
|
|
379
|
+
}
|
|
340
380
|
try {
|
|
341
381
|
const result = initializeGuard(root, { force, waitMs: wait_ms, noWait: no_wait });
|
|
342
382
|
return jsonToolResult({ status: 'ok', root, report: result.report });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@codapult/guard",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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",
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
"access": "public"
|
|
99
99
|
},
|
|
100
100
|
"dependencies": {
|
|
101
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
101
|
+
"@modelcontextprotocol/sdk": "^1.32.1",
|
|
102
102
|
"commander": "^15.0.0",
|
|
103
103
|
"picocolors": "^1.1.1",
|
|
104
104
|
"semver": "^7.8.5",
|
|
@@ -112,15 +112,15 @@
|
|
|
112
112
|
"@js-toolkit/prettier-config": "^1.0.1",
|
|
113
113
|
"@js-toolkit/tsconfig": "^1.0.1",
|
|
114
114
|
"@release-it/conventional-changelog": "^12.0.2",
|
|
115
|
-
"@types/node": "^26.6.
|
|
115
|
+
"@types/node": "^26.6.5",
|
|
116
116
|
"@typescript/native": "npm:typescript@^7.0.2",
|
|
117
|
-
"@vitest/coverage-v8": "^5.0.
|
|
118
|
-
"eslint": "^10.
|
|
117
|
+
"@vitest/coverage-v8": "^5.0.3",
|
|
118
|
+
"eslint": "^10.12.0",
|
|
119
119
|
"eslint-config-prettier": "^10.1.8",
|
|
120
120
|
"eslint-plugin-prettier": "^5.5.6",
|
|
121
|
-
"prettier": "^3.9.
|
|
122
|
-
"release-it": "^21.1.
|
|
123
|
-
"typescript-eslint": "^8.
|
|
124
|
-
"vitest": "^5.0.
|
|
121
|
+
"prettier": "^3.9.10",
|
|
122
|
+
"release-it": "^21.1.1",
|
|
123
|
+
"typescript-eslint": "^8.71.1",
|
|
124
|
+
"vitest": "^5.0.3"
|
|
125
125
|
}
|
|
126
126
|
}
|