@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
@@ -46,6 +46,8 @@ function isGuardRunManifest(value) {
46
46
  Number.isFinite(Date.parse(receipt.generatedAt)) &&
47
47
  (receipt.outcome === 'pass' ||
48
48
  receipt.outcome === 'fail' ||
49
+ receipt.outcome === 'incomplete' ||
50
+ receipt.outcome === 'policy-weakened' ||
49
51
  receipt.outcome === 'warning' ||
50
52
  receipt.outcome === 'needs-review' ||
51
53
  receipt.outcome === 'not-configured') &&
@@ -65,7 +67,96 @@ function isGuardRunManifest(value) {
65
67
  (receipt.subject.baseRef === undefined || typeof receipt.subject.baseRef === 'string') &&
66
68
  (receipt.subject.baseCommit === undefined ||
67
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') &&
68
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'))) &&
69
160
  receipt.stages !== null &&
70
161
  typeof receipt.stages === 'object' &&
71
162
  !Array.isArray(receipt.stages) &&
@@ -80,6 +171,14 @@ function isGuardRunManifest(value) {
80
171
  return ((candidate.status === 'passed' ||
81
172
  candidate.status === 'failed' ||
82
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') &&
83
182
  Number.isInteger(candidate.exitCode) &&
84
183
  typeof candidate.durationMs === 'number' &&
85
184
  Number.isFinite(candidate.durationMs) &&
@@ -144,6 +243,8 @@ function isGuardRunManifest(value) {
144
243
  manifest.durationMs >= 0 &&
145
244
  (manifest.outcome === 'pass' ||
146
245
  manifest.outcome === 'fail' ||
246
+ manifest.outcome === 'incomplete' ||
247
+ manifest.outcome === 'policy-weakened' ||
147
248
  manifest.outcome === 'warning' ||
148
249
  manifest.outcome === 'needs-review' ||
149
250
  manifest.outcome === 'not-configured') &&
@@ -257,6 +358,8 @@ export function summarizeGuardRuns(root, limit = 100) {
257
358
  total,
258
359
  passed: runs.filter((run) => run.outcome === 'pass').length,
259
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,
260
363
  warnings: runs.filter((run) => run.outcome === 'warning').length,
261
364
  averageDurationMs: total === 0 ? 0 : Math.round(runs.reduce((sum, run) => sum + run.durationMs, 0) / total),
262
365
  ...(total > 0 ? { last: runs[0] } : {}),
@@ -287,6 +390,12 @@ export function finishGuardRun(root, context, outcome, gate, command = 'verify',
287
390
  workingTreeDirty: receipt?.subject.workingTreeDirty ?? false,
288
391
  },
289
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 } : {}),
290
399
  stages: receipt?.stages ?? {},
291
400
  checks: receipt?.checks ?? {},
292
401
  findings: receipt?.findings ?? {
@@ -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;