@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 CHANGED
@@ -168,6 +168,10 @@ Guard does not assume a fixed `UI → actions → services → repositories →
168
168
  It can discover that shape when the project exhibits it, but observed patterns become enforceable
169
169
  only after explicit approval.
170
170
 
171
+ Policy paths are validated as project-relative paths. Guard rejects traversal, absolute paths, and
172
+ symlinks escaping the project root; import-boundary checks include imports, re-exports, and literal
173
+ dynamic imports.
174
+
171
175
  ## What Guard discovers
172
176
 
173
177
  The model is framework-aware without being framework-dependent:
@@ -186,6 +190,15 @@ The model is framework-aware without being framework-dependent:
186
190
  Capabilities are evidence, not requirements. A Vite app, Express service, Hono project, Node
187
191
  package, monorepo, or Next.js SaaS can all use the same Guard core.
188
192
 
193
+ ## What Guard is — and is not
194
+
195
+ Guard is an architecture control plane for AI-assisted development. It protects project-specific
196
+ boundaries and change impact using local facts, approved policy, and deterministic verification.
197
+
198
+ Guard is not a replacement for ESLint, TypeScript, tests, SAST, or a general-purpose PR bot. Those
199
+ tools answer different questions; Guard connects their results with the architectural memory that
200
+ an AI coding agent needs before and after changing a repository.
201
+
189
202
  ## The normal loop
190
203
 
191
204
  ```text
@@ -218,12 +231,37 @@ Guard state is stored in `.codapult/guard/`:
218
231
  ├── proposals.json evidence and approval history
219
232
  ├── baseline.json accepted pre-existing findings
220
233
  ├── agent.json AI-host completion-gate configuration
221
- └── history/ project snapshots for comparison
234
+ └── history/ project snapshots and local verification run manifests
222
235
  ```
223
236
 
224
237
  Commit policy and baseline files when the team wants shared guardrails. Treat cache artifacts as
225
238
  disposable according to the project’s policy, and never commit secrets.
226
239
 
240
+ ### Optional scoped budgets
241
+
242
+ Budgets are policy, not a universal style rule. Add them only for a named risk boundary such as a
243
+ service, route handler, or dependency-heavy module:
244
+
245
+ ```json
246
+ {
247
+ "id": "service-lines",
248
+ "description": "Services must remain reviewable.",
249
+ "metric": "lines",
250
+ "scope": ["src/services"],
251
+ "limit": 300,
252
+ "severity": "warning",
253
+ "reason": "Keep service changes reviewable by one owner.",
254
+ "status": "active"
255
+ }
256
+ ```
257
+
258
+ Place budgets in the `budgets` array in `.codapult/guard/rules.json`. Supported metrics are
259
+ `lines`, `bytes`, and `imports`. Generated files, schemas, migrations, and other paths are not
260
+ checked unless they are explicitly included in `scope`; a wildcard such as `*` can intentionally
261
+ cover the project root. Invalid, absolute, parent-directory, or nonexistent scopes fail the Guard
262
+ gate. Changing a budget is a policy decision:
263
+ review the diff and update its written `reason` rather than silently increasing the limit.
264
+
227
265
  ## AI agents and MCP
228
266
 
229
267
  Guard is deliberately model-agnostic. It does not send source code to a remote LLM and does not
@@ -259,6 +297,7 @@ Recommended agent loop:
259
297
 
260
298
  ```text
261
299
  task finished
300
+ → codapult_guard_next_action
262
301
  → codapult_guard_context
263
302
  → codapult_guard_review(requirement, diff)
264
303
  → codapult_guard_verify
@@ -269,6 +308,25 @@ task finished
269
308
  For Cursor, Claude Code, Codex, Gemini CLI, GitHub Copilot, and generic hosts, see
270
309
  [`docs/integrations/`](docs/integrations/).
271
310
 
311
+ For workflows where the authoring agent must not approve its own policy proposals, set Guard to
312
+ protected mode in `rules.json`:
313
+
314
+ ```json
315
+ {
316
+ "approval": {
317
+ "mode": "protected",
318
+ "allowMcpApproval": false,
319
+ "requireDistinctActor": true
320
+ }
321
+ }
322
+ ```
323
+
324
+ MCP can then read and propose policy, while approval happens through the CLI or a protected CI/PR
325
+ process. With `requireDistinctActor`, CLI and permitted MCP approvals require `GUARD_APPROVER`,
326
+ and Guard rejects the same declared actor when a proposal was generated with `GUARD_PROPOSER`.
327
+ These variables provide declared provenance only; external branch protection or signed identity
328
+ remains responsible for proving who approved the change.
329
+
272
330
  ## CI
