@open-agent-toolkit/cli 0.2.4 → 0.2.6
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/docs/cli-utilities/configuration.md +40 -0
- package/assets/docs/cli-utilities/tool-packs.md +31 -0
- package/assets/docs/workflows/projects/artifacts.md +27 -0
- package/assets/docs/workflows/projects/implementation-execution.md +30 -5
- package/assets/docs/workflows/skills/explainer-kit.md +125 -0
- package/assets/docs/workflows/skills/index.md +4 -0
- package/assets/migration/pjm-restructure.md +12 -0
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/explainer-kit/SKILL.md +95 -0
- package/assets/skills/explainer-kit/examples/project-explainer/content.md +31 -0
- package/assets/skills/explainer-kit/examples/project-explainer/fact-base.md +22 -0
- package/assets/skills/explainer-kit/examples/project-recap/content.md +34 -0
- package/assets/skills/explainer-kit/examples/project-recap/fact-base.md +22 -0
- package/assets/skills/explainer-kit/examples/theme-bundle.json +87 -0
- package/assets/skills/explainer-kit/palettes/ember.json +37 -0
- package/assets/skills/explainer-kit/palettes/forest.json +37 -0
- package/assets/skills/explainer-kit/palettes/neutral.json +37 -0
- package/assets/skills/explainer-kit/palettes/ocean.json +37 -0
- package/assets/skills/explainer-kit/palettes/violet.json +37 -0
- package/assets/skills/explainer-kit/profiles/clean.json +42 -0
- package/assets/skills/explainer-kit/profiles/editorial.json +42 -0
- package/assets/skills/explainer-kit/profiles/technical.json +39 -0
- package/assets/skills/explainer-kit/recipes/engineer-tour.json +33 -0
- package/assets/skills/explainer-kit/recipes/program-recap.json +34 -0
- package/assets/skills/explainer-kit/recipes/project-explainer.json +33 -0
- package/assets/skills/explainer-kit/recipes/project-recap.json +34 -0
- package/assets/skills/explainer-kit/references/contracts.md +101 -0
- package/assets/skills/explainer-kit/references/destination-contract.md +94 -0
- package/assets/skills/explainer-kit/references/extension-contract.md +71 -0
- package/assets/skills/explainer-kit/references/fact-base-contract.md +119 -0
- package/assets/skills/explainer-kit/schemas/build-record.schema.json +92 -0
- package/assets/skills/explainer-kit/schemas/durability-evidence.schema.json +53 -0
- package/assets/skills/explainer-kit/schemas/fact-base.schema.json +129 -0
- package/assets/skills/explainer-kit/schemas/manifest.schema.json +193 -0
- package/assets/skills/explainer-kit/schemas/publish-receipt.schema.json +97 -0
- package/assets/skills/explainer-kit/schemas/publish-request.schema.json +32 -0
- package/assets/skills/explainer-kit/schemas/run-request.schema.json +132 -0
- package/assets/skills/explainer-kit/schemas/theme.schema.json +217 -0
- package/assets/skills/explainer-kit/scripts/lib/content-approval.mjs +180 -0
- package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +680 -0
- package/assets/skills/explainer-kit/scripts/lib/durability.mjs +538 -0
- package/assets/skills/explainer-kit/scripts/lib/fact-base.mjs +494 -0
- package/assets/skills/explainer-kit/scripts/lib/fs-safe.mjs +218 -0
- package/assets/skills/explainer-kit/scripts/lib/qa.mjs +512 -0
- package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +289 -0
- package/assets/skills/explainer-kit/scripts/lib/records.mjs +231 -0
- package/assets/skills/explainer-kit/scripts/lib/render.mjs +485 -0
- package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +587 -0
- package/assets/skills/explainer-kit/scripts/lib/safe-paths.mjs +96 -0
- package/assets/skills/explainer-kit/scripts/lib/theme.mjs +332 -0
- package/assets/skills/explainer-kit/scripts/publish.mjs +76 -0
- package/assets/skills/explainer-kit/scripts/record-durability.mjs +64 -0
- package/assets/skills/explainer-kit/scripts/render-qa.mjs +273 -0
- package/assets/skills/explainer-kit/scripts/run.mjs +778 -0
- package/assets/skills/explainer-kit/scripts/validate.mjs +59 -0
- package/assets/skills/explainer-kit/templates/deck-shell.html +233 -0
- package/assets/skills/explainer-kit/templates/diagram-shell.html +219 -0
- package/assets/skills/explainer-kit/templates/engineer-tour.html +273 -0
- package/assets/skills/explainer-kit/templates/house-style.html +218 -0
- package/assets/skills/oat-explainer-kit/SKILL.md +92 -0
- package/assets/skills/oat-explainer-kit/references/config-contract.md +67 -0
- package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +143 -0
- package/assets/skills/oat-explainer-kit/references/migration.md +143 -0
- package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +103 -0
- package/assets/skills/oat-explainer-kit/scripts/check-core.mjs +127 -0
- package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +392 -0
- package/assets/skills/oat-explainer-kit/scripts/persist-intent.mjs +130 -0
- package/assets/skills/oat-explainer-kit/scripts/resolve-config.mjs +293 -0
- package/assets/skills/oat-explainer-kit/scripts/resolve-intent.mjs +209 -0
- package/assets/skills/oat-explainer-kit/scripts/resolve-paths.mjs +166 -0
- package/assets/skills/oat-explainer-kit/scripts/run.mjs +284 -0
- package/assets/skills/oat-project-autonomous/SKILL.md +22 -1
- package/assets/skills/oat-project-autonomous/references/gate-inventory.md +5 -1
- package/assets/skills/oat-project-complete/SKILL.md +166 -28
- package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +5 -1
- package/assets/skills/oat-project-implement/SKILL.md +1 -1
- package/assets/skills/oat-project-implement/references/completion-and-closeout.md +84 -9
- package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +5 -1
- package/assets/skills/oat-project-next/SKILL.md +31 -11
- package/assets/skills/oat-project-plan/SKILL.md +30 -1
- package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +5 -1
- package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +5 -1
- package/assets/skills/oat-project-summary/SKILL.md +20 -1
- package/assets/templates/state.md +4 -1
- package/dist/commands/config/index.d.ts.map +1 -1
- package/dist/commands/config/index.js +232 -1
- package/dist/commands/init/tools/shared/skill-manifest.d.ts +2 -2
- package/dist/commands/init/tools/shared/skill-manifest.d.ts.map +1 -1
- package/dist/commands/init/tools/shared/skill-manifest.js +2 -0
- package/dist/commands/project/archive/archive-utils.d.ts +12 -0
- package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
- package/dist/commands/project/archive/archive-utils.js +320 -8
- package/dist/commands/project/archive/index.d.ts.map +1 -1
- package/dist/commands/project/archive/index.js +1 -0
- package/dist/commands/project/archive/push-runner.d.ts +1 -0
- package/dist/commands/project/archive/push-runner.d.ts.map +1 -1
- package/dist/commands/project/archive/push-runner.js +9 -2
- package/dist/config/oat-config.d.ts +26 -0
- package/dist/config/oat-config.d.ts.map +1 -1
- package/dist/config/oat-config.js +88 -0
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +18 -0
- package/dist/validation/project-state.d.ts +3 -0
- package/dist/validation/project-state.d.ts.map +1 -1
- package/dist/validation/project-state.js +91 -0
- package/package.json +2 -2
|
@@ -0,0 +1,538 @@
|
|
|
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 { canonicalHash, validateContract } from './contracts.mjs';
|
|
8
|
+
import { writeFileAtomic, writeJsonAtomic } from './fs-safe.mjs';
|
|
9
|
+
import { validateSafeRelativePath } from './safe-paths.mjs';
|
|
10
|
+
|
|
11
|
+
const execFile = promisify(execFileCallback);
|
|
12
|
+
const MUTABLE_RECORDS = new Set(['manifest.json', 'build-record.json']);
|
|
13
|
+
const FAILURE_WARNING =
|
|
14
|
+
'E_DURABILITY: durability evidence verification failed.';
|
|
15
|
+
|
|
16
|
+
export async function recordDurability(request, options = {}) {
|
|
17
|
+
assertValid('durability-evidence', request);
|
|
18
|
+
|
|
19
|
+
const manifestPath = resolve(request.manifestPath);
|
|
20
|
+
const runRoot = dirname(manifestPath);
|
|
21
|
+
if (manifestPath !== joinWithin(runRoot, 'manifest.json')) {
|
|
22
|
+
throw new Error('Durability manifestPath must identify manifest.json.');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const manifest = await readJson(manifestPath);
|
|
26
|
+
const buildRecordPath = joinWithin(runRoot, manifest.buildRecord?.path);
|
|
27
|
+
const buildRecord = await readJson(buildRecordPath);
|
|
28
|
+
assertValid('build-record', buildRecord);
|
|
29
|
+
assertValid('manifest', manifest, { buildRecord });
|
|
30
|
+
if (
|
|
31
|
+
manifest.outcome !== 'built-not-durable' &&
|
|
32
|
+
manifest.outcome !== 'built-durable'
|
|
33
|
+
) {
|
|
34
|
+
throw new Error(
|
|
35
|
+
`Durability can only attest built runs, not ${manifest.outcome}.`,
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const verification = await verifyEvidence(request.evidence, {
|
|
40
|
+
runRoot,
|
|
41
|
+
manifest,
|
|
42
|
+
options,
|
|
43
|
+
});
|
|
44
|
+
const now = options.now ?? (() => new Date().toISOString());
|
|
45
|
+
|
|
46
|
+
if (!verification.verified) {
|
|
47
|
+
const errors = verification.errors;
|
|
48
|
+
updateOutcomes(manifest, buildRecord, {
|
|
49
|
+
durable: false,
|
|
50
|
+
warning: `${FAILURE_WARNING} ${errors.map(({ message }) => message).join(' ')}`,
|
|
51
|
+
completedAt: now(),
|
|
52
|
+
});
|
|
53
|
+
await persistRecords(runRoot, manifest, buildRecord);
|
|
54
|
+
return {
|
|
55
|
+
durable: false,
|
|
56
|
+
outcome: 'built-not-durable',
|
|
57
|
+
errors,
|
|
58
|
+
manifestPath,
|
|
59
|
+
buildRecordPath,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const attestedAt = now();
|
|
64
|
+
const alreadyRecorded =
|
|
65
|
+
manifest.outcome === 'built-durable' &&
|
|
66
|
+
verification.artifacts.every((artifact) =>
|
|
67
|
+
hasEvidence(artifact, verification.evidence),
|
|
68
|
+
) &&
|
|
69
|
+
(!verification.publishReceipt ||
|
|
70
|
+
manifest.publishReceipt?.hash ===
|
|
71
|
+
canonicalHash(verification.publishReceipt));
|
|
72
|
+
if (alreadyRecorded) {
|
|
73
|
+
return {
|
|
74
|
+
durable: true,
|
|
75
|
+
outcome: 'built-durable',
|
|
76
|
+
errors: [],
|
|
77
|
+
manifestPath,
|
|
78
|
+
buildRecordPath,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
for (const artifact of verification.artifacts) {
|
|
83
|
+
appendEvidence(artifact, {
|
|
84
|
+
...verification.evidence,
|
|
85
|
+
attestedAt,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
if (verification.publishReceipt) {
|
|
89
|
+
manifest.publishReceipt = {
|
|
90
|
+
path: 'publish-receipt.json',
|
|
91
|
+
hash: canonicalHash(verification.publishReceipt),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
updateOutcomes(manifest, buildRecord, {
|
|
95
|
+
durable: true,
|
|
96
|
+
completedAt: attestedAt,
|
|
97
|
+
});
|
|
98
|
+
await persistRecords(runRoot, manifest, buildRecord);
|
|
99
|
+
|
|
100
|
+
return {
|
|
101
|
+
durable: true,
|
|
102
|
+
outcome: 'built-durable',
|
|
103
|
+
errors: [],
|
|
104
|
+
manifestPath,
|
|
105
|
+
buildRecordPath,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export async function verifyRebuildability(artifact, runRoot) {
|
|
110
|
+
if (artifact?.rebuildable !== true) {
|
|
111
|
+
return {
|
|
112
|
+
verified: false,
|
|
113
|
+
reason: 'Artifact rebuildability defaults to false.',
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
if (
|
|
117
|
+
!artifact.rebuild ||
|
|
118
|
+
!Array.isArray(artifact.rebuild.argv) ||
|
|
119
|
+
artifact.rebuild.argv.length === 0
|
|
120
|
+
) {
|
|
121
|
+
return {
|
|
122
|
+
verified: false,
|
|
123
|
+
reason: 'Rebuildable artifacts require deterministic replay metadata.',
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const canonicalRunRoot = await realpath(runRoot);
|
|
129
|
+
const cwd = resolve(canonicalRunRoot, artifact.rebuild.cwd);
|
|
130
|
+
assertWithin(canonicalRunRoot, cwd, 'Replay cwd');
|
|
131
|
+
await realpath(cwd);
|
|
132
|
+
|
|
133
|
+
for (const [inputPath, expectedHash] of Object.entries(
|
|
134
|
+
artifact.rebuild.inputHashes ?? {},
|
|
135
|
+
)) {
|
|
136
|
+
const checked = validateSafeRelativePath(inputPath);
|
|
137
|
+
if (!checked.valid) {
|
|
138
|
+
return {
|
|
139
|
+
verified: false,
|
|
140
|
+
reason: `Replay input path is unsafe: ${inputPath}.`,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const absoluteInput = resolve(canonicalRunRoot, checked.normalizedPath);
|
|
144
|
+
assertWithin(canonicalRunRoot, absoluteInput, 'Replay input');
|
|
145
|
+
if ((await fileHash(absoluteInput)) !== expectedHash) {
|
|
146
|
+
return {
|
|
147
|
+
verified: false,
|
|
148
|
+
reason: `Replay input hash does not match: ${inputPath}.`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const outputPath = resolve(canonicalRunRoot, artifact.renderedPath);
|
|
154
|
+
assertWithin(canonicalRunRoot, outputPath, 'Replay output');
|
|
155
|
+
const original = await readFile(outputPath);
|
|
156
|
+
try {
|
|
157
|
+
const [command, ...args] = artifact.rebuild.argv;
|
|
158
|
+
await execFile(command, args, { cwd, maxBuffer: 10 * 1024 * 1024 });
|
|
159
|
+
if ((await fileHash(outputPath)) !== artifact.hash) {
|
|
160
|
+
return {
|
|
161
|
+
verified: false,
|
|
162
|
+
reason: `Deterministic replay hash does not match for ${artifact.renderedPath}.`,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
} finally {
|
|
166
|
+
await writeFileAtomic(runRoot, artifact.renderedPath, original);
|
|
167
|
+
}
|
|
168
|
+
return { verified: true, reason: null };
|
|
169
|
+
} catch (error) {
|
|
170
|
+
return {
|
|
171
|
+
verified: false,
|
|
172
|
+
reason: `Deterministic replay failed: ${errorMessage(error)}`,
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
async function verifyEvidence(evidence, context) {
|
|
178
|
+
const replayErrors = [];
|
|
179
|
+
for (const artifact of context.manifest.artifacts) {
|
|
180
|
+
if (artifact.status === 'built' && artifact.rebuildable === true) {
|
|
181
|
+
const replay = await verifyRebuildability(artifact, context.runRoot);
|
|
182
|
+
if (!replay.verified) {
|
|
183
|
+
replayErrors.push(error('replay-verification', replay.reason));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
if (replayErrors.length > 0) {
|
|
188
|
+
return { verified: false, errors: replayErrors };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return evidence.kind === 'commit'
|
|
192
|
+
? verifyCommitEvidence(evidence, context)
|
|
193
|
+
: verifyPublishEvidence(evidence, context);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
async function verifyCommitEvidence(evidence, { runRoot, manifest }) {
|
|
197
|
+
const errors = [];
|
|
198
|
+
let repoRoot;
|
|
199
|
+
let commit;
|
|
200
|
+
try {
|
|
201
|
+
repoRoot = await realpath(evidence.repoRoot);
|
|
202
|
+
runRoot = await realpath(runRoot);
|
|
203
|
+
assertWithin(repoRoot, runRoot, 'Manifest');
|
|
204
|
+
({ stdout: commit } = await execFile(
|
|
205
|
+
'git',
|
|
206
|
+
['rev-parse', '--verify', `${evidence.commit}^{commit}`],
|
|
207
|
+
{ cwd: repoRoot },
|
|
208
|
+
));
|
|
209
|
+
commit = commit.trim();
|
|
210
|
+
} catch (caught) {
|
|
211
|
+
return {
|
|
212
|
+
verified: false,
|
|
213
|
+
errors: [error('commit-verification', errorMessage(caught))],
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const manifestRepoPath = toRepoPath(
|
|
218
|
+
repoRoot,
|
|
219
|
+
joinWithin(runRoot, 'manifest.json'),
|
|
220
|
+
);
|
|
221
|
+
const buildRecordRepoPath = toRepoPath(
|
|
222
|
+
repoRoot,
|
|
223
|
+
joinWithin(runRoot, manifest.buildRecord.path),
|
|
224
|
+
);
|
|
225
|
+
const mutablePaths = new Set([manifestRepoPath, buildRecordRepoPath]);
|
|
226
|
+
for (const path of evidence.paths) {
|
|
227
|
+
if (
|
|
228
|
+
MUTABLE_RECORDS.has(path) ||
|
|
229
|
+
mutablePaths.has(path) ||
|
|
230
|
+
path.endsWith('/manifest.json') ||
|
|
231
|
+
path.endsWith('/build-record.json')
|
|
232
|
+
) {
|
|
233
|
+
errors.push(
|
|
234
|
+
error(
|
|
235
|
+
'mutable-record',
|
|
236
|
+
`Commit evidence must exclude mutable record ${path}.`,
|
|
237
|
+
),
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const required = immutablePackage(manifest);
|
|
243
|
+
const requiredByPath = new Map(
|
|
244
|
+
required.map((file) => [
|
|
245
|
+
toRepoPath(repoRoot, joinWithin(runRoot, file.path)),
|
|
246
|
+
file,
|
|
247
|
+
]),
|
|
248
|
+
);
|
|
249
|
+
for (const [path] of requiredByPath) {
|
|
250
|
+
if (!evidence.paths.includes(path)) {
|
|
251
|
+
errors.push(
|
|
252
|
+
error(
|
|
253
|
+
'missing-artifact',
|
|
254
|
+
`Commit evidence does not include required artifact ${path}.`,
|
|
255
|
+
),
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
for (const file of required) {
|
|
261
|
+
const localPath = joinWithin(runRoot, file.path);
|
|
262
|
+
if ((await fileHash(localPath)) !== file.hash) {
|
|
263
|
+
errors.push(
|
|
264
|
+
error(
|
|
265
|
+
'hash-mismatch',
|
|
266
|
+
`Retained package hash does not match manifest hash for ${file.path}.`,
|
|
267
|
+
),
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
for (const path of evidence.paths) {
|
|
273
|
+
if (mutablePaths.has(path) || MUTABLE_RECORDS.has(path)) {
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
try {
|
|
277
|
+
const { stdout: blob } = await execFile(
|
|
278
|
+
'git',
|
|
279
|
+
['cat-file', 'blob', `${commit}:${path}`],
|
|
280
|
+
{ cwd: repoRoot, encoding: 'buffer', maxBuffer: 50 * 1024 * 1024 },
|
|
281
|
+
);
|
|
282
|
+
const file = requiredByPath.get(path);
|
|
283
|
+
if (file && bufferHash(blob) !== file.hash) {
|
|
284
|
+
errors.push(
|
|
285
|
+
error(
|
|
286
|
+
'hash-mismatch',
|
|
287
|
+
`Commit blob hash does not match manifest hash for ${path}.`,
|
|
288
|
+
),
|
|
289
|
+
);
|
|
290
|
+
}
|
|
291
|
+
} catch (caught) {
|
|
292
|
+
errors.push(
|
|
293
|
+
error(
|
|
294
|
+
'missing-commit-path',
|
|
295
|
+
`Commit does not contain ${path}: ${errorMessage(caught)}`,
|
|
296
|
+
),
|
|
297
|
+
);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
return errors.length > 0
|
|
302
|
+
? { verified: false, errors }
|
|
303
|
+
: {
|
|
304
|
+
verified: true,
|
|
305
|
+
errors: [],
|
|
306
|
+
artifacts: requiredArtifacts(manifest),
|
|
307
|
+
evidence: {
|
|
308
|
+
kind: 'commit',
|
|
309
|
+
ref: commit,
|
|
310
|
+
paths: [...evidence.paths],
|
|
311
|
+
},
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
async function verifyPublishEvidence(evidence, { runRoot, manifest }) {
|
|
316
|
+
const expectedPath = joinWithin(runRoot, 'publish-receipt.json');
|
|
317
|
+
if (resolve(evidence.receiptPath) !== expectedPath) {
|
|
318
|
+
return {
|
|
319
|
+
verified: false,
|
|
320
|
+
errors: [
|
|
321
|
+
error(
|
|
322
|
+
'receipt-location',
|
|
323
|
+
'Publish receipt must be publish-receipt.json beside the manifest.',
|
|
324
|
+
),
|
|
325
|
+
],
|
|
326
|
+
};
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
let receipt;
|
|
330
|
+
try {
|
|
331
|
+
receipt = await readJson(expectedPath);
|
|
332
|
+
} catch (caught) {
|
|
333
|
+
return {
|
|
334
|
+
verified: false,
|
|
335
|
+
errors: [error('publish-receipt', errorMessage(caught))],
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
const validation = validateContract('publish-receipt', receipt, { manifest });
|
|
339
|
+
const errors = validation.errors.map(({ code, message }) =>
|
|
340
|
+
error(code, message),
|
|
341
|
+
);
|
|
342
|
+
if (
|
|
343
|
+
receipt.sentinel?.uploadVerified !== true ||
|
|
344
|
+
receipt.sentinel?.publicVerified !== true ||
|
|
345
|
+
receipt.sentinel?.deleted !== true
|
|
346
|
+
) {
|
|
347
|
+
errors.push(
|
|
348
|
+
error(
|
|
349
|
+
'sentinel-verification',
|
|
350
|
+
'Publish receipt sentinel verification is incomplete.',
|
|
351
|
+
),
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
const receiptByPath = new Map(
|
|
356
|
+
(receipt.artifacts ?? []).map((artifact) => [
|
|
357
|
+
artifact.relativePath,
|
|
358
|
+
artifact,
|
|
359
|
+
]),
|
|
360
|
+
);
|
|
361
|
+
const required = requiredArtifacts(manifest);
|
|
362
|
+
for (const artifact of required) {
|
|
363
|
+
const published = receiptByPath.get(artifact.renderedPath);
|
|
364
|
+
if (!published || published.hash !== artifact.hash) {
|
|
365
|
+
errors.push(
|
|
366
|
+
error(
|
|
367
|
+
'missing-artifact',
|
|
368
|
+
`Publish receipt does not verify ${artifact.renderedPath}.`,
|
|
369
|
+
),
|
|
370
|
+
);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return errors.length > 0
|
|
375
|
+
? { verified: false, errors }
|
|
376
|
+
: {
|
|
377
|
+
verified: true,
|
|
378
|
+
errors: [],
|
|
379
|
+
artifacts: required,
|
|
380
|
+
publishReceipt: receipt,
|
|
381
|
+
evidence: {
|
|
382
|
+
kind: 'publish',
|
|
383
|
+
ref: canonicalHash(receipt),
|
|
384
|
+
paths: required.map(({ renderedPath }) => renderedPath),
|
|
385
|
+
},
|
|
386
|
+
};
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function appendEvidence(artifact, evidence) {
|
|
390
|
+
artifact.durableEvidence ??= [];
|
|
391
|
+
if (hasEvidence(artifact, evidence)) {
|
|
392
|
+
return;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const prior = artifact.durableEvidence.findLast(
|
|
396
|
+
(existing) => existing.kind === evidence.kind,
|
|
397
|
+
);
|
|
398
|
+
const next = structuredClone(evidence);
|
|
399
|
+
if (
|
|
400
|
+
prior &&
|
|
401
|
+
(prior.ref !== next.ref || !arraysEqual(prior.paths, next.paths))
|
|
402
|
+
) {
|
|
403
|
+
next.supersedes = { ref: prior.ref, paths: [...prior.paths] };
|
|
404
|
+
}
|
|
405
|
+
artifact.durableEvidence.push(next);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function hasEvidence(artifact, evidence) {
|
|
409
|
+
return (artifact.durableEvidence ?? []).some(
|
|
410
|
+
(existing) =>
|
|
411
|
+
existing.kind === evidence.kind &&
|
|
412
|
+
existing.ref === evidence.ref &&
|
|
413
|
+
arraysEqual(existing.paths, evidence.paths),
|
|
414
|
+
);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
function updateOutcomes(
|
|
418
|
+
manifest,
|
|
419
|
+
buildRecord,
|
|
420
|
+
{ durable, warning, completedAt = new Date().toISOString() },
|
|
421
|
+
) {
|
|
422
|
+
const outcome = durable ? 'built-durable' : 'built-not-durable';
|
|
423
|
+
manifest.outcome = outcome;
|
|
424
|
+
buildRecord.outcome = outcome;
|
|
425
|
+
|
|
426
|
+
manifest.warnings = manifest.warnings.filter(
|
|
427
|
+
(entry) => !entry.startsWith('E_DURABILITY:'),
|
|
428
|
+
);
|
|
429
|
+
const stage = buildRecord.stages.find(({ id }) => id === 'durability');
|
|
430
|
+
if (!stage) {
|
|
431
|
+
throw new Error('Build record does not contain a durability stage.');
|
|
432
|
+
}
|
|
433
|
+
stage.status = durable ? 'passed' : 'warned';
|
|
434
|
+
stage.completedAt = completedAt;
|
|
435
|
+
stage.outputPaths = durable ? ['manifest.json', 'build-record.json'] : [];
|
|
436
|
+
stage.warnings = warning ? [warning] : [];
|
|
437
|
+
if (warning) {
|
|
438
|
+
manifest.warnings.push(warning);
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
async function persistRecords(runRoot, manifest, buildRecord) {
|
|
443
|
+
assertValid('build-record', buildRecord);
|
|
444
|
+
manifest.buildRecord.hash = canonicalHash(buildRecord);
|
|
445
|
+
assertValid('manifest', manifest, { buildRecord });
|
|
446
|
+
await writeJsonAtomic(runRoot, 'build-record.json', buildRecord);
|
|
447
|
+
await writeJsonAtomic(runRoot, 'manifest.json', manifest);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function requiredArtifacts(manifest) {
|
|
451
|
+
return manifest.artifacts.filter(
|
|
452
|
+
(artifact) =>
|
|
453
|
+
artifact.status === 'built' &&
|
|
454
|
+
artifact.rebuildable === false &&
|
|
455
|
+
typeof artifact.renderedPath === 'string',
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
function immutablePackage(manifest) {
|
|
460
|
+
const expectedPaths = [
|
|
461
|
+
manifest.source.factBasePath,
|
|
462
|
+
'source/fact-base.md',
|
|
463
|
+
...manifest.artifacts.map(({ contentPath }) => contentPath),
|
|
464
|
+
manifest.theme.path,
|
|
465
|
+
...manifest.artifacts
|
|
466
|
+
.filter(
|
|
467
|
+
({ status, renderedPath }) =>
|
|
468
|
+
status === 'built' && typeof renderedPath === 'string',
|
|
469
|
+
)
|
|
470
|
+
.map(({ renderedPath }) => renderedPath),
|
|
471
|
+
];
|
|
472
|
+
return [...new Set(expectedPaths)].map((path) => ({
|
|
473
|
+
path,
|
|
474
|
+
hash: manifest.immutableHashes[path],
|
|
475
|
+
}));
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
async function readJson(path) {
|
|
479
|
+
return JSON.parse(await readFile(path, 'utf8'));
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
function joinWithin(root, relativePath) {
|
|
483
|
+
const checked = validateSafeRelativePath(relativePath);
|
|
484
|
+
if (!checked.valid) {
|
|
485
|
+
throw new Error(
|
|
486
|
+
`Unsafe durability record path: ${checked.errors[0].message}`,
|
|
487
|
+
);
|
|
488
|
+
}
|
|
489
|
+
const path = resolve(root, checked.normalizedPath);
|
|
490
|
+
assertWithin(root, path, 'Durability record');
|
|
491
|
+
return path;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
function assertWithin(root, target, label) {
|
|
495
|
+
const path = relative(root, target);
|
|
496
|
+
if (path === '..' || path.startsWith(`..${sep}`)) {
|
|
497
|
+
throw new Error(`${label} escapes its configured root.`);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function toRepoPath(repoRoot, absolutePath) {
|
|
502
|
+
assertWithin(repoRoot, absolutePath, 'Evidence path');
|
|
503
|
+
return relative(repoRoot, absolutePath).split(sep).join('/');
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
async function fileHash(path) {
|
|
507
|
+
return bufferHash(await readFile(path));
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
function bufferHash(value) {
|
|
511
|
+
return `sha256:${createHash('sha256').update(value).digest('hex')}`;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
function assertValid(kind, value, context) {
|
|
515
|
+
const result = validateContract(kind, value, context);
|
|
516
|
+
if (!result.valid) {
|
|
517
|
+
throw new Error(
|
|
518
|
+
`Invalid ${kind}: ${result.errors
|
|
519
|
+
.map(({ path, code, message }) => `${path} [${code}]: ${message}`)
|
|
520
|
+
.join('; ')}`,
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
function arraysEqual(left, right) {
|
|
526
|
+
return (
|
|
527
|
+
left.length === right.length &&
|
|
528
|
+
left.every((value, index) => value === right[index])
|
|
529
|
+
);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function error(code, message) {
|
|
533
|
+
return { code, message };
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
function errorMessage(value) {
|
|
537
|
+
return value instanceof Error ? value.message : String(value);
|
|
538
|
+
}
|