@open-agent-toolkit/cli 0.2.73 → 0.2.75
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/bundle-metadata.json +1 -1
- package/assets/docs/cli-utilities/configuration.md +3 -56
- package/assets/docs/cli-utilities/tool-packs.md +4 -4
- package/assets/docs/contributing/code.md +2 -2
- package/assets/docs/contributing/index.md +0 -1
- package/assets/docs/reference/cli-reference.md +1 -1
- package/assets/docs/reference/troubleshooting.md +17 -40
- package/assets/docs/workflows/projects/artifacts.md +16 -34
- package/assets/docs/workflows/projects/lifecycle.md +20 -19
- package/assets/docs/workflows/skills/explainer-kit.md +180 -440
- package/assets/docs/workflows/skills/index.md +1 -2
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/explainer-kit/SKILL.md +96 -171
- package/assets/skills/explainer-kit/recipes/engineer-tour.json +1 -22
- package/assets/skills/explainer-kit/recipes/program-recap.json +1 -29
- package/assets/skills/explainer-kit/recipes/project-explainer.json +1 -22
- package/assets/skills/explainer-kit/recipes/project-recap.v2.json +1 -43
- package/assets/skills/explainer-kit/references/fact-base-contract.md +76 -101
- package/assets/skills/explainer-kit/references/recap-authoring.md +99 -0
- package/assets/skills/explainer-kit/schemas/manifest.schema.json +13 -135
- package/assets/skills/explainer-kit/scripts/bundle.mjs +712 -0
- package/assets/skills/explainer-kit/scripts/lib/claim-subject.mjs +40 -0
- package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +113 -1504
- package/assets/skills/explainer-kit/scripts/lib/failure.mjs +13 -0
- package/assets/skills/explainer-kit/scripts/lib/package-coverage.mjs +57 -403
- package/assets/skills/explainer-kit/scripts/lib/qa-result.mjs +168 -0
- package/assets/skills/explainer-kit/scripts/lib/qa.mjs +90 -488
- package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +28 -545
- package/assets/skills/explainer-kit/scripts/lib/run-package.mjs +120 -0
- package/assets/skills/explainer-kit/scripts/lib/sanitize.mjs +42 -0
- package/assets/skills/explainer-kit/scripts/record.mjs +335 -0
- package/assets/skills/explainer-kit/scripts/verify.mjs +648 -0
- package/assets/skills/oat-explainer-kit/SKILL.md +57 -117
- package/assets/skills/oat-explainer-kit/references/config-contract.md +20 -64
- package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +85 -237
- package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +18 -10
- package/assets/skills/oat-explainer-kit/scripts/check-core.mjs +1 -0
- package/assets/skills/oat-explainer-kit/scripts/check-terminal-outcome.mjs +256 -21
- package/assets/skills/oat-explainer-kit/scripts/persist-intent.mjs +82 -3
- package/assets/skills/oat-explainer-kit/scripts/resolve-config.mjs +0 -162
- package/assets/skills/oat-explainer-kit/scripts/resolve-intent.mjs +57 -123
- package/assets/skills/oat-project-autonomous/SKILL.md +14 -13
- package/assets/skills/oat-project-autonomous/references/gate-inventory.md +165 -165
- package/assets/skills/oat-project-complete/SKILL.md +168 -349
- package/assets/skills/oat-project-complete/scripts/consume-persisted-recap-intent.mjs +125 -0
- package/assets/skills/oat-project-complete/scripts/execute-synced-archive-entry.mjs +0 -30
- package/assets/skills/oat-project-complete/scripts/parse-completion-retry-fields.mjs +4 -37
- package/assets/skills/oat-project-complete/scripts/parse-synced-archive-resume-fields.mjs +17 -16
- package/assets/skills/oat-project-complete/scripts/recover-completion-receipts.mjs +9 -174
- package/assets/skills/oat-project-complete/scripts/resolve-completion-retry.mjs +1 -2
- package/assets/skills/oat-project-document/SKILL.md +1 -1
- package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-implement/SKILL.md +1 -1
- package/assets/skills/oat-project-implement/references/completion-and-closeout.md +44 -56
- package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-lite/SKILL.md +1 -1
- package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-plan/SKILL.md +5 -6
- package/assets/skills/oat-project-pr-final/SKILL.md +1 -1
- package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-quick-start/SKILL.md +1 -1
- package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +165 -165
- package/assets/skills/oat-project-summary/SKILL.md +35 -25
- package/assets/skills/oat-wave-execute/SKILL.md +11 -36
- package/assets/skills/oat-wave-program/SKILL.md +13 -37
- package/dist/commands/config/index.d.ts.map +1 -1
- package/dist/commands/config/index.js +9 -113
- package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
- package/dist/commands/project/archive/archive-utils.js +32 -184
- package/dist/commands/project/archive/explainer-package-coverage.d.ts +3 -8
- package/dist/commands/project/archive/explainer-package-coverage.d.ts.map +1 -1
- package/dist/commands/project/archive/explainer-package-coverage.js +1 -1
- package/dist/config/oat-config.d.ts +0 -11
- package/dist/config/oat-config.d.ts.map +1 -1
- package/dist/config/oat-config.js +0 -34
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +0 -8
- package/dist/release/public-package-contract.d.ts.map +1 -1
- package/dist/release/public-package-contract.js +2 -10
- package/dist/validation/project-state.d.ts.map +1 -1
- package/dist/validation/project-state.js +42 -3
- package/package.json +2 -2
- package/assets/docs/contributing/explainer-kit-verification.md +0 -133
- package/assets/docs/workflows/skills/explainer-kit-providers.md +0 -155
- package/assets/skills/explainer-kit/briefs/deep-dive.md +0 -35
- package/assets/skills/explainer-kit/briefs/project-page.md +0 -38
- package/assets/skills/explainer-kit/briefs/supporting-diagram.md +0 -28
- package/assets/skills/explainer-kit/briefs/walkthrough-deck.md +0 -35
- package/assets/skills/explainer-kit/examples/project-explainer/content.md +0 -31
- package/assets/skills/explainer-kit/examples/project-explainer/fact-base.md +0 -22
- package/assets/skills/explainer-kit/examples/project-recap/content.md +0 -77
- package/assets/skills/explainer-kit/examples/project-recap/fact-base.json +0 -104
- package/assets/skills/explainer-kit/examples/project-recap/fact-base.md +0 -44
- package/assets/skills/explainer-kit/examples/theme-bundle.json +0 -87
- package/assets/skills/explainer-kit/recipes/project-recap.v1.json +0 -93
- package/assets/skills/explainer-kit/references/contracts.md +0 -324
- package/assets/skills/explainer-kit/references/destination-contract.md +0 -210
- package/assets/skills/explainer-kit/references/extension-contract.md +0 -80
- package/assets/skills/explainer-kit/references/golden-conformance.md +0 -80
- package/assets/skills/explainer-kit/references/visual-review.md +0 -71
- package/assets/skills/explainer-kit/schemas/author-request.v2.schema.json +0 -212
- package/assets/skills/explainer-kit/schemas/author-request.v3.schema.json +0 -241
- package/assets/skills/explainer-kit/schemas/author-result.v2.schema.json +0 -52
- package/assets/skills/explainer-kit/schemas/build-record.schema.json +0 -98
- package/assets/skills/explainer-kit/schemas/durability-evidence.schema.json +0 -53
- package/assets/skills/explainer-kit/schemas/publish-receipt.v1.schema.json +0 -97
- package/assets/skills/explainer-kit/schemas/publish-receipt.v2.schema.json +0 -215
- package/assets/skills/explainer-kit/schemas/publish-request.v1.schema.json +0 -32
- package/assets/skills/explainer-kit/schemas/publish-request.v2.schema.json +0 -34
- package/assets/skills/explainer-kit/schemas/run-request.schema.json +0 -147
- package/assets/skills/explainer-kit/schemas/set-plan.v1.schema.json +0 -149
- package/assets/skills/explainer-kit/schemas/terminal-evidence.v1.schema.json +0 -81
- package/assets/skills/explainer-kit/schemas/visual-review-evidence.v1.schema.json +0 -66
- package/assets/skills/explainer-kit/schemas/visual-review-request.v1.schema.json +0 -117
- package/assets/skills/explainer-kit/schemas/visual-review-result.v1.schema.json +0 -80
- package/assets/skills/explainer-kit/scripts/lib/catalog.mjs +0 -349
- package/assets/skills/explainer-kit/scripts/lib/content-approval.mjs +0 -418
- package/assets/skills/explainer-kit/scripts/lib/diagram.mjs +0 -514
- package/assets/skills/explainer-kit/scripts/lib/durability.mjs +0 -643
- package/assets/skills/explainer-kit/scripts/lib/fact-base.mjs +0 -664
- package/assets/skills/explainer-kit/scripts/lib/internal-references.mjs +0 -538
- package/assets/skills/explainer-kit/scripts/lib/markdown.mjs +0 -414
- package/assets/skills/explainer-kit/scripts/lib/publication-policy.mjs +0 -54
- package/assets/skills/explainer-kit/scripts/lib/records.mjs +0 -954
- package/assets/skills/explainer-kit/scripts/lib/render.mjs +0 -703
- package/assets/skills/explainer-kit/scripts/lib/s3-roots.mjs +0 -353
- package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +0 -759
- package/assets/skills/explainer-kit/scripts/lib/set-plan.mjs +0 -209
- package/assets/skills/explainer-kit/scripts/lib/source-backlinks.mjs +0 -218
- package/assets/skills/explainer-kit/scripts/lib/terminal-evidence.mjs +0 -157
- package/assets/skills/explainer-kit/scripts/lib/visual-review.mjs +0 -400
- package/assets/skills/explainer-kit/scripts/publish.mjs +0 -76
- package/assets/skills/explainer-kit/scripts/record-durability.mjs +0 -64
- package/assets/skills/explainer-kit/scripts/render-qa.mjs +0 -461
- package/assets/skills/explainer-kit/scripts/run.mjs +0 -3038
- package/assets/skills/explainer-kit/scripts/validate.mjs +0 -59
- package/assets/skills/oat-explainer-kit/references/author-callback.md +0 -53
- package/assets/skills/oat-explainer-kit/references/migration.md +0 -144
- package/assets/skills/oat-explainer-kit/references/visual-review-callback.md +0 -83
- package/assets/skills/oat-explainer-kit/scripts/derive-destination.mjs +0 -91
- package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +0 -531
- package/assets/skills/oat-explainer-kit/scripts/probe-recap-seams.mjs +0 -371
- package/assets/skills/oat-explainer-kit/scripts/run.mjs +0 -905
- package/dist/commands/project/archive/explainer-source-backlinks.d.ts +0 -18
- package/dist/commands/project/archive/explainer-source-backlinks.d.ts.map +0 -1
- package/dist/commands/project/archive/explainer-source-backlinks.js +0 -27
- package/dist/commands/project/archive/explainer-terminal-evidence.d.ts +0 -29
- package/dist/commands/project/archive/explainer-terminal-evidence.d.ts.map +0 -1
- package/dist/commands/project/archive/explainer-terminal-evidence.js +0 -37
|
@@ -1,643 +0,0 @@
|
|
|
1
|
-
import { execFile as execFileCallback } from 'node:child_process';
|
|
2
|
-
import { createHash } from 'node:crypto';
|
|
3
|
-
import { readFile, realpath } from 'node:fs/promises';
|
|
4
|
-
import { dirname, relative, resolve, sep } from 'node:path';
|
|
5
|
-
import { promisify } from 'node:util';
|
|
6
|
-
|
|
7
|
-
import {
|
|
8
|
-
catalogFromManifest,
|
|
9
|
-
initiativeCatalogPath,
|
|
10
|
-
serializeInitiativeCatalog,
|
|
11
|
-
validateInitiativeCatalog,
|
|
12
|
-
} from './catalog.mjs';
|
|
13
|
-
import {
|
|
14
|
-
canonicalHash,
|
|
15
|
-
isVerifiablePublishReceipt,
|
|
16
|
-
validateContract,
|
|
17
|
-
} from './contracts.mjs';
|
|
18
|
-
import { writeFileAtomic, writeJsonAtomic } from './fs-safe.mjs';
|
|
19
|
-
import { validateSafeRelativePath } from './safe-paths.mjs';
|
|
20
|
-
|
|
21
|
-
const execFile = promisify(execFileCallback);
|
|
22
|
-
const MUTABLE_RECORDS = new Set(['manifest.json', 'build-record.json']);
|
|
23
|
-
const FAILURE_WARNING =
|
|
24
|
-
'E_DURABILITY: durability evidence verification failed.';
|
|
25
|
-
|
|
26
|
-
export async function recordDurability(request, options = {}) {
|
|
27
|
-
assertValid('durability-evidence', request);
|
|
28
|
-
|
|
29
|
-
const manifestPath = resolve(request.manifestPath);
|
|
30
|
-
const runRoot = dirname(manifestPath);
|
|
31
|
-
if (manifestPath !== joinWithin(runRoot, 'manifest.json')) {
|
|
32
|
-
throw new Error('Durability manifestPath must identify manifest.json.');
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const manifest = await readJson(manifestPath);
|
|
36
|
-
const buildRecordPath = joinWithin(runRoot, manifest.buildRecord?.path);
|
|
37
|
-
const buildRecord = await readJson(buildRecordPath);
|
|
38
|
-
assertValid('build-record', buildRecord);
|
|
39
|
-
assertValid('manifest', manifest, { buildRecord });
|
|
40
|
-
if (manifest.outcome === 'built-needs-review') {
|
|
41
|
-
throw new Error(
|
|
42
|
-
'built-needs-review requires a passing visual review before durability attestation.',
|
|
43
|
-
);
|
|
44
|
-
}
|
|
45
|
-
if (
|
|
46
|
-
manifest.outcome !== 'built-not-durable' &&
|
|
47
|
-
manifest.outcome !== 'built-durable'
|
|
48
|
-
) {
|
|
49
|
-
throw new Error(
|
|
50
|
-
`Durability can only attest built runs, not ${manifest.outcome}.`,
|
|
51
|
-
);
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
const verification = await verifyEvidence(request.evidence, {
|
|
55
|
-
runRoot,
|
|
56
|
-
manifest,
|
|
57
|
-
options,
|
|
58
|
-
});
|
|
59
|
-
const now = options.now ?? (() => new Date().toISOString());
|
|
60
|
-
|
|
61
|
-
if (!verification.verified) {
|
|
62
|
-
const errors = verification.errors;
|
|
63
|
-
updateOutcomes(manifest, buildRecord, {
|
|
64
|
-
durable: false,
|
|
65
|
-
warning: `${FAILURE_WARNING} ${errors.map(({ message }) => message).join(' ')}`,
|
|
66
|
-
completedAt: now(),
|
|
67
|
-
});
|
|
68
|
-
await persistRecords(runRoot, manifest, buildRecord);
|
|
69
|
-
return {
|
|
70
|
-
durable: false,
|
|
71
|
-
outcome: 'built-not-durable',
|
|
72
|
-
errors,
|
|
73
|
-
manifestPath,
|
|
74
|
-
buildRecordPath,
|
|
75
|
-
};
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
const attestedAt = now();
|
|
79
|
-
const alreadyRecorded =
|
|
80
|
-
manifest.outcome === 'built-durable' &&
|
|
81
|
-
verification.artifacts.every((artifact) =>
|
|
82
|
-
hasEvidence(artifact, verification.evidence),
|
|
83
|
-
) &&
|
|
84
|
-
(!verification.publishReceipt ||
|
|
85
|
-
manifest.publishReceipt?.hash ===
|
|
86
|
-
canonicalHash(verification.publishReceipt));
|
|
87
|
-
if (alreadyRecorded) {
|
|
88
|
-
return {
|
|
89
|
-
durable: true,
|
|
90
|
-
outcome: 'built-durable',
|
|
91
|
-
errors: [],
|
|
92
|
-
manifestPath,
|
|
93
|
-
buildRecordPath,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
for (const artifact of verification.artifacts) {
|
|
98
|
-
appendEvidence(artifact, {
|
|
99
|
-
...verification.evidence,
|
|
100
|
-
attestedAt,
|
|
101
|
-
});
|
|
102
|
-
}
|
|
103
|
-
if (verification.publishReceipt) {
|
|
104
|
-
manifest.publishReceipt = {
|
|
105
|
-
path: 'publish-receipt.json',
|
|
106
|
-
hash: canonicalHash(verification.publishReceipt),
|
|
107
|
-
};
|
|
108
|
-
}
|
|
109
|
-
updateOutcomes(manifest, buildRecord, {
|
|
110
|
-
durable: true,
|
|
111
|
-
completedAt: attestedAt,
|
|
112
|
-
});
|
|
113
|
-
await persistRecords(runRoot, manifest, buildRecord);
|
|
114
|
-
|
|
115
|
-
return {
|
|
116
|
-
durable: true,
|
|
117
|
-
outcome: 'built-durable',
|
|
118
|
-
errors: [],
|
|
119
|
-
manifestPath,
|
|
120
|
-
buildRecordPath,
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
export async function verifyRebuildability(artifact, runRoot) {
|
|
125
|
-
if (artifact?.rebuildable !== true) {
|
|
126
|
-
return {
|
|
127
|
-
verified: false,
|
|
128
|
-
reason: 'Artifact rebuildability defaults to false.',
|
|
129
|
-
};
|
|
130
|
-
}
|
|
131
|
-
if (
|
|
132
|
-
!artifact.rebuild ||
|
|
133
|
-
!Array.isArray(artifact.rebuild.argv) ||
|
|
134
|
-
artifact.rebuild.argv.length === 0
|
|
135
|
-
) {
|
|
136
|
-
return {
|
|
137
|
-
verified: false,
|
|
138
|
-
reason: 'Rebuildable artifacts require deterministic replay metadata.',
|
|
139
|
-
};
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
try {
|
|
143
|
-
const canonicalRunRoot = await realpath(runRoot);
|
|
144
|
-
const cwd = resolve(canonicalRunRoot, artifact.rebuild.cwd);
|
|
145
|
-
assertWithin(canonicalRunRoot, cwd, 'Replay cwd');
|
|
146
|
-
await realpath(cwd);
|
|
147
|
-
|
|
148
|
-
for (const [inputPath, expectedHash] of Object.entries(
|
|
149
|
-
artifact.rebuild.inputHashes ?? {},
|
|
150
|
-
)) {
|
|
151
|
-
const checked = validateSafeRelativePath(inputPath);
|
|
152
|
-
if (!checked.valid) {
|
|
153
|
-
return {
|
|
154
|
-
verified: false,
|
|
155
|
-
reason: `Replay input path is unsafe: ${inputPath}.`,
|
|
156
|
-
};
|
|
157
|
-
}
|
|
158
|
-
const absoluteInput = resolve(canonicalRunRoot, checked.normalizedPath);
|
|
159
|
-
assertWithin(canonicalRunRoot, absoluteInput, 'Replay input');
|
|
160
|
-
if ((await fileHash(absoluteInput)) !== expectedHash) {
|
|
161
|
-
return {
|
|
162
|
-
verified: false,
|
|
163
|
-
reason: `Replay input hash does not match: ${inputPath}.`,
|
|
164
|
-
};
|
|
165
|
-
}
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const outputPath = resolve(canonicalRunRoot, artifact.renderedPath);
|
|
169
|
-
assertWithin(canonicalRunRoot, outputPath, 'Replay output');
|
|
170
|
-
const original = await readFile(outputPath);
|
|
171
|
-
try {
|
|
172
|
-
const [command, ...args] = artifact.rebuild.argv;
|
|
173
|
-
await execFile(command, args, { cwd, maxBuffer: 10 * 1024 * 1024 });
|
|
174
|
-
if ((await fileHash(outputPath)) !== artifact.hash) {
|
|
175
|
-
return {
|
|
176
|
-
verified: false,
|
|
177
|
-
reason: `Deterministic replay hash does not match for ${artifact.renderedPath}.`,
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
} finally {
|
|
181
|
-
await writeFileAtomic(runRoot, artifact.renderedPath, original);
|
|
182
|
-
}
|
|
183
|
-
return { verified: true, reason: null };
|
|
184
|
-
} catch (caught) {
|
|
185
|
-
return {
|
|
186
|
-
verified: false,
|
|
187
|
-
reason: `Deterministic replay failed: ${errorMessage(caught)}`,
|
|
188
|
-
};
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
async function verifyEvidence(evidence, context) {
|
|
193
|
-
const immutableErrors = await verifyImmutablePackage(
|
|
194
|
-
context.runRoot,
|
|
195
|
-
context.manifest,
|
|
196
|
-
);
|
|
197
|
-
if (immutableErrors.length > 0) {
|
|
198
|
-
return { verified: false, errors: immutableErrors };
|
|
199
|
-
}
|
|
200
|
-
const replayErrors = [];
|
|
201
|
-
for (const artifact of context.manifest.artifacts) {
|
|
202
|
-
if (artifact.status === 'built' && artifact.rebuildable === true) {
|
|
203
|
-
const replay = await verifyRebuildability(artifact, context.runRoot);
|
|
204
|
-
if (!replay.verified) {
|
|
205
|
-
replayErrors.push(error('replay-verification', replay.reason));
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
}
|
|
209
|
-
if (replayErrors.length > 0) {
|
|
210
|
-
return { verified: false, errors: replayErrors };
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
return evidence.kind === 'commit'
|
|
214
|
-
? verifyCommitEvidence(evidence, context)
|
|
215
|
-
: verifyPublishEvidence(evidence, context);
|
|
216
|
-
}
|
|
217
|
-
|
|
218
|
-
async function verifyImmutablePackage(runRoot, manifest) {
|
|
219
|
-
const errors = [];
|
|
220
|
-
for (const [path, expectedHash] of Object.entries(manifest.immutableHashes)) {
|
|
221
|
-
try {
|
|
222
|
-
const bytes = await readFile(joinWithin(runRoot, path));
|
|
223
|
-
const actualHash = `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
224
|
-
if (actualHash !== expectedHash) {
|
|
225
|
-
errors.push(
|
|
226
|
-
error('hash-mismatch', `Immutable package hash changed for ${path}.`),
|
|
227
|
-
);
|
|
228
|
-
}
|
|
229
|
-
} catch (caught) {
|
|
230
|
-
errors.push(
|
|
231
|
-
error(
|
|
232
|
-
'missing-path',
|
|
233
|
-
`Immutable package evidence ${path} is unavailable: ${errorMessage(caught)}`,
|
|
234
|
-
),
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
return errors;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
async function verifyCommitEvidence(evidence, { runRoot, manifest }) {
|
|
242
|
-
const errors = [];
|
|
243
|
-
let repoRoot;
|
|
244
|
-
let commit;
|
|
245
|
-
try {
|
|
246
|
-
repoRoot = await realpath(evidence.repoRoot);
|
|
247
|
-
runRoot = await realpath(runRoot);
|
|
248
|
-
assertWithin(repoRoot, runRoot, 'Manifest');
|
|
249
|
-
({ stdout: commit } = await execFile(
|
|
250
|
-
'git',
|
|
251
|
-
['rev-parse', '--verify', `${evidence.commit}^{commit}`],
|
|
252
|
-
{ cwd: repoRoot },
|
|
253
|
-
));
|
|
254
|
-
commit = commit.trim();
|
|
255
|
-
} catch (caught) {
|
|
256
|
-
return {
|
|
257
|
-
verified: false,
|
|
258
|
-
errors: [error('commit-verification', errorMessage(caught))],
|
|
259
|
-
};
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
const manifestRepoPath = toRepoPath(
|
|
263
|
-
repoRoot,
|
|
264
|
-
joinWithin(runRoot, 'manifest.json'),
|
|
265
|
-
);
|
|
266
|
-
const buildRecordRepoPath = toRepoPath(
|
|
267
|
-
repoRoot,
|
|
268
|
-
joinWithin(runRoot, manifest.buildRecord.path),
|
|
269
|
-
);
|
|
270
|
-
const mutablePaths = new Set([manifestRepoPath, buildRecordRepoPath]);
|
|
271
|
-
for (const path of evidence.paths) {
|
|
272
|
-
if (
|
|
273
|
-
MUTABLE_RECORDS.has(path) ||
|
|
274
|
-
mutablePaths.has(path) ||
|
|
275
|
-
path.endsWith('/manifest.json') ||
|
|
276
|
-
path.endsWith('/build-record.json')
|
|
277
|
-
) {
|
|
278
|
-
errors.push(
|
|
279
|
-
error(
|
|
280
|
-
'mutable-record',
|
|
281
|
-
`Commit evidence must exclude mutable record ${path}.`,
|
|
282
|
-
),
|
|
283
|
-
);
|
|
284
|
-
}
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
const required = immutablePackage(manifest);
|
|
288
|
-
const requiredByPath = new Map(
|
|
289
|
-
required.map((file) => [
|
|
290
|
-
toRepoPath(repoRoot, joinWithin(runRoot, file.path)),
|
|
291
|
-
file,
|
|
292
|
-
]),
|
|
293
|
-
);
|
|
294
|
-
for (const [path] of requiredByPath) {
|
|
295
|
-
if (!evidence.paths.includes(path)) {
|
|
296
|
-
errors.push(
|
|
297
|
-
error(
|
|
298
|
-
'missing-artifact',
|
|
299
|
-
`Commit evidence does not include required artifact ${path}.`,
|
|
300
|
-
),
|
|
301
|
-
);
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
|
|
305
|
-
for (const file of required) {
|
|
306
|
-
const localPath = joinWithin(runRoot, file.path);
|
|
307
|
-
if ((await fileHash(localPath)) !== file.hash) {
|
|
308
|
-
errors.push(
|
|
309
|
-
error(
|
|
310
|
-
'hash-mismatch',
|
|
311
|
-
`Retained package hash does not match manifest hash for ${file.path}.`,
|
|
312
|
-
),
|
|
313
|
-
);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
for (const path of evidence.paths) {
|
|
318
|
-
if (mutablePaths.has(path) || MUTABLE_RECORDS.has(path)) {
|
|
319
|
-
continue;
|
|
320
|
-
}
|
|
321
|
-
try {
|
|
322
|
-
const { stdout: blob } = await execFile(
|
|
323
|
-
'git',
|
|
324
|
-
['cat-file', 'blob', `${commit}:${path}`],
|
|
325
|
-
{ cwd: repoRoot, encoding: 'buffer', maxBuffer: 50 * 1024 * 1024 },
|
|
326
|
-
);
|
|
327
|
-
const file = requiredByPath.get(path);
|
|
328
|
-
if (file && bufferHash(blob) !== file.hash) {
|
|
329
|
-
errors.push(
|
|
330
|
-
error(
|
|
331
|
-
'hash-mismatch',
|
|
332
|
-
`Commit blob hash does not match manifest hash for ${path}.`,
|
|
333
|
-
),
|
|
334
|
-
);
|
|
335
|
-
}
|
|
336
|
-
} catch (caught) {
|
|
337
|
-
errors.push(
|
|
338
|
-
error(
|
|
339
|
-
'missing-commit-path',
|
|
340
|
-
`Commit does not contain ${path}: ${errorMessage(caught)}`,
|
|
341
|
-
),
|
|
342
|
-
);
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
return errors.length > 0
|
|
347
|
-
? { verified: false, errors }
|
|
348
|
-
: {
|
|
349
|
-
verified: true,
|
|
350
|
-
errors: [],
|
|
351
|
-
artifacts: requiredArtifacts(manifest),
|
|
352
|
-
evidence: {
|
|
353
|
-
kind: 'commit',
|
|
354
|
-
ref: commit,
|
|
355
|
-
paths: [...evidence.paths],
|
|
356
|
-
},
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
async function verifyPublishEvidence(
|
|
361
|
-
evidence,
|
|
362
|
-
{ runRoot, manifest, options = {} },
|
|
363
|
-
) {
|
|
364
|
-
// Injectable for the rejection-branch test below, exactly as `fetchImpl` is
|
|
365
|
-
// on `defaultHttpGet`: the branch fires only when the builder and validator
|
|
366
|
-
// disagree, which correct production code never does on purpose.
|
|
367
|
-
const buildCatalog = options.buildCatalog ?? catalogFromManifest;
|
|
368
|
-
const expectedPath = joinWithin(runRoot, 'publish-receipt.json');
|
|
369
|
-
if (resolve(evidence.receiptPath) !== expectedPath) {
|
|
370
|
-
return {
|
|
371
|
-
verified: false,
|
|
372
|
-
errors: [
|
|
373
|
-
error(
|
|
374
|
-
'receipt-location',
|
|
375
|
-
'Publish receipt must be publish-receipt.json beside the manifest.',
|
|
376
|
-
),
|
|
377
|
-
],
|
|
378
|
-
};
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
let receipt;
|
|
382
|
-
try {
|
|
383
|
-
receipt = await readJson(expectedPath);
|
|
384
|
-
} catch (caught) {
|
|
385
|
-
return {
|
|
386
|
-
verified: false,
|
|
387
|
-
errors: [error('publish-receipt', errorMessage(caught))],
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
let catalogArtifact;
|
|
391
|
-
if (isVerifiablePublishReceipt(receipt)) {
|
|
392
|
-
try {
|
|
393
|
-
// Must rebuild the catalog under the policy the receipt declares. The
|
|
394
|
-
// connector embedded that policy in the catalog it hashed, so verifying a
|
|
395
|
-
// `protected` receipt against a `public`-shaped rebuild yields a
|
|
396
|
-
// different hash and rejects every protected publication as
|
|
397
|
-
// `cross-record-mismatch`.
|
|
398
|
-
const catalog = buildCatalog(manifest, receipt.roots?.publicBaseUrl, {
|
|
399
|
-
publicAccess: receipt.publicAccess,
|
|
400
|
-
});
|
|
401
|
-
// This is a builder/validator cross-check on a catalog rebuilt three
|
|
402
|
-
// lines above from the same inputs — durability never consumes catalog
|
|
403
|
-
// bytes from outside, only a recorded hash, so no externally-sourced
|
|
404
|
-
// catalog object exists here to validate. The value of the check is that
|
|
405
|
-
// `catalogFromManifest` and `validateInitiativeCatalog` are independent
|
|
406
|
-
// implementations of the same contract: if they ever diverge, this
|
|
407
|
-
// reports the specific contract violation instead of letting the
|
|
408
|
-
// divergence surface as an opaque `cross-record-mismatch`.
|
|
409
|
-
const catalogValidation = validateInitiativeCatalog(
|
|
410
|
-
catalog,
|
|
411
|
-
manifest,
|
|
412
|
-
receipt.roots?.publicBaseUrl,
|
|
413
|
-
{ publicAccess: receipt.publicAccess },
|
|
414
|
-
);
|
|
415
|
-
if (!catalogValidation.valid) {
|
|
416
|
-
return {
|
|
417
|
-
verified: false,
|
|
418
|
-
errors: [
|
|
419
|
-
error(
|
|
420
|
-
'publish-receipt',
|
|
421
|
-
`Rebuilt initiative catalog is invalid: ${catalogValidation.errors[0].message}`,
|
|
422
|
-
),
|
|
423
|
-
],
|
|
424
|
-
};
|
|
425
|
-
}
|
|
426
|
-
catalogArtifact = {
|
|
427
|
-
relativePath: initiativeCatalogPath(manifest.slug),
|
|
428
|
-
hash: bufferHash(Buffer.from(serializeInitiativeCatalog(catalog))),
|
|
429
|
-
};
|
|
430
|
-
} catch (caught) {
|
|
431
|
-
return {
|
|
432
|
-
verified: false,
|
|
433
|
-
errors: [error('publish-receipt', errorMessage(caught))],
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
const validation = validateContract('publish-receipt', receipt, {
|
|
438
|
-
manifest,
|
|
439
|
-
...(catalogArtifact && { catalogArtifact }),
|
|
440
|
-
});
|
|
441
|
-
const errors = validation.errors.map(({ code, message }) =>
|
|
442
|
-
error(code, message),
|
|
443
|
-
);
|
|
444
|
-
if (!sentinelVerificationComplete(receipt)) {
|
|
445
|
-
errors.push(
|
|
446
|
-
error(
|
|
447
|
-
'sentinel-verification',
|
|
448
|
-
'Publish receipt sentinel verification is incomplete.',
|
|
449
|
-
),
|
|
450
|
-
);
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
const receiptByPath = new Map(
|
|
454
|
-
(receipt.artifacts ?? []).map((artifact) => [
|
|
455
|
-
artifact.relativePath,
|
|
456
|
-
artifact,
|
|
457
|
-
]),
|
|
458
|
-
);
|
|
459
|
-
const required = requiredArtifacts(manifest);
|
|
460
|
-
for (const artifact of required) {
|
|
461
|
-
const published = receiptByPath.get(artifact.renderedPath);
|
|
462
|
-
if (!published || published.hash !== artifact.hash) {
|
|
463
|
-
errors.push(
|
|
464
|
-
error(
|
|
465
|
-
'missing-artifact',
|
|
466
|
-
`Publish receipt does not verify ${artifact.renderedPath}.`,
|
|
467
|
-
),
|
|
468
|
-
);
|
|
469
|
-
}
|
|
470
|
-
}
|
|
471
|
-
|
|
472
|
-
return errors.length > 0
|
|
473
|
-
? { verified: false, errors }
|
|
474
|
-
: {
|
|
475
|
-
verified: true,
|
|
476
|
-
errors: [],
|
|
477
|
-
artifacts: required,
|
|
478
|
-
publishReceipt: receipt,
|
|
479
|
-
evidence: {
|
|
480
|
-
kind: 'publish',
|
|
481
|
-
ref: canonicalHash(receipt),
|
|
482
|
-
paths: required.map(({ renderedPath }) => renderedPath),
|
|
483
|
-
},
|
|
484
|
-
};
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
function sentinelVerificationComplete(receipt) {
|
|
488
|
-
if (receipt.sentinel?.deleted !== true) return false;
|
|
489
|
-
if (receipt.schemaVersion === 'explainer-kit.publish-receipt/v1') {
|
|
490
|
-
return (
|
|
491
|
-
receipt.sentinel.uploadVerified === true &&
|
|
492
|
-
receipt.sentinel.publicVerified === true
|
|
493
|
-
);
|
|
494
|
-
}
|
|
495
|
-
if (receipt.schemaVersion !== 'explainer-kit.publish-receipt/v2') {
|
|
496
|
-
return false;
|
|
497
|
-
}
|
|
498
|
-
return (
|
|
499
|
-
receipt.sentinel.objectVerification?.status === 'verified' &&
|
|
500
|
-
(receipt.publicAccess === 'public'
|
|
501
|
-
? receipt.sentinel.publicVerification?.status === 'verified'
|
|
502
|
-
: receipt.sentinel.publicVerification?.status === 'skipped-protected')
|
|
503
|
-
);
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
function appendEvidence(artifact, evidence) {
|
|
507
|
-
artifact.durableEvidence ??= [];
|
|
508
|
-
if (hasEvidence(artifact, evidence)) {
|
|
509
|
-
return;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
const prior = artifact.durableEvidence.findLast(
|
|
513
|
-
(existing) => existing.kind === evidence.kind,
|
|
514
|
-
);
|
|
515
|
-
const next = structuredClone(evidence);
|
|
516
|
-
if (
|
|
517
|
-
prior &&
|
|
518
|
-
(prior.ref !== next.ref || !arraysEqual(prior.paths, next.paths))
|
|
519
|
-
) {
|
|
520
|
-
next.supersedes = { ref: prior.ref, paths: [...prior.paths] };
|
|
521
|
-
}
|
|
522
|
-
artifact.durableEvidence.push(next);
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
function hasEvidence(artifact, evidence) {
|
|
526
|
-
return (artifact.durableEvidence ?? []).some(
|
|
527
|
-
(existing) =>
|
|
528
|
-
existing.kind === evidence.kind &&
|
|
529
|
-
existing.ref === evidence.ref &&
|
|
530
|
-
arraysEqual(existing.paths, evidence.paths),
|
|
531
|
-
);
|
|
532
|
-
}
|
|
533
|
-
|
|
534
|
-
function updateOutcomes(
|
|
535
|
-
manifest,
|
|
536
|
-
buildRecord,
|
|
537
|
-
{ durable, warning, completedAt = new Date().toISOString() },
|
|
538
|
-
) {
|
|
539
|
-
const outcome = durable ? 'built-durable' : 'built-not-durable';
|
|
540
|
-
manifest.outcome = outcome;
|
|
541
|
-
buildRecord.outcome = outcome;
|
|
542
|
-
|
|
543
|
-
manifest.warnings = manifest.warnings.filter(
|
|
544
|
-
(entry) => !entry.startsWith('E_DURABILITY:'),
|
|
545
|
-
);
|
|
546
|
-
const stage = buildRecord.stages.find(({ id }) => id === 'durability');
|
|
547
|
-
if (!stage) {
|
|
548
|
-
throw new Error('Build record does not contain a durability stage.');
|
|
549
|
-
}
|
|
550
|
-
stage.status = durable ? 'passed' : 'warned';
|
|
551
|
-
stage.completedAt = completedAt;
|
|
552
|
-
stage.outputPaths = durable ? ['manifest.json', 'build-record.json'] : [];
|
|
553
|
-
stage.warnings = warning ? [warning] : [];
|
|
554
|
-
if (warning) {
|
|
555
|
-
manifest.warnings.push(warning);
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
async function persistRecords(runRoot, manifest, buildRecord) {
|
|
560
|
-
assertValid('build-record', buildRecord);
|
|
561
|
-
manifest.buildRecord.hash = canonicalHash(buildRecord);
|
|
562
|
-
assertValid('manifest', manifest, { buildRecord });
|
|
563
|
-
await writeJsonAtomic(runRoot, 'build-record.json', buildRecord);
|
|
564
|
-
await writeJsonAtomic(runRoot, 'manifest.json', manifest);
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
function requiredArtifacts(manifest) {
|
|
568
|
-
return manifest.artifacts.filter(
|
|
569
|
-
(artifact) =>
|
|
570
|
-
artifact.status === 'built' &&
|
|
571
|
-
artifact.rebuildable === false &&
|
|
572
|
-
typeof artifact.renderedPath === 'string',
|
|
573
|
-
);
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
function immutablePackage(manifest) {
|
|
577
|
-
return Object.entries(manifest.immutableHashes).map(([path, hash]) => ({
|
|
578
|
-
path,
|
|
579
|
-
hash,
|
|
580
|
-
}));
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
async function readJson(path) {
|
|
584
|
-
return JSON.parse(await readFile(path, 'utf8'));
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
function joinWithin(root, relativePath) {
|
|
588
|
-
const checked = validateSafeRelativePath(relativePath);
|
|
589
|
-
if (!checked.valid) {
|
|
590
|
-
throw new Error(
|
|
591
|
-
`Unsafe durability record path: ${checked.errors[0].message}`,
|
|
592
|
-
);
|
|
593
|
-
}
|
|
594
|
-
const path = resolve(root, checked.normalizedPath);
|
|
595
|
-
assertWithin(root, path, 'Durability record');
|
|
596
|
-
return path;
|
|
597
|
-
}
|
|
598
|
-
|
|
599
|
-
function assertWithin(root, target, label) {
|
|
600
|
-
const path = relative(root, target);
|
|
601
|
-
if (path === '..' || path.startsWith(`..${sep}`)) {
|
|
602
|
-
throw new Error(`${label} escapes its configured root.`);
|
|
603
|
-
}
|
|
604
|
-
}
|
|
605
|
-
|
|
606
|
-
function toRepoPath(repoRoot, absolutePath) {
|
|
607
|
-
assertWithin(repoRoot, absolutePath, 'Evidence path');
|
|
608
|
-
return relative(repoRoot, absolutePath).split(sep).join('/');
|
|
609
|
-
}
|
|
610
|
-
|
|
611
|
-
async function fileHash(path) {
|
|
612
|
-
return bufferHash(await readFile(path));
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
function bufferHash(value) {
|
|
616
|
-
return `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
617
|
-
}
|
|
618
|
-
|
|
619
|
-
function assertValid(kind, value, context) {
|
|
620
|
-
const result = validateContract(kind, value, context);
|
|
621
|
-
if (!result.valid) {
|
|
622
|
-
throw new Error(
|
|
623
|
-
`Invalid ${kind}: ${result.errors
|
|
624
|
-
.map(({ path, code, message }) => `${path} [${code}]: ${message}`)
|
|
625
|
-
.join('; ')}`,
|
|
626
|
-
);
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
function arraysEqual(left, right) {
|
|
631
|
-
return (
|
|
632
|
-
left.length === right.length &&
|
|
633
|
-
left.every((value, index) => value === right[index])
|
|
634
|
-
);
|
|
635
|
-
}
|
|
636
|
-
|
|
637
|
-
function error(code, message) {
|
|
638
|
-
return { code, message };
|
|
639
|
-
}
|
|
640
|
-
|
|
641
|
-
function errorMessage(value) {
|
|
642
|
-
return value instanceof Error ? value.message : String(value);
|
|
643
|
-
}
|