@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
@@ -0,0 +1,166 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { readFileSync } from 'node:fs';
4
+ import { resolve } from 'node:path';
5
+ import { spawnSync } from 'node:child_process';
6
+ import { fileURLToPath } from 'node:url';
7
+
8
+ export const GUARD_STATEMENT_TYPE = 'https://in-toto.io/Statement/v1';
9
+ export const GUARD_PREDICATE_TYPE = 'https://codapult.dev/attestations/guard-verification/v1';
10
+
11
+ function fail(message) {
12
+ throw new Error(message);
13
+ }
14
+
15
+ function isRecord(value) {
16
+ return value !== null && typeof value === 'object' && !Array.isArray(value);
17
+ }
18
+
19
+ function isSha(value) {
20
+ return typeof value === 'string' && /^[a-f0-9]{40}$/i.test(value);
21
+ }
22
+
23
+ /** Validate the Guard statement after its file signature and signer identity have been verified. */
24
+ export function validateGuardStatement(statement, { expectedHead, expectedBase }) {
25
+ if (!isSha(expectedHead) || !isSha(expectedBase)) {
26
+ fail('Expected head and base values must be full 40-character commit SHAs.');
27
+ }
28
+ if (!isRecord(statement) || statement._type !== GUARD_STATEMENT_TYPE) {
29
+ fail(`Unsupported in-toto statement type; expected ${GUARD_STATEMENT_TYPE}.`);
30
+ }
31
+ if (statement.predicateType !== GUARD_PREDICATE_TYPE) {
32
+ fail(`Unsupported Guard predicate type; expected ${GUARD_PREDICATE_TYPE}.`);
33
+ }
34
+ if (!Array.isArray(statement.subject) || statement.subject.length !== 1) {
35
+ fail('Guard statement must contain exactly one subject.');
36
+ }
37
+
38
+ const fingerprint = statement.subject[0]?.digest?.sha256;
39
+ const receipt = statement.predicate?.receipt;
40
+ const scope = receipt?.verificationScope;
41
+ if (statement.subject[0]?.name !== 'codapult-guard:verification-context') {
42
+ fail('Guard statement subject name is invalid.');
43
+ }
44
+ if (typeof fingerprint !== 'string' || !/^[a-f0-9]{64}$/.test(fingerprint)) {
45
+ fail('Guard statement subject must contain a SHA-256 digest.');
46
+ }
47
+ if (!isRecord(receipt) || receipt.version !== 1 || receipt.kind !== 'verification-receipt') {
48
+ fail('Unsupported Guard verification receipt schema.');
49
+ }
50
+ if (
51
+ receipt.outcome !== 'pass' ||
52
+ typeof receipt.runId !== 'string' ||
53
+ receipt.runId.length === 0
54
+ ) {
55
+ fail('Guard receipt is not a successful verification run.');
56
+ }
57
+ if (
58
+ !isRecord(scope) ||
59
+ scope.algorithm !== 'sha256' ||
60
+ scope.complete !== true ||
61
+ scope.stable !== true ||
62
+ scope.fingerprint !== fingerprint
63
+ ) {
64
+ fail('Guard receipt scope is incomplete, unstable, or does not match the statement subject.');
65
+ }
66
+ if (
67
+ !isSha(receipt.subject?.headSha) ||
68
+ receipt.subject.headSha.toLowerCase() !== expectedHead.toLowerCase() ||
69
+ !isSha(receipt.subject?.baseSha) ||
70
+ receipt.subject.baseSha.toLowerCase() !== expectedBase.toLowerCase()
71
+ ) {
72
+ fail('Guard receipt does not match the expected pull request head and base SHAs.');
73
+ }
74
+
75
+ return {
76
+ runId: receipt.runId,
77
+ headSha: receipt.subject.headSha,
78
+ baseSha: receipt.subject.baseSha,
79
+ fingerprint,
80
+ };
81
+ }
82
+
83
+ function parseArgs(args) {
84
+ const [statementPath, ...options] = args;
85
+ const values = new Map();
86
+ for (let index = 0; index < options.length; index += 2) {
87
+ const key = options[index];
88
+ const value = options[index + 1];
89
+ if (!key?.startsWith('--') || !value || values.has(key)) {
90
+ fail('Invalid or duplicate command-line option.');
91
+ }
92
+ values.set(key, value);
93
+ }
94
+
95
+ const required = [
96
+ '--repo',
97
+ '--signer-workflow',
98
+ '--source-ref',
99
+ '--expected-head',
100
+ '--expected-base',
101
+ ];
102
+ if (
103
+ !statementPath ||
104
+ required.some((key) => !values.has(key)) ||
105
+ values.size !== required.length
106
+ ) {
107
+ fail(
108
+ 'Usage: node verify-guard-attestation.mjs <statement.json> --repo ORG/REPO --signer-workflow ORG/REPO/.github/workflows/FILE.yml --source-ref refs/heads/main --expected-head SHA --expected-base SHA',
109
+ );
110
+ }
111
+ return { statementPath, values };
112
+ }
113
+
114
+ export function verifyWithGitHub(
115
+ statementPath,
116
+ { repo, signerWorkflow, sourceRef },
117
+ run = spawnSync,
118
+ ) {
119
+ const args = [
120
+ 'attestation',
121
+ 'verify',
122
+ statementPath,
123
+ '--repo',
124
+ repo,
125
+ '--signer-workflow',
126
+ signerWorkflow,
127
+ '--source-ref',
128
+ sourceRef,
129
+ '--format=json',
130
+ ];
131
+ const result = run('gh', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] });
132
+ if (result.error) fail(`Could not run GitHub CLI: ${result.error.message}`);
133
+ if (result.status !== 0) {
134
+ fail(
135
+ `GitHub attestation verification failed: ${(result.stderr || result.stdout || '').trim()}`,
136
+ );
137
+ }
138
+ }
139
+
140
+ export function main(args = process.argv.slice(2), run = spawnSync) {
141
+ const { statementPath, values } = parseArgs(args);
142
+ const repo = values.get('--repo');
143
+ const signerWorkflow = values.get('--signer-workflow');
144
+ const sourceRef = values.get('--source-ref');
145
+ if (!/^[^/\s]+\/[^/\s]+$/.test(repo)) fail('--repo must use ORG/REPO format.');
146
+ if (!signerWorkflow || !sourceRef) fail('Signer workflow and source ref must be nonempty.');
147
+
148
+ verifyWithGitHub(statementPath, { repo, signerWorkflow, sourceRef }, run);
149
+ const statement = JSON.parse(readFileSync(statementPath, 'utf8'));
150
+ const receipt = validateGuardStatement(statement, {
151
+ expectedHead: values.get('--expected-head'),
152
+ expectedBase: values.get('--expected-base'),
153
+ });
154
+ process.stdout.write(
155
+ `Verified Guard receipt ${receipt.runId}: head ${receipt.headSha}, base ${receipt.baseSha}, scope sha256:${receipt.fingerprint}\n`,
156
+ );
157
+ }
158
+
159
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
160
+ try {
161
+ main();
162
+ } catch (error) {
163
+ process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
164
+ process.exitCode = 1;
165
+ }
166
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@codapult/guard",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "mcpName": "io.github.codapult/guard",
5
5
  "description": "Local-first architecture guardrails for JavaScript and TypeScript projects, with first-class support for Next.js SaaS and AI-assisted development",
6
6
  "license": "MIT",
@@ -44,7 +44,8 @@
44
44
  "./package.json": "./package.json"
45
45
  },
46
46
  "files": [
47
- "dist"
47
+ "dist",
48
+ "docs"
48
49
  ],
49
50
  "scripts": {
50
51
  "clean": "node scripts/clean-dist.mjs",
@@ -104,6 +105,7 @@
104
105
  "semver": "^7.8.5",
105
106
  "ts-morph": "^28.0.0",
106
107
  "typescript": "npm:@typescript/typescript6@^6.0.2",
108
+ "yaml": "^2.9.1",
107
109
  "zod": "^4.6.7"
108
110
  },
109
111
  "devDependencies": {