@codapult/guard 0.10.0 → 0.11.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.
Files changed (40) hide show
  1. package/README.md +144 -4
  2. package/dist/adapters/agents/agent-integration.js +26 -3
  3. package/dist/cli/commands/guard.d.ts +2 -0
  4. package/dist/cli/commands/guard.js +77 -11
  5. package/dist/cli/index.js +2 -0
  6. package/dist/core/guard.d.ts +28 -1
  7. package/dist/core/guard.js +37 -0
  8. package/dist/core/history/runs.d.ts +64 -1
  9. package/dist/core/history/runs.js +109 -0
  10. package/dist/core/output/attestation.d.ts +17 -0
  11. package/dist/core/output/attestation.js +22 -0
  12. package/dist/core/policy/schemas.d.ts +47 -0
  13. package/dist/core/policy/schemas.js +67 -0
  14. package/dist/core/verification/github-actions.d.ts +25 -0
  15. package/dist/core/verification/github-actions.js +397 -0
  16. package/dist/core/verification/github-context.d.ts +8 -0
  17. package/dist/core/verification/github-context.js +41 -0
  18. package/dist/core/verification/verify.d.ts +23 -4
  19. package/dist/core/verification/verify.js +601 -47
  20. package/dist/index.d.ts +1 -0
  21. package/dist/index.js +1 -0
  22. package/dist/mcp/prompts.js +7 -4
  23. package/dist/mcp/tools/guard.js +9 -3
  24. package/docs/extensions.md +79 -0
  25. package/docs/guard-agent-integration.md +51 -0
  26. package/docs/guard-ci.yml +202 -0
  27. package/docs/guard-fixtures.md +127 -0
  28. package/docs/guard-trusted-pr-attestation.yml +213 -0
  29. package/docs/guard.md +1125 -0
  30. package/docs/integrations/README.md +38 -0
  31. package/docs/integrations/claude-code.md +92 -0
  32. package/docs/integrations/codex.md +58 -0
  33. package/docs/integrations/cursor.md +116 -0
  34. package/docs/integrations/gemini-cli.md +47 -0
  35. package/docs/integrations/generic.md +41 -0
  36. package/docs/integrations/github-copilot.md +72 -0
  37. package/docs/policy-mutations.md +75 -0
  38. package/docs/releasing.md +58 -0
  39. package/docs/verify-guard-attestation.mjs +166 -0
  40. package/package.json +4 -2
