@codapult/guard 0.9.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 +154 -10
  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 +107 -2
  9. package/dist/core/history/runs.js +244 -18
  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 +701 -40
  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 +11 -9
@@ -13,6 +13,108 @@ export interface GuardRunPolicy {
13
13
  changedFromBase?: boolean | undefined;
14
14
  decisionIds?: string[] | undefined;
15
15
  }
16
+ export interface GuardVerificationReceiptCheck {
17
+ status: 'passed' | 'failed' | 'not-configured';
18
+ exitCode: number;
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
+ }[];
68
+ }
69
+ export interface GuardVerificationReceipt {
70
+ version: 1;
71
+ kind: 'verification-receipt';
72
+ runId: string;
73
+ generatedAt: string;
74
+ outcome: GuardRunManifest['outcome'];
75
+ gate: string;
76
+ subject: {
77
+ commit?: string | undefined;
78
+ baseRef?: string | undefined;
79
+ baseCommit?: string | undefined;
80
+ headSha?: string | undefined;
81
+ baseSha?: string | undefined;
82
+ mergeSha?: string | undefined;
83
+ githubEvent?: string | undefined;
84
+ changedFiles: string[];
85
+ baseChangedFiles?: string[] | undefined;
86
+ workingTreeFiles?: string[] | undefined;
87
+ workingTreeDirty: boolean;
88
+ };
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;
99
+ stages: Record<string, 'ok' | 'fail' | 'skipped'>;
100
+ checks: Record<string, GuardVerificationReceiptCheck>;
101
+ findings: {
102
+ total: number;
103
+ errors: number;
104
+ warnings: number;
105
+ waived: number;
106
+ waiverWarnings: number;
107
+ expiredWaivers: number;
108
+ fingerprints: string[];
109
+ ruleIds: string[];
110
+ };
111
+ contractIssues: string[];
112
+ decisionIds: string[];
113
+ tool: {
114
+ name: string;
115
+ version: string;
116
+ };
117
+ }
16
118
  export interface GuardRunManifest {
17
119
  version: 1;
18
120
  runId: string;
@@ -20,11 +122,12 @@ export interface GuardRunManifest {
20
122
  startedAt: string;
21
123
  completedAt: string;
22
124
  durationMs: number;
23
- outcome: 'pass' | 'fail' | 'warning' | 'needs-review' | 'not-configured';
125
+ outcome: 'pass' | 'fail' | 'incomplete' | 'policy-weakened' | 'warning' | 'needs-review' | 'not-configured';
24
126
  gate: string;
25
127
  stages: Record<string, GuardRunStage>;
26
128
  commit?: string | undefined;
27
129
  policy?: GuardRunPolicy | undefined;
130
+ receipt?: GuardVerificationReceipt | undefined;
28
131
  }
29
132
  export interface GuardRunContext {
30
133
  runId: string;
@@ -36,6 +139,8 @@ export interface GuardRunSummary {
36
139
  total: number;
37
140
  passed: number;
38
141
  failed: number;
142
+ incomplete: number;
143
+ policyWeakened: number;
39
144
  warnings: number;
40
145
  averageDurationMs: number;
41
146
  last?: GuardRunManifest | undefined;
@@ -49,4 +154,4 @@ export declare function writeGuardRun(root: string, manifest: GuardRunManifest):
49
154
  /** Reads locally persisted run manifests without reaching a remote service. */
50
155
  export declare function listGuardRuns(root: string, limit?: number): GuardRunManifest[];
51
156
  export declare function summarizeGuardRuns(root: string, limit?: number): GuardRunSummary;
52
- export declare function finishGuardRun(root: string, context: GuardRunContext, outcome: GuardRunManifest['outcome'], gate: string, command?: string, policy?: GuardRunPolicy): GuardRunManifest;
157
+ export declare function finishGuardRun(root: string, context: GuardRunContext, outcome: GuardRunManifest['outcome'], gate: string, command?: string, policy?: GuardRunPolicy, receipt?: Omit<GuardVerificationReceipt, 'runId' | 'generatedAt' | 'outcome' | 'gate' | 'subject'> & Pick<GuardVerificationReceipt, 'subject'>): GuardRunManifest;
@@ -12,11 +12,208 @@ export class GuardRunManifestError extends Error {
12
12
  this.name = 'GuardRunManifestError';
13
13
  }
14
14
  }
15
+ function isGuardRunPolicy(value) {
16
+ if (value === null || typeof value !== 'object')
17
+ return false;
18
+ const policy = value;
19
+ return (typeof policy.fingerprint === 'string' &&
20
+ policy.fingerprint.length > 0 &&
21
+ (policy.source === 'working-tree' || policy.source === 'git-ref') &&
22
+ (policy.revision === undefined ||
23
+ (Number.isInteger(policy.revision) && policy.revision >= 0)) &&
24
+ (policy.ref === undefined || typeof policy.ref === 'string') &&
25
+ (policy.changedFromBase === undefined || typeof policy.changedFromBase === 'boolean') &&
26
+ (policy.decisionIds === undefined ||
27
+ (Array.isArray(policy.decisionIds) &&
28
+ new Set(policy.decisionIds).size === policy.decisionIds.length &&
29
+ policy.decisionIds.every((decisionId) => typeof decisionId === 'string' && decisionId.trim().length > 0) &&
30
+ (policy.source === 'working-tree' || policy.decisionIds.length === 0))));
31
+ }
15
32
  function isGuardRunManifest(value) {
16
33
  if (value === null || typeof value !== 'object')
17
34
  return false;
18
35
  const manifest = value;
19
36
  const stages = manifest.stages;
37
+ const receipt = manifest.receipt;
38
+ const validReceipt = receipt === undefined ||
39
+ (receipt !== null &&
40
+ typeof receipt === 'object' &&
41
+ receipt.version === 1 &&
42
+ receipt.kind === 'verification-receipt' &&
43
+ typeof receipt.runId === 'string' &&
44
+ receipt.runId === manifest.runId &&
45
+ typeof receipt.generatedAt === 'string' &&
46
+ Number.isFinite(Date.parse(receipt.generatedAt)) &&
47
+ (receipt.outcome === 'pass' ||
48
+ receipt.outcome === 'fail' ||
49
+ receipt.outcome === 'incomplete' ||
50
+ receipt.outcome === 'policy-weakened' ||
51
+ receipt.outcome === 'warning' ||
52
+ receipt.outcome === 'needs-review' ||
53
+ receipt.outcome === 'not-configured') &&
54
+ typeof receipt.gate === 'string' &&
55
+ receipt.subject !== null &&
56
+ typeof receipt.subject === 'object' &&
57
+ Array.isArray(receipt.subject.changedFiles) &&
58
+ receipt.subject.changedFiles.every((file) => typeof file === 'string') &&
59
+ (receipt.subject.baseChangedFiles === undefined ||
60
+ (Array.isArray(receipt.subject.baseChangedFiles) &&
61
+ receipt.subject.baseChangedFiles.every((file) => typeof file === 'string'))) &&
62
+ (receipt.subject.workingTreeFiles === undefined ||
63
+ (Array.isArray(receipt.subject.workingTreeFiles) &&
64
+ receipt.subject.workingTreeFiles.every((file) => typeof file === 'string'))) &&
65
+ typeof receipt.subject.workingTreeDirty === 'boolean' &&
66
+ (receipt.subject.commit === undefined || typeof receipt.subject.commit === 'string') &&
67
+ (receipt.subject.baseRef === undefined || typeof receipt.subject.baseRef === 'string') &&
68
+ (receipt.subject.baseCommit === undefined ||
69
+ typeof receipt.subject.baseCommit === 'string') &&
70
+ (receipt.subject.headSha === undefined || /^[a-f0-9]{40}$/i.test(receipt.subject.headSha)) &&
71
+ (receipt.subject.baseSha === undefined || /^[a-f0-9]{40}$/i.test(receipt.subject.baseSha)) &&
72
+ (receipt.subject.mergeSha === undefined ||
73
+ /^[a-f0-9]{40}$/i.test(receipt.subject.mergeSha)) &&
74
+ (receipt.subject.githubEvent === undefined ||
75
+ typeof receipt.subject.githubEvent === 'string') &&
76
+ (receipt.policy === undefined || isGuardRunPolicy(receipt.policy)) &&
77
+ (receipt.verificationScope === undefined ||
78
+ (receipt.verificationScope !== null &&
79
+ typeof receipt.verificationScope === 'object' &&
80
+ receipt.verificationScope.algorithm === 'sha256' &&
81
+ /^[a-f0-9]{64}$/.test(receipt.verificationScope.fingerprint) &&
82
+ typeof receipt.verificationScope.complete === 'boolean' &&
83
+ typeof receipt.verificationScope.stable === 'boolean' &&
84
+ Array.isArray(receipt.verificationScope.files) &&
85
+ receipt.verificationScope.files.every((file) => file !== null &&
86
+ typeof file === 'object' &&
87
+ typeof file.path === 'string' &&
88
+ (file.status === 'present' ||
89
+ file.status === 'deleted' ||
90
+ file.status === 'unavailable') &&
91
+ (file.status === 'unavailable'
92
+ ? file.sha256 === undefined
93
+ : typeof file.sha256 === 'string' && /^[a-f0-9]{64}$/.test(file.sha256))))) &&
94
+ (receipt.policySurface === undefined ||
95
+ (receipt.policySurface !== null &&
96
+ typeof receipt.policySurface === 'object' &&
97
+ Array.isArray(receipt.policySurface.protectedPaths) &&
98
+ receipt.policySurface.protectedPaths.every((path) => typeof path === 'string') &&
99
+ Array.isArray(receipt.policySurface.changedPaths) &&
100
+ receipt.policySurface.changedPaths.every((path) => typeof path === 'string') &&
101
+ (receipt.policySurface.changes === undefined ||
102
+ (Array.isArray(receipt.policySurface.changes) &&
103
+ receipt.policySurface.changes.every((change) => change !== null &&
104
+ typeof change === 'object' &&
105
+ typeof change.path === 'string' &&
106
+ typeof change.pattern === 'string' &&
107
+ typeof change.kind === 'string' &&
108
+ (change.severity === 'warning' || change.severity === 'error')))) &&
109
+ Array.isArray(receipt.policySurface.suppressionMarkers) &&
110
+ receipt.policySurface.suppressionMarkers.every((marker) => typeof marker === 'string') &&
111
+ typeof receipt.policySurface.workflowChanged === 'boolean' &&
112
+ typeof receipt.policySurface.complete === 'boolean')) &&
113
+ (receipt.ciIntegrity === undefined ||
114
+ (receipt.ciIntegrity !== null &&
115
+ typeof receipt.ciIntegrity === 'object' &&
116
+ (receipt.ciIntegrity.status === 'ok' ||
117
+ receipt.ciIntegrity.status === 'policy-weakened' ||
118
+ receipt.ciIntegrity.status === 'incomplete') &&
119
+ Array.isArray(receipt.ciIntegrity.workflows) &&
120
+ receipt.ciIntegrity.workflows.every((workflow) => workflow !== null &&
121
+ typeof workflow === 'object' &&
122
+ typeof workflow.path === 'string' &&
123
+ typeof workflow.job === 'string' &&
124
+ (workflow.digest === undefined || typeof workflow.digest === 'string') &&
125
+ (workflow.baseDigest === undefined || typeof workflow.baseDigest === 'string') &&
126
+ Array.isArray(workflow.actions) &&
127
+ workflow.actions.every((action) => typeof action === 'string') &&
128
+ (workflow.baseActions === undefined ||
129
+ (Array.isArray(workflow.baseActions) &&
130
+ workflow.baseActions.every((action) => typeof action === 'string'))) &&
131
+ (workflow.actionChanges === undefined ||
132
+ (Array.isArray(workflow.actionChanges) &&
133
+ workflow.actionChanges.every((change) => change !== null &&
134
+ typeof change === 'object' &&
135
+ typeof change.name === 'string' &&
136
+ Array.isArray(change.base) &&
137
+ change.base.every((action) => typeof action === 'string') &&
138
+ Array.isArray(change.current) &&
139
+ change.current.every((action) => typeof action === 'string')))) &&
140
+ (workflow.status === 'ok' ||
141
+ workflow.status === 'policy-weakened' ||
142
+ workflow.status === 'incomplete') &&
143
+ Array.isArray(workflow.issues) &&
144
+ workflow.issues.every((issue) => issue !== null &&
145
+ typeof issue === 'object' &&
146
+ typeof issue.code === 'string' &&
147
+ typeof issue.message === 'string')))) &&
148
+ (receipt.requiredStages === undefined ||
149
+ (Array.isArray(receipt.requiredStages) &&
150
+ receipt.requiredStages.every((stage) => typeof stage === 'string'))) &&
151
+ (receipt.incompleteStages === undefined ||
152
+ (Array.isArray(receipt.incompleteStages) &&
153
+ receipt.incompleteStages.every((stage) => typeof stage === 'string'))) &&
154
+ (receipt.effectiveInvocation === undefined ||
155
+ (receipt.effectiveInvocation !== null &&
156
+ typeof receipt.effectiveInvocation === 'object' &&
157
+ typeof receipt.effectiveInvocation.command === 'string' &&
158
+ Array.isArray(receipt.effectiveInvocation.args) &&
159
+ receipt.effectiveInvocation.args.every((argument) => typeof argument === 'string'))) &&
160
+ receipt.stages !== null &&
161
+ typeof receipt.stages === 'object' &&
162
+ !Array.isArray(receipt.stages) &&
163
+ Object.values(receipt.stages).every((status) => status === 'ok' || status === 'fail' || status === 'skipped') &&
164
+ receipt.checks !== null &&
165
+ typeof receipt.checks === 'object' &&
166
+ !Array.isArray(receipt.checks) &&
167
+ Object.values(receipt.checks).every((check) => {
168
+ if (check === null || typeof check !== 'object')
169
+ return false;
170
+ const candidate = check;
171
+ return ((candidate.status === 'passed' ||
172
+ candidate.status === 'failed' ||
173
+ candidate.status === 'not-configured') &&
174
+ (candidate.command === undefined || typeof candidate.command === 'string') &&
175
+ (candidate.args === undefined ||
176
+ (Array.isArray(candidate.args) &&
177
+ candidate.args.every((argument) => typeof argument === 'string'))) &&
178
+ (candidate.execution === undefined ||
179
+ candidate.execution === 'attempted' ||
180
+ candidate.execution === 'rejected' ||
181
+ candidate.execution === 'not-configured') &&
182
+ Number.isInteger(candidate.exitCode) &&
183
+ typeof candidate.durationMs === 'number' &&
184
+ Number.isFinite(candidate.durationMs) &&
185
+ candidate.durationMs >= 0);
186
+ }) &&
187
+ receipt.findings !== null &&
188
+ typeof receipt.findings === 'object' &&
189
+ ['total', 'errors', 'warnings', 'waived', 'waiverWarnings', 'expiredWaivers'].every((key) => {
190
+ const countValue = receipt.findings[key];
191
+ return typeof countValue === 'number' && Number.isInteger(countValue) && countValue >= 0;
192
+ }) &&
193
+ Array.isArray(receipt.findings.fingerprints) &&
194
+ receipt.findings.fingerprints.every((fingerprint) => typeof fingerprint === 'string') &&
195
+ Array.isArray(receipt.findings.ruleIds) &&
196
+ receipt.findings.ruleIds.every((ruleId) => typeof ruleId === 'string') &&
197
+ Array.isArray(receipt.contractIssues) &&
198
+ receipt.contractIssues.every((issue) => typeof issue === 'string') &&
199
+ Array.isArray(receipt.decisionIds) &&
200
+ new Set(receipt.decisionIds).size === receipt.decisionIds.length &&
201
+ receipt.decisionIds.every((decisionId) => typeof decisionId === 'string') &&
202
+ receipt.tool !== null &&
203
+ typeof receipt.tool === 'object' &&
204
+ typeof receipt.tool.name === 'string' &&
205
+ typeof receipt.tool.version === 'string');
206
+ const receiptMatchesManifest = receipt === undefined ||
207
+ (receipt.outcome === manifest.outcome &&
208
+ receipt.gate === manifest.gate &&
209
+ (manifest.commit === undefined
210
+ ? receipt.subject.commit === undefined
211
+ : receipt.subject.commit === manifest.commit) &&
212
+ (manifest.policy === undefined
213
+ ? receipt.policy === undefined
214
+ : receipt.policy?.fingerprint === manifest.policy.fingerprint &&
215
+ receipt.policy.source === manifest.policy.source &&
216
+ receipt.policy.revision === manifest.policy.revision));
20
217
  const validStages = stages !== null &&
21
218
  typeof stages === 'object' &&
22
219
  !Array.isArray(stages) &&
@@ -46,27 +243,16 @@ function isGuardRunManifest(value) {
46
243
  manifest.durationMs >= 0 &&
47
244
  (manifest.outcome === 'pass' ||
48
245
  manifest.outcome === 'fail' ||
246
+ manifest.outcome === 'incomplete' ||
247
+ manifest.outcome === 'policy-weakened' ||
49
248
  manifest.outcome === 'warning' ||
50
249
  manifest.outcome === 'needs-review' ||
51
250
  manifest.outcome === 'not-configured') &&
52
251
  typeof manifest.gate === 'string' &&
53
252
  validStages &&
54
- (manifest.policy === undefined ||
55
- (manifest.policy !== null &&
56
- typeof manifest.policy === 'object' &&
57
- typeof manifest.policy.fingerprint === 'string' &&
58
- manifest.policy.fingerprint.length > 0 &&
59
- (manifest.policy.source === 'working-tree' || manifest.policy.source === 'git-ref') &&
60
- (manifest.policy.revision === undefined ||
61
- (Number.isInteger(manifest.policy.revision) && manifest.policy.revision >= 0)) &&
62
- (manifest.policy.ref === undefined || typeof manifest.policy.ref === 'string') &&
63
- (manifest.policy.changedFromBase === undefined ||
64
- typeof manifest.policy.changedFromBase === 'boolean') &&
65
- (manifest.policy.decisionIds === undefined ||
66
- (manifest.policy.source === 'working-tree' &&
67
- Array.isArray(manifest.policy.decisionIds) &&
68
- new Set(manifest.policy.decisionIds).size === manifest.policy.decisionIds.length &&
69
- manifest.policy.decisionIds.every((decisionId) => typeof decisionId === 'string' && decisionId.trim().length > 0))))));
253
+ validReceipt &&
254
+ receiptMatchesManifest &&
255
+ (manifest.policy === undefined || isGuardRunPolicy(manifest.policy)));
70
256
  }
71
257
  export function startGuardRun() {
72
258
  return {
@@ -172,12 +358,14 @@ export function summarizeGuardRuns(root, limit = 100) {
172
358
  total,
173
359
  passed: runs.filter((run) => run.outcome === 'pass').length,
174
360
  failed: runs.filter((run) => run.outcome === 'fail').length,
361
+ incomplete: runs.filter((run) => run.outcome === 'incomplete').length,
362
+ policyWeakened: runs.filter((run) => run.outcome === 'policy-weakened').length,
175
363
  warnings: runs.filter((run) => run.outcome === 'warning').length,
176
364
  averageDurationMs: total === 0 ? 0 : Math.round(runs.reduce((sum, run) => sum + run.durationMs, 0) / total),
177
365
  ...(total > 0 ? { last: runs[0] } : {}),
178
366
  };
179
367
  }
180
- export function finishGuardRun(root, context, outcome, gate, command = 'verify', policy) {
368
+ export function finishGuardRun(root, context, outcome, gate, command = 'verify', policy, receipt) {
181
369
  let commit;
182
370
  try {
183
371
  commit = execFileSync('git', ['rev-parse', 'HEAD'], { cwd: root, stdio: 'pipe' })
@@ -187,18 +375,56 @@ export function finishGuardRun(root, context, outcome, gate, command = 'verify',
187
375
  catch {
188
376
  commit = undefined;
189
377
  }
378
+ const completedAt = new Date().toISOString();
379
+ const completedReceipt = {
380
+ version: 1,
381
+ kind: 'verification-receipt',
382
+ runId: context.runId,
383
+ generatedAt: completedAt,
384
+ outcome,
385
+ gate,
386
+ subject: {
387
+ ...receipt?.subject,
388
+ ...(commit ? { commit } : {}),
389
+ changedFiles: receipt?.subject.changedFiles ?? [],
390
+ workingTreeDirty: receipt?.subject.workingTreeDirty ?? false,
391
+ },
392
+ ...(policy ? { policy } : {}),
393
+ ...(receipt?.verificationScope ? { verificationScope: receipt.verificationScope } : {}),
394
+ ...(receipt?.policySurface ? { policySurface: receipt.policySurface } : {}),
395
+ ...(receipt?.ciIntegrity ? { ciIntegrity: receipt.ciIntegrity } : {}),
396
+ requiredStages: receipt?.requiredStages ?? [],
397
+ incompleteStages: receipt?.incompleteStages ?? [],
398
+ ...(receipt?.effectiveInvocation ? { effectiveInvocation: receipt.effectiveInvocation } : {}),
399
+ stages: receipt?.stages ?? {},
400
+ checks: receipt?.checks ?? {},
401
+ findings: receipt?.findings ?? {
402
+ total: 0,
403
+ errors: 0,
404
+ warnings: 0,
405
+ waived: 0,
406
+ waiverWarnings: 0,
407
+ expiredWaivers: 0,
408
+ fingerprints: [],
409
+ ruleIds: [],
410
+ },
411
+ contractIssues: receipt?.contractIssues ?? [],
412
+ decisionIds: receipt?.decisionIds ?? policy?.decisionIds ?? [],
413
+ tool: receipt?.tool ?? { name: '@codapult/guard', version: 'unknown' },
414
+ };
190
415
  const manifest = {
191
416
  version: 1,
192
417
  runId: context.runId,
193
418
  command,
194
419
  startedAt: context.startedAt,
195
- completedAt: new Date().toISOString(),
420
+ completedAt,
196
421
  durationMs: Math.max(0, Date.now() - context.startedAtMs),
197
422
  outcome,
198
423
  gate,
199
424
  stages: context.stages,
200
425
  ...(commit ? { commit } : {}),
201
426
  ...(policy ? { policy } : {}),
427
+ receipt: completedReceipt,
202
428
  };
203
429
  try {
204
430
  writeGuardRun(root, manifest);
@@ -0,0 +1,17 @@
1
+ import type { GuardVerificationReceipt } from '../history/runs.js';
2
+ export declare const GUARD_VERIFICATION_PREDICATE_TYPE = "https://codapult.dev/attestations/guard-verification/v1";
3
+ export interface GuardVerificationInTotoStatement {
4
+ _type: 'https://in-toto.io/Statement/v1';
5
+ subject: {
6
+ name: 'codapult-guard:verification-context';
7
+ digest: {
8
+ sha256: string;
9
+ };
10
+ }[];
11
+ predicateType: typeof GUARD_VERIFICATION_PREDICATE_TYPE;
12
+ predicate: {
13
+ receipt: GuardVerificationReceipt;
14
+ };
15
+ }
16
+ /** Bind a successful Guard receipt to the exact policy and file scope it verified. */
17
+ export declare function guardReceiptToInToto(receipt: GuardVerificationReceipt): GuardVerificationInTotoStatement;
@@ -0,0 +1,22 @@
1
+ export const GUARD_VERIFICATION_PREDICATE_TYPE = 'https://codapult.dev/attestations/guard-verification/v1';
2
+ /** Bind a successful Guard receipt to the exact policy and file scope it verified. */
3
+ export function guardReceiptToInToto(receipt) {
4
+ const scope = receipt.verificationScope;
5
+ if (receipt.outcome !== 'pass' ||
6
+ !scope?.complete ||
7
+ !scope.stable ||
8
+ !/^[a-f0-9]{64}$/.test(scope.fingerprint)) {
9
+ throw new Error('Only a passing receipt with a complete, stable verification scope can be attested.');
10
+ }
11
+ return {
12
+ _type: 'https://in-toto.io/Statement/v1',
13
+ subject: [
14
+ {
15
+ name: 'codapult-guard:verification-context',
16
+ digest: { sha256: scope.fingerprint },
17
+ },
18
+ ],
19
+ predicateType: GUARD_VERIFICATION_PREDICATE_TYPE,
20
+ predicate: { receipt },
21
+ };
22
+ }
@@ -226,6 +226,44 @@ export declare const guardConfigSchema: z.ZodObject<{
226
226
  warningDays: z.ZodNumber;
227
227
  maxDays: z.ZodOptional<z.ZodNumber>;
228
228
  }, z.core.$strip>>;
229
+ protectedPaths: z.ZodOptional<z.ZodArray<z.ZodString>>;
230
+ policySurface: z.ZodOptional<z.ZodObject<{
231
+ paths: z.ZodDefault<z.ZodArray<z.ZodObject<{
232
+ pattern: z.ZodString;
233
+ kind: z.ZodEnum<{
234
+ "ci-workflow": "ci-workflow";
235
+ custom: "custom";
236
+ "dependency-input": "dependency-input";
237
+ "guard-policy": "guard-policy";
238
+ "ignore-config": "ignore-config";
239
+ ownership: "ownership";
240
+ "suppression-source": "suppression-source";
241
+ "tool-config": "tool-config";
242
+ }>;
243
+ severity: z.ZodDefault<z.ZodEnum<{
244
+ error: "error";
245
+ warning: "warning";
246
+ }>>;
247
+ }, z.core.$strip>>>;
248
+ }, z.core.$strip>>;
249
+ githubActions: z.ZodOptional<z.ZodObject<{
250
+ workflows: z.ZodArray<z.ZodObject<{
251
+ path: z.ZodString;
252
+ job: z.ZodString;
253
+ requiredRunIncludes: z.ZodArray<z.ZodString>;
254
+ requiredNeeds: z.ZodOptional<z.ZodArray<z.ZodString>>;
255
+ requirePullRequest: z.ZodDefault<z.ZodBoolean>;
256
+ requireUnfilteredPullRequest: z.ZodDefault<z.ZodBoolean>;
257
+ requireUnconditionalJob: z.ZodDefault<z.ZodBoolean>;
258
+ requireFullShaActions: z.ZodDefault<z.ZodBoolean>;
259
+ disallowContinueOnError: z.ZodDefault<z.ZodBoolean>;
260
+ maxPermissions: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
261
+ none: "none";
262
+ read: "read";
263
+ write: "write";
264
+ }>>>;
265
+ }, z.core.$strip>>;
266
+ }, z.core.$strip>>;
229
267
  }, z.core.$strip>;
230
268
  export declare const guardContractsFileSchema: z.ZodObject<{
231
269
  version: z.ZodLiteral<1>;
@@ -408,5 +446,14 @@ export declare const guardAgentConfigSchema: z.ZodObject<{
408
446
  test: "test";
409
447
  typecheck: "typecheck";
410
448
  }>>;
449
+ requiredStages: z.ZodDefault<z.ZodArray<z.ZodEnum<{
450
+ adapters: "adapters";
451
+ "architecture-policy": "architecture-policy";
452
+ "ci-integrity": "ci-integrity";
453
+ "policy-surface": "policy-surface";
454
+ "project-checks": "project-checks";
455
+ runtime: "runtime";
456
+ "verification-scope": "verification-scope";
457
+ }>>>;
411
458
  }, z.core.$strip>;
412
459
  }, z.core.$strip>;
@@ -89,6 +89,61 @@ export const guardConfigSchema = z.object({
89
89
  path: ['maxDays'],
90
90
  })
91
91
  .optional(),
92
+ protectedPaths: z
93
+ .array(z
94
+ .string()
95
+ .trim()
96
+ .min(1)
97
+ .refine((path) => !path.startsWith('/') && !path.includes('\\') && !path.split('/').includes('..'), 'protectedPaths entries must be repository-relative paths without parent traversal'))
98
+ .optional(),
99
+ policySurface: z
100
+ .object({
101
+ paths: z
102
+ .array(z.object({
103
+ pattern: z
104
+ .string()
105
+ .trim()
106
+ .min(1)
107
+ .refine((path) => !path.startsWith('/') && !path.includes('\\') && !path.split('/').includes('..'), 'policySurface paths must be repository-relative without parent traversal'),
108
+ kind: z.enum([
109
+ 'ci-workflow',
110
+ 'guard-policy',
111
+ 'ownership',
112
+ 'dependency-input',
113
+ 'tool-config',
114
+ 'ignore-config',
115
+ 'suppression-source',
116
+ 'custom',
117
+ ]),
118
+ severity: z.enum(['warning', 'error']).default('error'),
119
+ }))
120
+ .default([]),
121
+ })
122
+ .optional(),
123
+ githubActions: z
124
+ .object({
125
+ workflows: z
126
+ .array(z.object({
127
+ path: z
128
+ .string()
129
+ .trim()
130
+ .min(1)
131
+ .refine((path) => !path.startsWith('/') && !path.includes('\\') && !path.split('/').includes('..'), 'GitHub Actions workflow paths must be repository-relative without parent traversal'),
132
+ job: z.string().trim().min(1),
133
+ requiredRunIncludes: z.array(z.string().trim().min(1)).min(1),
134
+ requiredNeeds: z.array(z.string().trim().min(1)).optional(),
135
+ requirePullRequest: z.boolean().default(true),
136
+ requireUnfilteredPullRequest: z.boolean().default(true),
137
+ requireUnconditionalJob: z.boolean().default(true),
138
+ requireFullShaActions: z.boolean().default(true),
139
+ disallowContinueOnError: z.boolean().default(true),
140
+ maxPermissions: z
141
+ .record(z.string().trim().min(1), z.enum(['none', 'read', 'write']))
142
+ .optional(),
143
+ }))
144
+ .min(1),
145
+ })
146
+ .optional(),
92
147
  });
93
148
  export const guardContractsFileSchema = z.object({
94
149
  version: z.literal(1),
@@ -167,5 +222,17 @@ export const guardAgentConfigSchema = z.object({
167
222
  maxIterations: z.number().int().min(1).max(10),
168
223
  projectChecks: z.boolean(),
169
224
  checks: z.array(z.enum(['lint', 'typecheck', 'test', 'build'])),
225
+ requiredStages: z
226
+ .array(z.enum([
227
+ 'runtime',
228
+ 'project-checks',
229
+ 'adapters',
230
+ 'architecture-policy',
231
+ 'policy-surface',
232
+ 'ci-integrity',
233
+ 'verification-scope',
234
+ ]))
235
+ .min(1)
236
+ .default(['architecture-policy']),
170
237
  }),
171
238
  });
@@ -0,0 +1,25 @@
1
+ import type { GuardGitHubActionsPolicy } from '../guard.js';
2
+ export interface GuardCiIntegrityIssue {
3
+ code: 'workflow-unreadable' | 'workflow-invalid' | 'pull-request-trigger-missing' | 'pull-request-filtered' | 'pull-request-target-present' | 'job-missing' | 'job-conditional' | 'command-missing' | 'step-conditional' | 'continue-on-error' | 'dependency-missing' | 'dependency-conditional' | 'dependency-continue-on-error' | 'action-not-sha-pinned' | 'action-reference-changed' | 'base-workflow-unavailable' | 'permission-expansion';
4
+ message: string;
5
+ }
6
+ export interface GuardCiIntegrityWorkflowResult {
7
+ path: string;
8
+ job: string;
9
+ digest?: string | undefined;
10
+ baseDigest?: string | undefined;
11
+ actions: string[];
12
+ baseActions?: string[] | undefined;
13
+ actionChanges?: {
14
+ name: string;
15
+ base: string[];
16
+ current: string[];
17
+ }[] | undefined;
18
+ status: 'ok' | 'policy-weakened' | 'incomplete';
19
+ issues: GuardCiIntegrityIssue[];
20
+ }
21
+ export interface GuardCiIntegrityResult {
22
+ status: 'ok' | 'policy-weakened' | 'incomplete';
23
+ workflows: GuardCiIntegrityWorkflowResult[];
24
+ }
25
+ export declare function auditGitHubActions(root: string, policy: GuardGitHubActionsPolicy | undefined, baseRef?: string): GuardCiIntegrityResult | undefined;