273
331
 
274
332
  Copy the consumer workflow into a project that has installed and initialized Guard:
@@ -298,11 +356,25 @@ real project shapes.
298
356
  | `doctor` | Diagnose invalid or missing Guard artifacts. |
299
357
  | `history` / `history-diff` | Inspect project model, module graph, and architecture-edge evolution. |
300
358
  | `impact <files...>` | Explain dependencies, transitive dependents, capabilities, and contracts affected by files. |
359
+ | `policy explain <id>` | Explain a policy item, its evidence, and approval history. |
301
360
  | `rules` / `contracts` | Approve or reject proposed policy. |
302
361
  | `baseline` | Review or intentionally accept existing findings. |
303
362
 
363
+ Use `analyze --refresh` after a structural change. `doctor --fix-cache` removes only the disposable
364
+ discovery cache; it does not change rules, contracts, baseline, or source files.
365
+
366
+ Guard state is safe for concurrent CLI/MCP readers and writers: derived facts are published as an
367
+ atomic generation, policy updates use revisions, and short write contention is waited out. A
368
+ process that dies while holding the state lock is detected by its local PID and the lock is
369
+ recovered; use `--no-wait` when an integration needs immediate contention feedback.
370
+
304
371
  Run `pnpm exec codapult-guard <command> --help` for command-specific options.
305
372
 
373
+ Every `verify` run also writes a local manifest under
374
+ `.codapult/guard/history/runs/<run-id>.json`. It records stage durations, the outcome, and the
375
+ policy gate that decided the result. It contains no source code or external telemetry. These
376
+ manifests make a failed run explainable without turning Guard into a production tracing system.
377
+
306
378
  ## Security and data handling
307
379
 
308
380
  - Deterministic discovery and checks run locally.
@@ -5,17 +5,19 @@ export interface CommandResult {
5
5
  exitCode: number;
6
6
  stdout: string;
7
7
  stderr: string;
8
- timedOut?: boolean;
9
- truncated?: boolean;
8
+ timedOut?: boolean | undefined;
9
+ truncated?: boolean | undefined;
10
10
  }
11
- export declare function runProjectCommand(command: string, cwd: string, options?: {
12
- timeout?: number;
13
- env?: NodeJS.ProcessEnv;
14
- }): CommandResult;
15
- export declare function commandResponse(result: CommandResult): {
11
+ export interface RunProjectCommandOptions {
12
+ timeout?: number | undefined;
13
+ env?: NodeJS.ProcessEnv | undefined;
14
+ }
15
+ export interface CommandResponse {
16
16
  content: {
17
17
  type: 'text';
18
18
  text: string;
19
19
  }[];
20
20
  isError: boolean;
21
- };
21
+ }
22
+ export declare function runProjectCommand(command: string, cwd: string, options?: RunProjectCommandOptions): CommandResult;
23
+ export declare function commandResponse(result: CommandResult): CommandResponse;
@@ -1,4 +1,4 @@
1
- import { execSync } from 'node:child_process';
1
+ import { execFileSync } from 'node:child_process';
2
2
  const MAX_OUTPUT_CHARS = 20_000;
3
3
  const MAX_BUFFER_BYTES = 2_000_000;