package/README.md CHANGED
@@ -6,6 +6,7 @@
6
6
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
7
  [![Node.js >=20.19](https://img.shields.io/badge/node-%3E%3D20.19-339933.svg?logo=node.js&logoColor=white)](package.json)
8
8
  [![TypeScript](https://img.shields.io/badge/TypeScript-first-3178C6.svg?logo=typescript&logoColor=white)](https://www.typescriptlang.org/)
9
+ [![MCP Registry](https://img.shields.io/badge/MCP_Registry-Listed-4B6A88?style=flat-square)](https://registry.modelcontextprotocol.io/?q=io.github.codapult%2Fguard)
9
10
 
10
11
  ## Architecture guardrails for AI coding agents
11
12
 
@@ -388,6 +389,131 @@ review control when policy changes require a separate approval. For example, a r
388
389
  `.codapult/guard/* @architecture-owners` to `CODEOWNERS`. Guard reports the policy change; the
389
390
  hosting platform enforces reviewer authorization.
390
391
 
392
+ Guard also reports changes to verification-sensitive files using built-in paths for CI workflows,
393
+ package scripts and lockfiles, and common formatter, linter, typecheck, and test configurations.
394
+ Projects can extend this list in the base revision's `.codapult/guard/rules.json`:
395
+
396
+ ```json
397
+ {
398
+ "version": 1,
399
+ "rules": [],
400
+ "protectedPaths": [".husky/**", "config/security-checks/**"]
401
+ }
402
+ ```
403
+
404
+ For classified entries and warning-only paths, use the explicit `policySurface.paths` form in that
405
+ same base policy:
406
+
407
+ ```json
408
+ {
409
+ "version": 1,
410
+ "rules": [],
411
+ "policySurface": {
412
+ "paths": [
413
+ { "pattern": "config/security-checks/**", "kind": "custom", "severity": "error" },
414
+ { "pattern": "**/.custom-ignore", "kind": "ignore-config", "severity": "warning" }
415
+ ]
416
+ }
417
+ }
418
+ ```
419
+
420
+ Supported kinds are `ci-workflow`, `guard-policy`, `ownership`, `dependency-input`, `tool-config`, `ignore-config`,
421
+ `suppression-source`, and `custom`. `error` entries fail when `--fail-on-protected-changes` is
422
+ enabled; `warning` entries are reported without blocking. Built-in verification-sensitive paths
423
+ remain blocking. Both forms are read from the base policy during PR verification, so the PR cannot
424
+ remove its own path coverage.
425
+
426
+ In pull request CI, add `--fail-on-protected-changes` alongside `--policy-base`. This fails the
427
+ Guard invocation when a listed path changes without a matching CI integrity contract, or when a
428
+ contracted workflow fails its checks. It emits all matched paths in JSON output. Protect
429
+ the Guard workflow itself and require its status check in repository settings: a workflow edited
430
+ or removed by a PR cannot independently enforce its own integrity. Guard reports workflow edits,
431
+ but GitHub branch protection is what makes the check mandatory. The sample workflow rejects PR
432
+ changes to package manifests, common npm/pnpm/Yarn/Bun lockfiles, package-manager config, and
433
+ workspace install inputs before
434
+ selecting pnpm or installing dependencies. Review those changes in a separate PR. This preflight is
435
+ defense in depth: repository rules must still protect the workflow and require the Guard check.
436
+
437
+ Guard reports added `eslint-disable`, `@ts-ignore`, `@ts-nocheck`, `@ts-expect-error`, and
438
+ `biome-ignore`, and `prettier-ignore` markers in the PR diff. It reports the file and marker
439
+ without interpreting the tool-specific reason or whether the suppression is justified. Other
440
+ tools can be covered by protecting their configuration/source paths and running them in a trusted
441
+ CI workflow. The matched paths, markers, and scan completeness are recorded in the verification
442
+ result and receipt.
443
+
444
+ ### Required verification stages
445
+
446
+ `agent.json` declares which stages must complete for verification to pass:
447
+
448
+ ```json
449
+ {
450
+ "version": 1,
451
+ "tools": "auto",
452
+ "tooling": {},
453
+ "completionGate": {
454
+ "enabled": true,
455
+ "maxIterations": 3,
456
+ "projectChecks": true,
457
+ "checks": ["lint", "typecheck", "test", "build"],
458
+ "requiredStages": ["architecture-policy", "project-checks", "runtime"]
459
+ }
460
+ }
461
+ ```
462
+
463
+ Supported stages are `runtime`, `project-checks`, `adapters`, `architecture-policy`,
464
+ `policy-surface`, `ci-integrity`, and `verification-scope`. The compatibility default requires `architecture-policy`;
465
+ projects opt into additional requirements. If a required stage is skipped or cannot be confirmed,
466
+ the result is `incomplete`, the CLI exits nonzero, and the receipt names the incomplete stages.
467
+ When base policy is used, `policy-surface` is required automatically. A configured CI integrity
468
+ contract makes `ci-integrity` required automatically. `verification-scope` is always required:
469
+ Guard hashes its discovered project inputs before checks and confirms that the scope stayed stable
470
+ through the run.
471
+
472
+ ### GitHub Actions integrity contract
473
+
474
+ Declare the exact workflow and required job in the base revision's `rules.json`:
475
+
476
+ ```json
477
+ {
478
+ "version": 1,
479
+ "rules": [],
480
+ "githubActions": {
481
+ "workflows": [
482
+ {
483
+ "path": ".github/workflows/guard.yml",
484
+ "job": "guard",
485
+ "requiredRunIncludes": [
486
+ "pnpm exec codapult-guard verify",
487
+ "--policy-base",
488
+ "--fail-on-policy-change",
489
+ "--fail-on-protected-changes"
490
+ ],
491
+ "maxPermissions": { "contents": "read", "security-events": "write" }
492
+ }
493
+ ]
494
+ }
495
+ }
496
+ ```
497
+
498
+ Guard parses the declared workflow and checks its pull request trigger, configured filters, required
499
+ job, required command fragments in the Guard invocation, conditional execution, `continue-on-error`,
500
+ declared job dependencies, explicit permission ceilings, and full commit SHA pins for external
501
+ actions. Local actions cannot meet immutable SHA pinning; if a workflow uses one, set
502
+ `requireFullShaActions` to `false` and list its source under `protectedPaths` in the base policy.
503
+ Guard compares each action ref with the contracted workflow at the base revision; additions,
504
+ removals, and SHA changes are reported as `policy-weakened` for separate review even when the new
505
+ ref is itself SHA-pinned. Missing protections are reported as `policy-weakened`; malformed or
506
+ unreadable workflow data is `incomplete`. The result and receipt include base/current workflow
507
+ digests, action refs and their changes, and issues. Guard does not
508
+ evaluate arbitrary GitHub expressions or prove that repository settings require the check; branch
509
+ protection or rulesets must still require the expected check source.
510
+ The audit rejects `pull_request_target` on a contracted verification workflow because Guard cannot
511
+ prove that it never executes untrusted PR content with that event's elevated context.
512
+
513
+ The receipt records the effective Guard invocation and each project check's command and arguments.
514
+ Credential-like values are redacted from command evidence; treat receipts as review artifacts, not
515
+ as a substitute for trusted CI logs.
516
+
391
517
  To make policy changes fail this invocation explicitly, add:
392
518
 
393
519
  ```bash
@@ -418,6 +544,13 @@ The package repository separately runs its own unit, typecheck, build, and packa
418
544
  [`.github/workflows/ci.yml`](.github/workflows/ci.yml), while the fixture workflow tests Guard on
419
545
  real project shapes.
420
546
 
547
+ To add a separately signed Guard receipt, copy
548
+ [`docs/guard-trusted-pr-attestation.yml`](docs/guard-trusted-pr-attestation.yml) to
549
+ `.github/workflows/guard-trusted-pr-attestation.yml` and follow the
550
+ [trusted-attestation setup guide](docs/guard.md#install-the-trusted-github-actions-workflow).
551
+ This optional workflow re-runs Guard; it does not replace the required `Guard` check or attest
552
+ project test/build results.
553
+
421
554
  ## CLI surface
422
555
 
423
556
  | Command | Purpose |
@@ -460,10 +593,17 @@ production tracing system or an identity/signature ledger.
460
593
 
461
594
  The manifest also contains a `verification-receipt` object. It is a compact, machine-readable
462
595
  record of the commit, policy base and fingerprint, decision IDs, check statuses, finding counts,
463
- contract issues, waiver state, and Guard version. It intentionally excludes command output and
464
- source code. The receipt describes what Guard decided; it is not a cryptographic signature or proof
465
- of CI identity. When a policy base is supplied, it separates the base diff from dirty working-tree
466
- files so local verification cannot silently present an incomplete change set.
596
+ contract issues, waiver state, and Guard version. Its `verificationScope` contains SHA-256 hashes
597
+ and paths for files discovered by Guard plus changed and deleted paths; the fingerprint binds that
598
+ inventory to the effective policy and Guard version. The `verification-scope` stage is incomplete
599
+ if Guard cannot hash the full scope or if the inputs changed during the run. The receipt excludes
600
+ source contents and command output. It describes what Guard decided; it is not a cryptographic
601
+ signature or proof of CI identity. When a policy base is supplied, it separates the base diff from
602
+ dirty working-tree files so local verification cannot silently present an incomplete change set.
603
+ For portable exchange, `verify --attestation-out <file>` exports a passing receipt as an unsigned
604
+ in-toto Statement. The format is independent of the CI platform and signing mechanism; a trusted CI
605
+ signer can sign it using the mechanism supported by that platform. The attestation guide includes a
606
+ GitHub Actions example using Sigstore. See the [attestation guide](docs/guard.md#exporting-and-signing-a-verification-attestation).
467
607
 
468
608
  ## Security and data handling
469
609
 
@@ -1,4 +1,5 @@
1
- import { existsSync, lstatSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
1
+ import { lstatSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
2
+ import { randomUUID } from 'node:crypto';
2
3
  import { dirname, resolve } from 'node:path';
3
4
  export class GuardAgentInstallError extends Error {
4
5
  constructor(message) {
@@ -8,6 +9,21 @@ export class GuardAgentInstallError extends Error {
8
9
  }
9
10
  const START_MARKER = '<!-- codapult-guard:start -->';
10
11
  const END_MARKER = '<!-- codapult-guard:end -->';
12
+ function writeInstructionFile(path, content) {
13
+ const temporaryPath = `${path}.tmp-${process.pid}-${randomUUID()}`;
14
+ try {
15
+ writeFileSync(temporaryPath, content, { encoding: 'utf8', flag: 'wx' });
16
+ renameSync(temporaryPath, path);
17
+ }
18
+ finally {
19
+ try {
20
+ unlinkSync(temporaryPath);
21
+ }
22
+ catch {
23
+ // The temporary file was renamed or could not be created.
24
+ }
25
+ }
26
+ }
11
27
  const instruction = `${START_MARKER}
12
28
  ## Codapult Guard completion gate
13
29
 
@@ -56,7 +72,14 @@ export function installGuardAgentInstructions(root, target) {
56
72
  assertNoSymlinkPath(root, relativePath);
57
73
  const path = resolve(root, relativePath);
58
74
  const nextBlock = instruction;
59
- const existing = existsSync(path) ? readFileSync(path, 'utf8') : '';
75
+ let existing = '';
76
+ try {
77
+ existing = readFileSync(path, 'utf8');
78
+ }
79
+ catch (error) {
80
+ if (error.code !== 'ENOENT')
81
+ throw error;
82
+ }
60
83
  const start = existing.indexOf(START_MARKER);
61
84
  const end = existing.indexOf(END_MARKER);
62
85
  const content = start >= 0 && end >= start
@@ -65,7 +88,7 @@ export function installGuardAgentInstructions(root, target) {
65
88
  ? `${existing.trimEnd()}\n\n${renderContent(target)}`
66
89
  : renderContent(target);
67
90
  mkdirSync(dirname(path), { recursive: true });
68
- writeFileSync(path, content, 'utf8');
91
+ writeInstructionFile(path, content);
69
92
  return { target, path: relativePath, action: existing ? 'updated' : 'created' };
70
93
  }
71
94
  export function installGuardAgentTargets(root, targetsToInstall) {
@@ -19,6 +19,8 @@ interface GuardVerifyOptions extends GuardOutputOptions {
19
19
  projectChecks?: boolean | undefined;
20
20
  policyBase?: string | undefined;
21
21
  failOnPolicyChange?: boolean | undefined;
22
+ failOnProtectedChanges?: boolean | undefined;
23
+ attestationOut?: string | undefined;
22
24
  }
23
25
  interface GuardGovernanceOptions extends GuardOutputOptions {
24
26
  strict?: boolean | undefined;
@@ -1,5 +1,5 @@
1
1
  import { randomUUID } from 'node:crypto';
2
- import { existsSync, readFileSync } from 'node:fs';
2
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
3
3
  import { relative, resolve } from 'node:path';
4
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';
@@ -12,6 +12,15 @@ import { guardFindingsToSarif } from '../../core/output/sarif.js';
12
12
  import { analyzeProjectImpact } from '../../core/analysis/impact.js';
13
13
  import { listGuardRuns, summarizeGuardRuns } from '../../core/history/runs.js';
14
14
  import { auditGuardGovernance } from '../../core/audit/governance.js';
15
+ import { guardReceiptToInToto } from '../../core/output/attestation.js';
16
+ function terminalSafe(value) {
17
+ let output = '';
18
+ for (const character of value) {
19
+ const code = character.charCodeAt(0);
20
+ output += code <= 0x1f || (code >= 0x7f && code <= 0x9f) ? ' ' : character;
21
+ }
22
+ return output;
23
+ }
15
24
  function renderFindings(findings) {
16
25
  for (const finding of findings) {
17
26
  const printer = finding.severity === 'error' ? fail : finding.severity === 'warning' ? warn : info;
@@ -49,7 +58,7 @@ function loadConfigSafely(root, options = {}) {
49
58
  if (options.json || options.machine)
50
59
  console.log(JSON.stringify(payload, null, 2));
51
60
  else
52
- fail(`${payload.message} Run \`codapult-guard doctor\` for details.`);
61
+ fail(`${terminalSafe(payload.message)} Run \`codapult-guard doctor\` for details.`);
53
62
  process.exitCode = 1;
54
63
  return { invalid: true };
55
64
  }
@@ -217,7 +226,7 @@ export function guardRunsCommand(options = {}) {
217
226
  return;
218
227
  }
219
228
  heading('Codapult Guard Runs');
220
- info(`${result.summary.total} run(s); ${result.summary.failed} failed, ${result.summary.warnings} warning, average ${result.summary.averageDurationMs} ms`);
229
+ info(`${result.summary.total} run(s); ${result.summary.failed} failed, ${result.summary.incomplete} incomplete, ${result.summary.policyWeakened} policy-weakened, ${result.summary.warnings} warning, average ${result.summary.averageDurationMs} ms`);
221
230
  for (const run of result.runs) {
222
231
  info(`${run.runId}: ${run.outcome}, gate=${run.gate}, ${run.durationMs} ms`);
223
232
  }
@@ -312,10 +321,26 @@ export function guardVerifyCommand(options = {}) {
312
321
  projectChecks: options.projectChecks,
313
322
  policyBase: options.policyBase,
314
323
  failOnPolicyChange: options.failOnPolicyChange,
324
+ failOnProtectedChanges: options.failOnProtectedChanges,
315
325
  });
326
+ let attestationFailed = false;
327
+ if (options.attestationOut) {
328
+ try {
329
+ if (!result.run.receipt)
330
+ throw new Error('Guard did not produce a verification receipt.');
331
+ const statement = guardReceiptToInToto(result.run.receipt);
332
+ writeFileSync(resolve(options.attestationOut), `${JSON.stringify(statement, null, 2)}\n`);
333
+ if (!options.json)
334
+ info(`in-toto verification statement written to ${options.attestationOut}`);
335
+ }
336
+ catch (error) {
337
+ attestationFailed = true;
338
+ fail(error instanceof Error ? error.message : 'Could not write the in-toto statement.');
339
+ }
340
+ }
316
341
  if (options.json) {
317
342
  console.log(JSON.stringify(result, null, 2));
318
- process.exitCode = result.status === 'ok' ? 0 : 1;
343
+ process.exitCode = result.status === 'ok' && !attestationFailed ? 0 : 1;
319
344
  return;
320
345
  }
321
346
  heading('Codapult Guard Verify');
@@ -330,21 +355,62 @@ export function guardVerifyCommand(options = {}) {
330
355
  warn('The working-tree policy differs from the base policy; the base policy was used.');
331
356
  }
332
357
  }
358
+ if (result.policySurface) {
359
+ dim(`protected paths changed: ${result.policySurface.changedPaths.length}`);
360
+ for (const path of result.policySurface.changedPaths)
361
+ warn(`verification-sensitive: ${terminalSafe(path)}`);
362
+ for (const change of result.policySurface.changes) {
363
+ dim(`policy surface: ${change.kind}/${change.severity} ${terminalSafe(change.path)} (${terminalSafe(change.pattern)})`);
364
+ }
365
+ for (const marker of result.policySurface.suppressionMarkers)
366
+ warn(`inline suppression added: ${terminalSafe(marker)}`);
367
+ if (result.policySurface.workflowChanged)
368
+ warn('Workflow changes require host-enforced required checks and protected review.');
369
+ }
370
+ if (result.ciIntegrity) {
371
+ if (result.ciIntegrity.status === 'ok')
372
+ success('CI integrity: passed');
373
+ else if (result.ciIntegrity.status === 'policy-weakened') {
374
+ fail('CI integrity: policy weakened');
375
+ }
376
+ else {
377
+ warn('CI integrity: incomplete');
378
+ }
379
+ for (const workflow of result.ciIntegrity.workflows) {
380
+ for (const change of workflow.actionChanges ?? []) {
381
+ warn(`[${terminalSafe(workflow.path)}] action refs changed for ${terminalSafe(change.name)}: ${terminalSafe(change.base.join(', ') || '(none)')} → ${terminalSafe(change.current.join(', ') || '(none)')}`);
382
+ }
383
+ for (const issue of workflow.issues)
384
+ fail(`[${terminalSafe(workflow.path)}] ${terminalSafe(issue.message)}`);
385
+ }
386
+ }
387
+ const receipt = result.run.receipt;
388
+ if (receipt?.effectiveInvocation) {
389
+ dim(`effective invocation: ${terminalSafe(receipt.effectiveInvocation.command)} ${terminalSafe(receipt.effectiveInvocation.args.join(' '))}`);
390
+ }
391
+ if (result.incompleteStages && result.incompleteStages.length > 0) {
392
+ warn(`Required stages incomplete: ${result.incompleteStages.join(', ')}`);
393
+ }
333
394
  for (const [name, check] of Object.entries(result.checks)) {
334
395
  if (check.status === 'passed')
335
- success(`${name}: passed`);
396
+ success(`${terminalSafe(name)}: passed`);
336
397
  else if (check.status === 'not-configured')
337
- dim(`${name}: not configured`);
398
+ dim(`${terminalSafe(name)}: not configured`);
338
399
  else
339
- fail(`${name}: failed (exit ${check.exitCode})`);
400
+ fail(`${terminalSafe(name)}: failed (exit ${check.exitCode})`);
340
401
  }
341
402
  for (const [name, check] of Object.entries(result.adapters ?? {})) {
342
403
  if (check.status === 'passed')
343
- success(`${name}: passed`);
404
+ success(`${terminalSafe(name)}: passed`);
344
405
  else if (check.status === 'not-configured')
345
- dim(`${name}: not configured`);
406
+ dim(`${terminalSafe(name)}: not configured`);
346
407
  else
347
- fail(`${name}: failed (exit ${check.exitCode})`);
408
+ fail(`${terminalSafe(name)}: failed (exit ${check.exitCode})`);
409
+ }
410
+ for (const [name, evidence] of Object.entries(receipt?.checks ?? {})) {
411
+ if (!evidence.command)
412
+ continue;
413
+ dim(`${terminalSafe(name)} command (${evidence.execution ?? 'unknown'}): ${terminalSafe(evidence.command)}${(evidence.args?.length ?? 0) > 0 ? ` ${terminalSafe(evidence.args?.join(' ') ?? '')}` : ''}`);
348
414
  }
349
415
  const findings = result.architecture?.findings ?? [];
350
416
  if (findings.length === 0)
@@ -358,7 +424,7 @@ export function guardVerifyCommand(options = {}) {
358
424
  if (result.contractIssues.length > 0)
359
425
  fail(`contracts: ${result.contractIssues.length} invalid`);
360
426
  dim(`requirements: ${result.requirement.message}`);
361
- process.exitCode = result.status === 'ok' ? 0 : 1;
427
+ process.exitCode = result.status === 'ok' && !attestationFailed ? 0 : 1;
362
428
  }
363
429
  export function guardCheckCommand(options = {}) {
364
430
  const root = getRoot();
package/dist/cli/index.js CHANGED
@@ -44,6 +44,8 @@ guard
44
44
  .option('--changed')
45
45
  .option('--policy-base <ref>', 'read policy artifacts from this Git ref')
46
46
  .option('--fail-on-policy-change', 'fail when the working-tree policy differs from the base ref')
47
+ .option('--fail-on-protected-changes', 'fail when protected verification paths change relative to base')
48
+ .option('--attestation-out <file>', 'write a passing verification as an in-toto statement')
47
49
  .option('--json')
48
50
  .action(guardVerifyCommand);
49
51
  guard
@@ -3,12 +3,13 @@ import { clearDiscoveryCache, discoverProject, discoverProjectWithMetrics, findG
3
3
  import { type GuardImpactAnalysis } from './analysis/impact.js';
4
4
  import type { GuardAdapterName, GuardApprovalMode, GuardContractKind, GuardBudgetMetric, GuardRuleKind, GuardRuleStatus, GuardSeverity, GuardToolMode, GuardWaiver, GuardWaiverPolicy } from './model/types.js';
5
5
  export type { GuardAdapterName, GuardApprovalMode, GuardContractKind, GuardBudgetMetric, GuardRuleKind, GuardRuleStatus, GuardSeverity, GuardToolMode, GuardWaiver, GuardWaiverPolicy, GuardWaiverDecision, } from './model/types.js';
6
- export type GuardOutcomeStatus = 'pass' | 'fail' | 'warning' | 'needs-review' | 'not-configured';
6
+ export type GuardOutcomeStatus = 'pass' | 'fail' | 'incomplete' | 'policy-weakened' | 'warning' | 'needs-review' | 'not-configured';
7
7
  export interface GuardOutcomeInput {
8
8
  configured?: boolean | undefined;
9
9
  errors?: number | undefined;
10
10
  warnings?: number | undefined;
11
11
  needsReview?: boolean | undefined;
12
+ incomplete?: boolean | undefined;
12
13
  }
13
14
  interface GuardStateLockOptions {
14
15
  waitMs?: number | undefined;
@@ -86,6 +87,30 @@ export interface GuardConfig {
86
87
  budgets?: GuardBudget[] | undefined;
87
88
  approval?: GuardApprovalPolicy | undefined;
88
89
  waiverPolicy?: GuardWaiverPolicy | undefined;
90
+ protectedPaths?: string[] | undefined;
91
+ policySurface?: GuardPolicySurfacePolicy | undefined;
92
+ githubActions?: GuardGitHubActionsPolicy | undefined;
93
+ }
94
+ export interface GuardPolicySurfacePolicy {
95
+ paths: {
96
+ pattern: string;
97
+ kind: 'ci-workflow' | 'guard-policy' | 'ownership' | 'dependency-input' | 'tool-config' | 'ignore-config' | 'suppression-source' | 'custom';
98
+ severity: 'warning' | 'error';
99
+ }[];
100
+ }
101
+ export interface GuardGitHubActionsPolicy {
102
+ workflows: {
103
+ path: string;
104
+ job: string;
105
+ requiredRunIncludes: string[];
106
+ requiredNeeds?: string[] | undefined;
107
+ requirePullRequest?: boolean | undefined;
108
+ requireUnfilteredPullRequest?: boolean | undefined;
109
+ requireUnconditionalJob?: boolean | undefined;
110
+ requireFullShaActions?: boolean | undefined;
111
+ disallowContinueOnError?: boolean | undefined;
112
+ maxPermissions?: Record<string, 'none' | 'read' | 'write'> | undefined;
113
+ }[];
89
114
  }
90
115
  export declare function getGuardPolicyMutationPolicy(guardConfig: GuardConfig | undefined): GuardPolicyMutationPolicy;
91
116
  export interface GuardApprovalPolicy {
@@ -155,8 +180,10 @@ export interface GuardAgentConfig {
155
180
  maxIterations: number;
156
181
  projectChecks: boolean;
157
182
  checks: ('lint' | 'typecheck' | 'test' | 'build')[];
183
+ requiredStages?: GuardRequiredStage[] | undefined;
158
184
  };
159
185
  }
186
+ export type GuardRequiredStage = 'runtime' | 'project-checks' | 'adapters' | 'architecture-policy' | 'policy-surface' | 'ci-integrity' | 'verification-scope';
160
187
  export interface GuardFinding {
161
188
  ruleId: string;
162
189
  severity: GuardSeverity;
@@ -17,6 +17,8 @@ export function classifyGuardOutcome(input) {
17
17
  return 'fail';
18
18
  if (input.needsReview)
19
19
  return 'needs-review';
20
+ if (input.incomplete)
21
+ return 'incomplete';
20
22
  return (input.warnings ?? 0) > 0 ? 'warning' : 'pass';
21
23
  }
22
24
  export function getGuardPolicyMutationPolicy(guardConfig) {
@@ -129,6 +131,7 @@ export const defaultGuardAgentConfig = {
129
131
  maxIterations: 3,
130
132
  projectChecks: true,
131
133
  checks: ['lint', 'typecheck', 'test', 'build'],
134
+ requiredStages: ['architecture-policy'],
132
135
  },
133
136
  };
134
137
  export class GuardAlreadyInitializedError extends Error {
@@ -428,6 +431,40 @@ export function fingerprintGuardConfig(guardConfig) {
428
431
  contracts: [...(guardConfig.contracts ?? [])].sort((left, right) => left.id.localeCompare(right.id)),
429
432
  budgets: [...(guardConfig.budgets ?? [])].sort((left, right) => left.id.localeCompare(right.id)),
430
433
  approval: guardConfig.approval ?? defaultGuardConfig.approval,
434
+ ...(guardConfig.protectedPaths !== undefined && guardConfig.protectedPaths.length > 0
435
+ ? { protectedPaths: [...guardConfig.protectedPaths].sort() }
436
+ : {}),
437
+ ...(guardConfig.policySurface
438
+ ? {
439
+ policySurface: {
440
+ paths: [...guardConfig.policySurface.paths]
441
+ .map((entry) => ({ ...entry, severity: entry.severity ?? 'error' }))
442
+ .sort((left, right) => `${left.pattern}:${left.kind}:${left.severity}`.localeCompare(`${right.pattern}:${right.kind}:${right.severity}`)),
443
+ },
444
+ }
445
+ : {}),
446
+ ...(guardConfig.githubActions
447
+ ? {
448
+ githubActions: {
449
+ workflows: guardConfig.githubActions.workflows
450
+ .map((workflow) => ({
451
+ ...workflow,
452
+ requiredNeeds: workflow.requiredNeeds
453
+ ? [...workflow.requiredNeeds].sort()
454
+ : undefined,
455
+ requirePullRequest: workflow.requirePullRequest ?? true,
456
+ requireUnfilteredPullRequest: workflow.requireUnfilteredPullRequest ?? true,
457
+ requireUnconditionalJob: workflow.requireUnconditionalJob ?? true,
458
+ requireFullShaActions: workflow.requireFullShaActions ?? true,
459
+ disallowContinueOnError: workflow.disallowContinueOnError ?? true,
460
+ maxPermissions: workflow.maxPermissions
461
+ ? Object.fromEntries(Object.entries(workflow.maxPermissions).sort(([left], [right]) => left.localeCompare(right)))
462
+ : undefined,
463
+ }))
464
+ .sort((left, right) => `${left.path}:${left.job}`.localeCompare(`${right.path}:${right.job}`)),
465
+ },
466
+ }
467
+ : {}),
431
468
  }))
432
469
  .digest('hex');
433
470
  }
@@ -17,6 +17,54 @@ export interface GuardVerificationReceiptCheck {
17
17
  status: 'passed' | 'failed' | 'not-configured';
18
18
  exitCode: number;
19
19
  durationMs: number;
20
+ command?: string | undefined;
21
+ args?: string[] | undefined;
22
+ execution?: 'attempted' | 'rejected' | 'not-configured' | undefined;
23
+ }
24
+ export interface GuardVerificationReceiptPolicySurface {
25
+ protectedPaths: string[];
26
+ changedPaths: string[];
27
+ changes?: {
28
+ path: string;
29
+ pattern: string;
30
+ kind: string;
31
+ severity: 'warning' | 'error';
32
+ }[];
33
+ suppressionMarkers: string[];
34
+ workflowChanged: boolean;
35
+ complete: boolean;
36
+ }
37
+ export interface GuardVerificationScope {
38
+ algorithm: 'sha256';
39
+ fingerprint: string;
40
+ complete: boolean;
41
+ stable: boolean;
42
+ files: {
43
+ path: string;
44
+ status: 'present' | 'deleted' | 'unavailable';
45
+ sha256?: string | undefined;
46
+ }[];
47
+ }
48
+ export interface GuardVerificationCiIntegrity {
49
+ status: 'ok' | 'policy-weakened' | 'incomplete';
50
+ workflows: {
51
+ path: string;
52
+ job: string;
53
+ digest?: string | undefined;
54
+ baseDigest?: string | undefined;
55
+ actions: string[];
56
+ baseActions?: string[] | undefined;
57
+ actionChanges?: {
58
+ name: string;
59
+ base: string[];
60
+ current: string[];
61
+ }[] | undefined;
62
+ status: 'ok' | 'policy-weakened' | 'incomplete';
63
+ issues: {
64
+ code: string;
65
+ message: string;
66
+ }[];
67
+ }[];
20
68
  }
21
69
  export interface GuardVerificationReceipt {
22
70
  version: 1;
@@ -29,12 +77,25 @@ export interface GuardVerificationReceipt {
29
77
  commit?: string | undefined;
30
78
  baseRef?: string | undefined;
31
79
  baseCommit?: string | undefined;
80
+ headSha?: string | undefined;
81
+ baseSha?: string | undefined;
82
+ mergeSha?: string | undefined;
83
+ githubEvent?: string | undefined;
32
84
  changedFiles: string[];
33
85
  baseChangedFiles?: string[] | undefined;
34
86
  workingTreeFiles?: string[] | undefined;
35
87
  workingTreeDirty: boolean;
36
88
  };
37
89
  policy?: GuardRunPolicy | undefined;
90
+ verificationScope?: GuardVerificationScope | undefined;
91
+ policySurface?: GuardVerificationReceiptPolicySurface | undefined;
92
+ ciIntegrity?: GuardVerificationCiIntegrity | undefined;
93
+ requiredStages?: string[] | undefined;
94
+ incompleteStages?: string[] | undefined;
95
+ effectiveInvocation?: {
96
+ command: string;
97
+ args: string[];
98
+ } | undefined;
38
99
  stages: Record<string, 'ok' | 'fail' | 'skipped'>;
39
100
  checks: Record<string, GuardVerificationReceiptCheck>;
40
101
  findings: {
@@ -61,7 +122,7 @@ export interface GuardRunManifest {
61
122
  startedAt: string;
62
123
  completedAt: string;
63
124
  durationMs: number;
64
- outcome: 'pass' | 'fail' | 'warning' | 'needs-review' | 'not-configured';
125
+ outcome: 'pass' | 'fail' | 'incomplete' | 'policy-weakened' | 'warning' | 'needs-review' | 'not-configured';
65
126
  gate: string;
66
127
  stages: Record<string, GuardRunStage>;
67
128
  commit?: string | undefined;
@@ -78,6 +139,8 @@ export interface GuardRunSummary {
78
139
  total: number;
79
140
  passed: number;
80
141
  failed: number;
142
+ incomplete: number;
143
+ policyWeakened: number;
81
144
  warnings: number;
82
145
  averageDurationMs: number;
83
146
  last?: GuardRunManifest | undefined;