4
4
  function redactOutput(value) {
@@ -13,9 +13,30 @@ function captureOutput(value) {
13
13
  ? { value: `${redacted.slice(0, MAX_OUTPUT_CHARS)}\n[output truncated]`, truncated: true }
14
14
  : { value: redacted, truncated: false };
15
15
  }
16
+ function parseCommand(command) {
17
+ const parts = command.trim().split(/\s+/).filter(Boolean);
18
+ if (parts.length === 0 || parts.some((part) => /[;&|<>`$()]/.test(part)))
19
+ return undefined;
20
+ const [executable, ...args] = parts;
21
+ const windowsExecutable = process.platform === 'win32' && /^(?:npm|npx|pnpm|yarn|bun)$/.test(executable)
22
+ ? `${executable}.cmd`
23
+ : executable;
24
+ return { executable: windowsExecutable, args };
25
+ }
16
26
  export function runProjectCommand(command, cwd, options = {}) {
27
+ const parsed = parseCommand(command);
28
+ if (!parsed) {
29
+ return {
30
+ command,
31
+ status: 'failed',
32
+ passed: false,
33
+ exitCode: 2,
34
+ stdout: '',
35
+ stderr: 'Unsafe or empty project command rejected.',
36
+ };
37
+ }
17
38
  try {
18
- const stdout = execSync(command, {
39
+ const stdout = execFileSync(parsed.executable, parsed.args, {
19
40
  cwd,
20
41
  env: { ...process.env, ...options.env },
21
42
  stdio: 'pipe',
@@ -1,20 +1,33 @@
1
1
  import { type CommandResult } from './command.js';
2
- export type ProjectCheck = 'lint' | 'typecheck' | 'test' | 'build';
3
- export type GuardAdapter = 'dependency-graph' | 'security' | 'dependency-hygiene';
4
- export type ExternalToolMode = 'auto' | 'on' | 'off';
2
+ import type { GuardAdapterName, GuardToolMode, ProjectCheck } from '../core/model/types.js';
3
+ export type { ProjectCheck } from '../core/model/types.js';
4
+ export type GuardAdapter = GuardAdapterName;
5
+ export type ExternalToolMode = GuardToolMode;
5
6
  export type ProjectCheckResults = Partial<Record<ProjectCheck, CommandResult>>;
7
+ export interface ProjectChecksOptions {
8
+ timeout?: number | undefined;
9
+ }
10
+ export interface WorkspaceProjectChecksOptions extends ProjectChecksOptions {
11
+ rootResults?: ProjectCheckResults | undefined;
12
+ }
13
+ export interface ProjectAdaptersOptions {
14
+ mode?: Exclude<ExternalToolMode, 'off'> | undefined;
15
+ timeout?: number | undefined;
16
+ tooling?: Partial<Record<GuardAdapter, {
17
+ script: string;
18
+ enabled: boolean;
19
+ }>> | undefined;
20
+ }
6
21
  export interface ProjectRuntimeDiagnostics {
7
22
  node: string;
8
23
  packageManager: string;
9
- declaredPackageManager?: string;
10
- declaredNode?: string;
24
+ declaredPackageManager?: string | undefined;
25
+ declaredNode?: string | undefined;
11
26
  compatible: boolean;
12
27
  issues: string[];
13
28
  }
14
29
  export declare function inspectProjectRuntime(root: string): ProjectRuntimeDiagnostics;
15
- export declare function runProjectChecks(root: string, checks: ProjectCheck[], options?: {
16
- timeout?: number;
17
- }): ProjectCheckResults;
30
+ export declare function runProjectChecks(root: string, checks: ProjectCheck[], options?: ProjectChecksOptions): ProjectCheckResults;
18
31
  /**
19
32
  * Runs checks for workspace packages only when the root package does not own
20
33
  * the corresponding check. Root orchestration remains the source of truth.
@@ -22,16 +35,6 @@ export declare function runProjectChecks(root: string, checks: ProjectCheck[], o
22
35
  export declare function runWorkspaceProjectChecks(root: string, packages: {
23
36
  path: string;
24
37
  scripts: Record<string, string>;
25
- }[], checks: ProjectCheck[], options?: {
26
- timeout?: number;
27
- rootResults?: ProjectCheckResults;
28
- }): Record<string, ProjectCheckResults>;
38
+ }[], checks: ProjectCheck[], options?: WorkspaceProjectChecksOptions): Record<string, ProjectCheckResults>;
29
39
  /** Runs only explicitly configured project scripts; Guard does not recreate these tools. */
30
- export declare function runProjectAdapters(root: string, options?: {
31
- mode?: Exclude<ExternalToolMode, 'off'>;
32
- timeout?: number;
33
- tooling?: Partial<Record<GuardAdapter, {
34
- script: string;
35
- enabled: boolean;
36
- }>>;
37
- }): Partial<Record<GuardAdapter, CommandResult>>;
40
+ export declare function runProjectAdapters(root: string, options?: ProjectAdaptersOptions): Partial<Record<GuardAdapter, CommandResult>>;
@@ -1,58 +1,57 @@
1
1
  import { type GuardToolMode } from '../../core/guard.js';
2
- export declare function guardInitCommand(options?: {
3
- force?: boolean;
4
- }): void;
5
- export declare function guardAnalyzeCommand(): void;
6
- export declare function guardProposeCommand(options?: {
7
- json?: boolean;
8
- }): void;
9
- export declare function guardInstallAgentCommand(target?: string, options?: {
10
- json?: boolean;
11
- }): void;
12
- export declare function guardDoctorCommand(options?: {
13
- json?: boolean;
2
+ interface GuardOutputOptions {
3
+ json?: boolean | undefined;
4
+ }
5
+ interface GuardInitOptions {
6
+ force?: boolean | undefined;
7
+ wait?: boolean | undefined;
8
+ }
9
+ interface GuardAnalyzeOptions {
10
+ refresh?: boolean | undefined;
11
+ wait?: boolean | undefined;
12
+ }
13
+ interface GuardVerifyOptions extends GuardOutputOptions {
14
+ checks?: string | undefined;
15
+ changed?: boolean | undefined;
16
+ requirement?: string | undefined;
17
+ tools?: GuardToolMode | undefined;
18
+ strict?: boolean | undefined;
19
+ projectChecks?: boolean | undefined;
20
+ }
21
+ interface GuardCheckOptions extends GuardOutputOptions {
22
+ changed?: boolean | undefined;
23
+ sarif?: boolean | undefined;
24
+ }
25
+ interface GuardBaselineOptions extends GuardOutputOptions {
26
+ all?: boolean | undefined;
27
+ reason?: string | undefined;
28
+ }
29
+ interface GuardApprovalOptions {
30
+ all?: boolean | undefined;
31
+ }
32
+ interface GuardReviewOptions {
33
+ maxDiffChars?: string | undefined;
34
+ requirement?: string | undefined;
35
+ base?: string | undefined;
36
+ }
37
+ export declare function guardInitCommand(options?: GuardInitOptions): void;
38
+ export declare function guardAnalyzeCommand(options?: GuardAnalyzeOptions): void;
39
+ export declare function guardProposeCommand(options?: GuardOutputOptions): void;
40
+ export declare function guardInstallAgentCommand(target?: string, options?: GuardOutputOptions): void;
41
+ export declare function guardDoctorCommand(options?: GuardOutputOptions & {
42
+ fixCache?: boolean | undefined;
14
43
  }): void;
15
44
  export declare function guardHistoryCommand(): void;
16
- export declare function guardImpactCommand(files: string[], options?: {
17
- json?: boolean;
18
- }): void;
19
- export declare function guardHistoryDiffCommand(from: string, to: string, options?: {
20
- json?: boolean;
21
- }): void;
22
- export declare function guardVerifyCommand(options?: {
23
- checks?: string;
24
- changed?: boolean;
25
- json?: boolean;
26
- requirement?: string;
27
- tools?: GuardToolMode;
28
- strict?: boolean;
29
- projectChecks?: boolean;
30
- }): void;
31
- export declare function guardCheckCommand(options?: {
32
- changed?: boolean;
33
- json?: boolean;
34
- sarif?: boolean;
35
- }): void;
36
- export declare function guardAuditCommand(options?: {
37
- json?: boolean;
38
- }): void;
39
- export declare function guardBaselineCommand(action: 'list' | 'accept' | 'remove', ids?: string, options?: {
40
- all?: boolean;
41
- reason?: string;
42
- json?: boolean;
43
- }): void;
44
- export declare function guardRulesApproveCommand(ids?: string, options?: {
45
- all?: boolean;
46
- }): void;
47
- export declare function guardContractsApproveCommand(ids?: string, options?: {
48
- all?: boolean;
49
- }): void;
50
- export declare function guardContractsRejectCommand(ids?: string, options?: {
51
- all?: boolean;
52
- }): void;
53
- export declare function guardReviewCommand(options?: {
54
- maxDiffChars?: string;
55
- requirement?: string;
56
- base?: string;
57
- }): void;
45
+ export declare function guardImpactCommand(files: string[], options?: GuardOutputOptions): void;
46
+ export declare function guardHistoryDiffCommand(from: string, to: string, options?: GuardOutputOptions): void;
47
+ export declare function guardVerifyCommand(options?: GuardVerifyOptions): void;
48
+ export declare function guardCheckCommand(options?: GuardCheckOptions): void;
49
+ export declare function guardAuditCommand(options?: GuardOutputOptions): void;
50
+ export declare function guardBaselineCommand(action: 'list' | 'accept' | 'remove', ids?: string, options?: GuardBaselineOptions): void;
51
+ export declare function guardRulesApproveCommand(ids?: string, options?: GuardApprovalOptions): void;
52
+ export declare function guardPolicyExplainCommand(id: string, options?: GuardOutputOptions): void;
53
+ export declare function guardContractsApproveCommand(ids?: string, options?: GuardApprovalOptions): void;
54
+ export declare function guardContractsRejectCommand(ids?: string, options?: GuardApprovalOptions): void;
55
+ export declare function guardReviewCommand(options?: GuardReviewOptions): void;
58
56
  export declare function guardIsInitialized(root: string): boolean;
57
+ export {};