@open-agent-toolkit/cli 0.2.30 → 0.2.32
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 +41 -0
- package/assets/docs/contributing/code.md +19 -7
- package/assets/docs/contributing/explainer-kit-verification.md +9 -1
- package/assets/docs/contributing/skills.md +9 -0
- package/assets/docs/provider-sync/commands.md +15 -10
- package/assets/docs/provider-sync/manifest-and-drift.md +23 -0
- package/assets/docs/provider-sync/providers.md +8 -2
- package/assets/docs/provider-sync/scope-and-surface.md +3 -1
- package/assets/docs/reference/file-locations.md +5 -0
- package/assets/docs/reference/troubleshooting.md +18 -0
- package/assets/docs/workflows/skills/explainer-kit-providers.md +16 -5
- package/assets/docs/workflows/skills/explainer-kit.md +98 -38
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/create-agnostic-skill/references/docs/skills-guide.md +34 -22
- package/assets/skills/explainer-kit/SKILL.md +33 -8
- package/assets/skills/explainer-kit/briefs/project-recap.md +25 -7
- package/assets/skills/explainer-kit/recipes/project-recap.v2.json +72 -0
- package/assets/skills/explainer-kit/references/contracts.md +49 -17
- package/assets/skills/explainer-kit/references/destination-contract.md +141 -25
- package/assets/skills/explainer-kit/references/extension-contract.md +19 -10
- package/assets/skills/explainer-kit/references/visual-authoring.md +24 -0
- package/assets/skills/explainer-kit/references/visual-review.md +19 -5
- package/assets/skills/explainer-kit/schemas/author-request.v3.schema.json +241 -0
- package/assets/skills/explainer-kit/schemas/publish-receipt.v2.schema.json +215 -0
- package/assets/skills/explainer-kit/schemas/publish-request.v2.schema.json +34 -0
- package/assets/skills/explainer-kit/schemas/run-request.schema.json +4 -1
- package/assets/skills/explainer-kit/schemas/terminal-evidence.v1.schema.json +81 -0
- package/assets/skills/explainer-kit/schemas/visual-review-evidence.v1.schema.json +66 -0
- package/assets/skills/explainer-kit/scripts/lib/catalog.mjs +109 -3
- package/assets/skills/explainer-kit/scripts/lib/contracts.mjs +550 -17
- package/assets/skills/explainer-kit/scripts/lib/durability.mjs +90 -8
- package/assets/skills/explainer-kit/scripts/lib/fs-safe.mjs +5 -0
- package/assets/skills/explainer-kit/scripts/lib/internal-references.mjs +538 -0
- package/assets/skills/explainer-kit/scripts/lib/package-coverage.mjs +129 -11
- package/assets/skills/explainer-kit/scripts/lib/publication-policy.mjs +54 -0
- package/assets/skills/explainer-kit/scripts/lib/recipes.mjs +2 -1
- package/assets/skills/explainer-kit/scripts/lib/records.mjs +139 -22
- package/assets/skills/explainer-kit/scripts/lib/s3-roots.mjs +353 -0
- package/assets/skills/explainer-kit/scripts/lib/s3-static.mjs +237 -107
- package/assets/skills/explainer-kit/scripts/lib/set-plan.mjs +1 -0
- package/assets/skills/explainer-kit/scripts/lib/terminal-evidence.mjs +157 -0
- package/assets/skills/explainer-kit/scripts/lib/visual-review.mjs +26 -6
- package/assets/skills/explainer-kit/scripts/run.mjs +1006 -144
- package/assets/skills/oat-agent-instructions-analyze/references/docs/provider-reference.md +14 -2
- package/assets/skills/oat-agent-instructions-apply/references/docs/provider-reference.md +14 -2
- package/assets/skills/oat-explainer-kit/SKILL.md +16 -3
- package/assets/skills/oat-explainer-kit/references/config-contract.md +13 -8
- package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +50 -6
- package/assets/skills/oat-explainer-kit/references/migration.md +2 -1
- package/assets/skills/oat-explainer-kit/references/visual-review-callback.md +11 -0
- package/assets/skills/oat-explainer-kit/scripts/bind-project-sources.mjs +37 -15
- package/assets/skills/oat-explainer-kit/scripts/check-terminal-outcome.mjs +83 -0
- package/assets/skills/oat-explainer-kit/scripts/derive-destination.mjs +91 -0
- package/assets/skills/oat-explainer-kit/scripts/finalize-tracked-run.mjs +66 -10
- package/assets/skills/oat-explainer-kit/scripts/resolve-config.mjs +60 -21
- package/assets/skills/oat-explainer-kit/scripts/resolve-paths.mjs +52 -8
- package/assets/skills/oat-explainer-kit/scripts/run.mjs +271 -36
- package/assets/skills/oat-project-autonomous/references/gate-inventory.md +2 -2
- package/assets/skills/oat-project-complete/SKILL.md +19 -3
- package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +2 -2
- package/assets/skills/oat-project-implement/references/completion-and-closeout.md +8 -0
- package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +2 -2
- package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +2 -2
- package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +2 -2
- package/dist/commands/config/index.d.ts.map +1 -1
- package/dist/commands/config/index.js +18 -0
- package/dist/commands/init/index.d.ts +2 -2
- package/dist/commands/init/index.d.ts.map +1 -1
- package/dist/commands/init/index.js +22 -13
- package/dist/commands/project/archive/archive-utils.d.ts.map +1 -1
- package/dist/commands/project/archive/archive-utils.js +57 -7
- package/dist/commands/project/archive/explainer-terminal-evidence.d.ts +29 -0
- package/dist/commands/project/archive/explainer-terminal-evidence.d.ts.map +1 -0
- package/dist/commands/project/archive/explainer-terminal-evidence.js +37 -0
- package/dist/commands/remove/skill/remove-skill.d.ts +4 -1
- package/dist/commands/remove/skill/remove-skill.d.ts.map +1 -1
- package/dist/commands/remove/skill/remove-skill.js +44 -5
- package/dist/commands/shared/adopt-stray.d.ts +5 -0
- package/dist/commands/shared/adopt-stray.d.ts.map +1 -1
- package/dist/commands/shared/adopt-stray.js +25 -4
- package/dist/commands/shared/native-skill-disposition.d.ts +22 -0
- package/dist/commands/shared/native-skill-disposition.d.ts.map +1 -0
- package/dist/commands/shared/native-skill-disposition.js +60 -0
- package/dist/commands/status/index.d.ts +2 -2
- package/dist/commands/status/index.d.ts.map +1 -1
- package/dist/commands/status/index.js +22 -13
- package/dist/config/oat-config.d.ts +2 -0
- package/dist/config/oat-config.d.ts.map +1 -1
- package/dist/config/oat-config.js +4 -0
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +1 -0
- package/dist/engine/compute-plan.d.ts +3 -2
- package/dist/engine/compute-plan.d.ts.map +1 -1
- package/dist/engine/compute-plan.js +1 -1
- package/dist/engine/index.d.ts +1 -1
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +1 -1
- package/dist/providers/copilot/paths.d.ts.map +1 -1
- package/dist/providers/copilot/paths.js +6 -4
- package/package.json +2 -2
- package/dist/commands/shared/cursor-skill-disposition.d.ts +0 -17
- package/dist/commands/shared/cursor-skill-disposition.d.ts.map +0 -1
- package/dist/commands/shared/cursor-skill-disposition.js +0 -41
- /package/assets/skills/explainer-kit/recipes/{project-recap.json → project-recap.v1.json} +0 -0
- /package/assets/skills/explainer-kit/schemas/{publish-receipt.schema.json → publish-receipt.v1.schema.json} +0 -0
- /package/assets/skills/explainer-kit/schemas/{publish-request.schema.json → publish-request.v1.schema.json} +0 -0
|
@@ -15,10 +15,25 @@ import { promisify } from 'node:util';
|
|
|
15
15
|
import {
|
|
16
16
|
catalogFromManifest,
|
|
17
17
|
initiativeCatalogPath,
|
|
18
|
+
resolvePublicVerificationPolicy,
|
|
18
19
|
serializeInitiativeCatalog,
|
|
19
20
|
validateInitiativeCatalog,
|
|
20
21
|
} from './catalog.mjs';
|
|
21
22
|
import { validateContract } from './contracts.mjs';
|
|
23
|
+
import { assertManifestPublishable } from './publication-policy.mjs';
|
|
24
|
+
import {
|
|
25
|
+
composePublicationTarget,
|
|
26
|
+
describeRootDivergence,
|
|
27
|
+
isPrivatePublicHost,
|
|
28
|
+
normalizePublishRoots,
|
|
29
|
+
privatePublicRootAllowed,
|
|
30
|
+
rootDivergenceWarningSuppressed,
|
|
31
|
+
} from './s3-roots.mjs';
|
|
32
|
+
|
|
33
|
+
export {
|
|
34
|
+
composePublicationTarget,
|
|
35
|
+
normalizePublishRoots,
|
|
36
|
+
} from './s3-roots.mjs';
|
|
22
37
|
|
|
23
38
|
const execFile = promisify(execFileCallback);
|
|
24
39
|
const SENTINEL_BODY = 'explainer-kit sentinel\n';
|
|
@@ -28,54 +43,6 @@ const AUTH_PATTERN =
|
|
|
28
43
|
/(?:credential|expired|sso session|access key|invalidclienttokenid|unrecognizedclient|token)/i;
|
|
29
44
|
const NOT_FOUND_PATTERN = /(?:\b404\b|not found|nosuchkey)/i;
|
|
30
45
|
|
|
31
|
-
export function normalizePublishRoots(s3Uri, publicBaseUrl) {
|
|
32
|
-
if (typeof s3Uri !== 'string' || !s3Uri.startsWith('s3://')) {
|
|
33
|
-
throw publishError('E_PUBLISH_ROOTS', 'S3 root must use s3://.');
|
|
34
|
-
}
|
|
35
|
-
const withoutScheme = s3Uri.slice('s3://'.length).replace(/\/+$/, '');
|
|
36
|
-
const slash = withoutScheme.indexOf('/');
|
|
37
|
-
const bucket = slash === -1 ? withoutScheme : withoutScheme.slice(0, slash);
|
|
38
|
-
const keyPrefix =
|
|
39
|
-
slash === -1
|
|
40
|
-
? ''
|
|
41
|
-
: withoutScheme
|
|
42
|
-
.slice(slash + 1)
|
|
43
|
-
.split('/')
|
|
44
|
-
.filter(Boolean)
|
|
45
|
-
.join('/');
|
|
46
|
-
if (!bucket || /\s/.test(bucket)) {
|
|
47
|
-
throw publishError('E_PUBLISH_ROOTS', 'S3 root has an invalid bucket.');
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
let publicUrl;
|
|
51
|
-
try {
|
|
52
|
-
publicUrl = new URL(publicBaseUrl);
|
|
53
|
-
} catch {
|
|
54
|
-
throw publishError('E_PUBLISH_ROOTS', 'Public root must be a valid URL.');
|
|
55
|
-
}
|
|
56
|
-
if (
|
|
57
|
-
publicUrl.protocol !== 'https:' ||
|
|
58
|
-
publicUrl.username ||
|
|
59
|
-
publicUrl.password ||
|
|
60
|
-
publicUrl.search ||
|
|
61
|
-
publicUrl.hash
|
|
62
|
-
) {
|
|
63
|
-
throw publishError(
|
|
64
|
-
'E_PUBLISH_ROOTS',
|
|
65
|
-
'Public root must be credential-free HTTPS without query or fragment.',
|
|
66
|
-
);
|
|
67
|
-
}
|
|
68
|
-
publicUrl.pathname = publicUrl.pathname.replace(/\/+$/, '');
|
|
69
|
-
const normalizedPublic = publicUrl.toString().replace(/\/$/, '');
|
|
70
|
-
const normalizedS3 = `s3://${bucket}${keyPrefix ? `/${keyPrefix}` : ''}`;
|
|
71
|
-
return {
|
|
72
|
-
bucket,
|
|
73
|
-
keyPrefix,
|
|
74
|
-
s3Uri: normalizedS3,
|
|
75
|
-
publicBaseUrl: normalizedPublic,
|
|
76
|
-
};
|
|
77
|
-
}
|
|
78
|
-
|
|
79
46
|
export function createSentinelRelativePath(
|
|
80
47
|
runId,
|
|
81
48
|
randomBytes = nodeRandomBytes,
|
|
@@ -115,6 +82,14 @@ export async function publishS3Static(request, dependencies = {}) {
|
|
|
115
82
|
const siteRoot = resolve(cwd, request.siteRoot);
|
|
116
83
|
const manifestPath = resolve(cwd, request.manifestPath);
|
|
117
84
|
const manifest = await readJson(manifestPath, 'manifest');
|
|
85
|
+
const buildRecord =
|
|
86
|
+
manifest.outcome === 'incomplete'
|
|
87
|
+
? await readJson(
|
|
88
|
+
resolve(dirname(manifestPath), manifest.buildRecord?.path ?? ''),
|
|
89
|
+
'build record',
|
|
90
|
+
)
|
|
91
|
+
: undefined;
|
|
92
|
+
assertManifestPublishable(manifest, { buildRecord });
|
|
118
93
|
const manifestValidation = validateContract('manifest', manifest);
|
|
119
94
|
if (!manifestValidation.valid) {
|
|
120
95
|
throw publishError(
|
|
@@ -136,12 +111,33 @@ export async function publishS3Static(request, dependencies = {}) {
|
|
|
136
111
|
manifest.runId,
|
|
137
112
|
randomBytes,
|
|
138
113
|
);
|
|
139
|
-
const sentinelTargetPath =
|
|
140
|
-
|
|
114
|
+
const sentinelTargetPath = composePublicationTarget(
|
|
115
|
+
sentinelRelativePath,
|
|
116
|
+
roots,
|
|
117
|
+
);
|
|
118
|
+
// Resolved before the catalog is built: the catalog is hashed and uploaded
|
|
119
|
+
// before any public verification runs, so it must carry the policy up front.
|
|
120
|
+
const publicAccess =
|
|
121
|
+
request.schemaVersion === 'explainer-kit.publish-request/v1'
|
|
122
|
+
? 'public'
|
|
123
|
+
: request.publicAccess;
|
|
124
|
+
const verificationPolicy = resolvePublicVerificationPolicy(publicAccess);
|
|
125
|
+
|
|
126
|
+
const divergence = describeRootDivergence(roots);
|
|
127
|
+
if (divergence && !rootDivergenceWarningSuppressed()) {
|
|
128
|
+
(dependencies.warn ?? ((message) => console.warn(message)))(
|
|
129
|
+
`explainer-kit: ${divergence}`,
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const catalog = catalogFromManifest(manifest, roots.publicBaseUrl, {
|
|
134
|
+
publicAccess,
|
|
135
|
+
});
|
|
141
136
|
const catalogValidation = validateInitiativeCatalog(
|
|
142
137
|
catalog,
|
|
143
138
|
manifest,
|
|
144
139
|
roots.publicBaseUrl,
|
|
140
|
+
{ publicAccess },
|
|
145
141
|
);
|
|
146
142
|
if (!catalogValidation.valid) {
|
|
147
143
|
throw publishError(
|
|
@@ -164,6 +160,7 @@ export async function publishS3Static(request, dependencies = {}) {
|
|
|
164
160
|
const sentinelBodyPath = join(sentinelDirectory, 'sentinel.txt');
|
|
165
161
|
const catalogBodyPath = join(sentinelDirectory, 'catalog.json');
|
|
166
162
|
const catalogArtifact = {
|
|
163
|
+
source: { kind: 'auxiliary', name: 'catalog' },
|
|
167
164
|
manifestPath: catalogManifestPath,
|
|
168
165
|
publishPath: catalogPublishPath,
|
|
169
166
|
filePath: catalogBodyPath,
|
|
@@ -192,21 +189,41 @@ export async function publishS3Static(request, dependencies = {}) {
|
|
|
192
189
|
{ sleep },
|
|
193
190
|
);
|
|
194
191
|
sentinelUploaded = true;
|
|
195
|
-
await
|
|
192
|
+
const sentinelMetadata = await readObjectMetadata(
|
|
196
193
|
command,
|
|
197
194
|
headObjectArgs(roots, sentinelRelativePath, awsOptions),
|
|
198
195
|
{ sleep },
|
|
199
196
|
);
|
|
200
|
-
const
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
197
|
+
const sentinelObjectVerification = await verifyObjectBytes({
|
|
198
|
+
command,
|
|
199
|
+
metadata: sentinelMetadata,
|
|
200
|
+
roots,
|
|
201
|
+
relativePath: sentinelRelativePath,
|
|
202
|
+
expectedHash: fileHash(Buffer.from(SENTINEL_BODY)),
|
|
203
|
+
downloadPath: join(sentinelDirectory, 'sentinel-download.txt'),
|
|
204
|
+
awsOptions,
|
|
205
|
+
sleep,
|
|
206
|
+
});
|
|
207
|
+
let sentinelPublicVerification = {
|
|
208
|
+
status: verificationPolicy.receiptSkipStatus,
|
|
209
|
+
};
|
|
210
|
+
if (verificationPolicy.verifyPublicly) {
|
|
211
|
+
const sentinelResponse = await httpGet(sentinelTargetPath.publicUrl);
|
|
212
|
+
if (
|
|
213
|
+
sentinelResponse.status < 200 ||
|
|
214
|
+
sentinelResponse.status > 299 ||
|
|
215
|
+
!responseBytes(sentinelResponse.body).equals(Buffer.from(SENTINEL_BODY))
|
|
216
|
+
) {
|
|
217
|
+
throw publishError(
|
|
218
|
+
'E_PUBLISH_ROOTS',
|
|
219
|
+
'Public root did not serve the uploaded sentinel.',
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
sentinelPublicVerification = {
|
|
223
|
+
status: 'verified',
|
|
224
|
+
httpStatus: sentinelResponse.status,
|
|
225
|
+
hash: fileHash(responseBytes(sentinelResponse.body)),
|
|
226
|
+
};
|
|
210
227
|
}
|
|
211
228
|
await runAws(
|
|
212
229
|
command,
|
|
@@ -217,7 +234,7 @@ export async function publishS3Static(request, dependencies = {}) {
|
|
|
217
234
|
|
|
218
235
|
const receiptArtifacts = [];
|
|
219
236
|
for (const artifact of publicationArtifacts) {
|
|
220
|
-
const target =
|
|
237
|
+
const target = composePublicationTarget(artifact.publishPath, roots);
|
|
221
238
|
const metadata = await readExistingMetadata(
|
|
222
239
|
command,
|
|
223
240
|
headObjectArgs(roots, artifact.publishPath, awsOptions),
|
|
@@ -238,51 +255,88 @@ export async function publishS3Static(request, dependencies = {}) {
|
|
|
238
255
|
{ sleep },
|
|
239
256
|
);
|
|
240
257
|
}
|
|
241
|
-
const uploadedMetadata = await
|
|
258
|
+
const uploadedMetadata = await readObjectMetadata(
|
|
242
259
|
command,
|
|
243
260
|
headObjectArgs(roots, artifact.publishPath, awsOptions),
|
|
244
261
|
{ sleep },
|
|
245
262
|
);
|
|
246
|
-
assertMetadata(uploadedMetadata
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
)
|
|
263
|
+
assertMetadata(uploadedMetadata, artifact);
|
|
264
|
+
const objectVerification = await verifyObjectBytes({
|
|
265
|
+
command,
|
|
266
|
+
metadata: uploadedMetadata,
|
|
267
|
+
roots,
|
|
268
|
+
relativePath: artifact.publishPath,
|
|
269
|
+
expectedHash: artifact.hash,
|
|
270
|
+
downloadPath: join(
|
|
271
|
+
sentinelDirectory,
|
|
272
|
+
`object-${receiptArtifacts.length}.download`,
|
|
273
|
+
),
|
|
274
|
+
awsOptions,
|
|
275
|
+
sleep,
|
|
276
|
+
});
|
|
277
|
+
let publicVerification = {
|
|
278
|
+
status: verificationPolicy.receiptSkipStatus,
|
|
279
|
+
};
|
|
280
|
+
if (verificationPolicy.verifyPublicly) {
|
|
281
|
+
const response = await httpGet(target.publicUrl);
|
|
282
|
+
if (response.status < 200 || response.status > 299) {
|
|
283
|
+
throw publishError(
|
|
284
|
+
'E_PUBLISH_VERIFY',
|
|
285
|
+
`Public verification failed for ${artifact.manifestPath}.`,
|
|
286
|
+
);
|
|
287
|
+
}
|
|
288
|
+
const servedType = headerValue(response.headers, 'content-type');
|
|
289
|
+
if (!contentTypesMatch(servedType, artifact.contentType)) {
|
|
290
|
+
throw publishError(
|
|
291
|
+
'E_PUBLISH_VERIFY',
|
|
292
|
+
`Public content type mismatch for ${artifact.manifestPath}.`,
|
|
293
|
+
);
|
|
294
|
+
}
|
|
295
|
+
const publicHash = fileHash(responseBytes(response.body));
|
|
296
|
+
if (publicHash !== artifact.hash) {
|
|
297
|
+
throw publishError(
|
|
298
|
+
'E_PUBLISH_VERIFY',
|
|
299
|
+
`Public bytes do not match the manifest for ${artifact.manifestPath}.`,
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
publicVerification = {
|
|
303
|
+
status: 'verified',
|
|
304
|
+
httpStatus: response.status,
|
|
305
|
+
hash: publicHash,
|
|
306
|
+
};
|
|
266
307
|
}
|
|
267
308
|
receiptArtifacts.push({
|
|
309
|
+
source: artifact.source,
|
|
268
310
|
relativePath: artifact.manifestPath,
|
|
269
311
|
hash: artifact.hash,
|
|
270
312
|
s3Uri: target.s3Uri,
|
|
271
313
|
publicUrl: target.publicUrl,
|
|
272
|
-
httpStatus: response.status,
|
|
273
314
|
contentType: artifact.contentType,
|
|
315
|
+
objectVerification,
|
|
316
|
+
publicVerification,
|
|
274
317
|
});
|
|
275
318
|
}
|
|
276
319
|
|
|
277
320
|
const receipt = {
|
|
278
|
-
schemaVersion: 'explainer-kit.publish-receipt/
|
|
321
|
+
schemaVersion: 'explainer-kit.publish-receipt/v2',
|
|
279
322
|
provider: 's3-static',
|
|
280
323
|
publishedAt: now(),
|
|
324
|
+
publicAccess,
|
|
325
|
+
// Durable trace for the anti-SSRF opt-in. Without it, a run that
|
|
326
|
+
// published to an internal address with the control disabled is
|
|
327
|
+
// indistinguishable in retained evidence from one that did not, because
|
|
328
|
+
// the variable is read straight from the environment and enters no other
|
|
329
|
+
// record. Emitted only in that exceptional case, so ordinary receipts are
|
|
330
|
+
// byte-identical to before.
|
|
331
|
+
...(privatePublicRootAllowed() &&
|
|
332
|
+
isPrivatePublicHost(new URL(roots.publicBaseUrl).hostname) && {
|
|
333
|
+
publicRootPolicy: 'private-allowed',
|
|
334
|
+
}),
|
|
281
335
|
roots: { s3Uri: roots.s3Uri, publicBaseUrl: roots.publicBaseUrl },
|
|
282
336
|
sentinel: {
|
|
283
337
|
relativePath: sentinelRelativePath,
|
|
284
|
-
|
|
285
|
-
|
|
338
|
+
objectVerification: sentinelObjectVerification,
|
|
339
|
+
publicVerification: sentinelPublicVerification,
|
|
286
340
|
deleted: sentinelDeleted,
|
|
287
341
|
},
|
|
288
342
|
artifacts: receiptArtifacts,
|
|
@@ -375,6 +429,7 @@ async function collectArtifacts(manifest, siteRoot) {
|
|
|
375
429
|
}
|
|
376
430
|
const contentType = contentTypeFor(publishPath, entry.mediaType);
|
|
377
431
|
artifacts.push({
|
|
432
|
+
source: { kind: 'manifest', artifactId: entry.id },
|
|
378
433
|
manifestPath: entry.renderedPath,
|
|
379
434
|
publishPath,
|
|
380
435
|
filePath,
|
|
@@ -411,6 +466,8 @@ function putObjectArgs({
|
|
|
411
466
|
cacheControl,
|
|
412
467
|
'--metadata',
|
|
413
468
|
`explainer-sha256=${hash.slice('sha256:'.length)}`,
|
|
469
|
+
'--checksum-sha256',
|
|
470
|
+
Buffer.from(hash.slice('sha256:'.length), 'hex').toString('base64'),
|
|
414
471
|
'--no-cli-pager',
|
|
415
472
|
],
|
|
416
473
|
awsOptions,
|
|
@@ -426,6 +483,8 @@ function headObjectArgs(roots, relativePath, awsOptions) {
|
|
|
426
483
|
roots.bucket,
|
|
427
484
|
'--key',
|
|
428
485
|
objectKey(roots, relativePath),
|
|
486
|
+
'--checksum-mode',
|
|
487
|
+
'ENABLED',
|
|
429
488
|
'--output',
|
|
430
489
|
'json',
|
|
431
490
|
'--no-cli-pager',
|
|
@@ -434,6 +493,24 @@ function headObjectArgs(roots, relativePath, awsOptions) {
|
|
|
434
493
|
);
|
|
435
494
|
}
|
|
436
495
|
|
|
496
|
+
function getObjectArgs(roots, relativePath, downloadPath, awsOptions) {
|
|
497
|
+
return withAwsOptions(
|
|
498
|
+
[
|
|
499
|
+
's3api',
|
|
500
|
+
'get-object',
|
|
501
|
+
'--bucket',
|
|
502
|
+
roots.bucket,
|
|
503
|
+
'--key',
|
|
504
|
+
objectKey(roots, relativePath),
|
|
505
|
+
'--checksum-mode',
|
|
506
|
+
'ENABLED',
|
|
507
|
+
downloadPath,
|
|
508
|
+
'--no-cli-pager',
|
|
509
|
+
],
|
|
510
|
+
awsOptions,
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
|
|
437
514
|
function deleteObjectArgs(roots, relativePath, awsOptions) {
|
|
438
515
|
return withAwsOptions(
|
|
439
516
|
[
|
|
@@ -500,6 +577,11 @@ async function readExistingMetadata(command, args, { sleep }) {
|
|
|
500
577
|
}
|
|
501
578
|
}
|
|
502
579
|
|
|
580
|
+
async function readObjectMetadata(command, args, { sleep }) {
|
|
581
|
+
const result = await runAws(command, args, { sleep });
|
|
582
|
+
return parseAwsJson(result.stdout);
|
|
583
|
+
}
|
|
584
|
+
|
|
503
585
|
function matchesPublishedArtifact(metadata, artifact) {
|
|
504
586
|
return (
|
|
505
587
|
metadata?.Metadata?.['explainer-sha256'] ===
|
|
@@ -509,20 +591,75 @@ function matchesPublishedArtifact(metadata, artifact) {
|
|
|
509
591
|
);
|
|
510
592
|
}
|
|
511
593
|
|
|
512
|
-
function assertMetadata(
|
|
513
|
-
|
|
594
|
+
function assertMetadata(metadata, artifact) {
|
|
595
|
+
if (!matchesPublishedArtifact(metadata, artifact)) {
|
|
596
|
+
throw publishError(
|
|
597
|
+
'E_PUBLISH_VERIFY',
|
|
598
|
+
`Uploaded metadata mismatch for ${artifact.manifestPath}.`,
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
async function verifyObjectBytes({
|
|
604
|
+
command,
|
|
605
|
+
metadata,
|
|
606
|
+
roots,
|
|
607
|
+
relativePath,
|
|
608
|
+
expectedHash,
|
|
609
|
+
downloadPath,
|
|
610
|
+
awsOptions,
|
|
611
|
+
sleep,
|
|
612
|
+
}) {
|
|
613
|
+
const expectedBase64 = Buffer.from(
|
|
614
|
+
expectedHash.slice('sha256:'.length),
|
|
615
|
+
'hex',
|
|
616
|
+
).toString('base64');
|
|
617
|
+
if (typeof metadata.ChecksumSHA256 === 'string') {
|
|
618
|
+
if (metadata.ChecksumSHA256 !== expectedBase64) {
|
|
619
|
+
throw publishError(
|
|
620
|
+
'E_PUBLISH_VERIFY',
|
|
621
|
+
`Authenticated object checksum mismatch for ${relativePath}.`,
|
|
622
|
+
);
|
|
623
|
+
}
|
|
624
|
+
return {
|
|
625
|
+
status: 'verified',
|
|
626
|
+
method: 'service-checksum',
|
|
627
|
+
hash: expectedHash,
|
|
628
|
+
};
|
|
629
|
+
}
|
|
630
|
+
|
|
514
631
|
try {
|
|
515
|
-
|
|
632
|
+
await runAws(
|
|
633
|
+
command,
|
|
634
|
+
getObjectArgs(roots, relativePath, downloadPath, awsOptions),
|
|
635
|
+
{ sleep },
|
|
636
|
+
);
|
|
516
637
|
} catch {
|
|
517
638
|
throw publishError(
|
|
518
639
|
'E_PUBLISH_VERIFY',
|
|
519
|
-
|
|
640
|
+
`Authenticated object-byte verification is unavailable for ${relativePath}.`,
|
|
520
641
|
);
|
|
521
642
|
}
|
|
522
|
-
if (
|
|
643
|
+
if (fileHash(await readFile(downloadPath)) !== expectedHash) {
|
|
523
644
|
throw publishError(
|
|
524
645
|
'E_PUBLISH_VERIFY',
|
|
525
|
-
`
|
|
646
|
+
`Authenticated object bytes do not match ${relativePath}.`,
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
return {
|
|
650
|
+
status: 'verified',
|
|
651
|
+
method: 'authenticated-download',
|
|
652
|
+
hash: expectedHash,
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
function parseAwsJson(stdout) {
|
|
657
|
+
try {
|
|
658
|
+
return JSON.parse(stdout || '{}');
|
|
659
|
+
} catch {
|
|
660
|
+
throw publishError(
|
|
661
|
+
'E_PUBLISH_VERIFY',
|
|
662
|
+
'AWS returned invalid object metadata.',
|
|
526
663
|
);
|
|
527
664
|
}
|
|
528
665
|
}
|
|
@@ -554,17 +691,6 @@ function objectKey(roots, relativePath) {
|
|
|
554
691
|
return roots.keyPrefix ? `${roots.keyPrefix}/${relativePath}` : relativePath;
|
|
555
692
|
}
|
|
556
693
|
|
|
557
|
-
function targetPath(relativePath, roots) {
|
|
558
|
-
const suffix = relativePath
|
|
559
|
-
.split('/')
|
|
560
|
-
.map((part) => encodeURIComponent(part))
|
|
561
|
-
.join('/');
|
|
562
|
-
return {
|
|
563
|
-
s3Uri: `${roots.s3Uri}/${relativePath}`,
|
|
564
|
-
publicUrl: `${roots.publicBaseUrl}/${suffix}`,
|
|
565
|
-
};
|
|
566
|
-
}
|
|
567
|
-
|
|
568
694
|
function fileHash(bytes) {
|
|
569
695
|
return `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
570
696
|
}
|
|
@@ -596,9 +722,13 @@ async function defaultCommand(file, args) {
|
|
|
596
722
|
return execFile(file, args, { maxBuffer: 1024 * 1024 });
|
|
597
723
|
}
|
|
598
724
|
|
|
599
|
-
async function defaultHttpGet(url) {
|
|
600
|
-
|
|
601
|
-
|
|
725
|
+
export async function defaultHttpGet(url, { fetchImpl = fetch } = {}) {
|
|
726
|
+
// A canonical public artifact URL is uploaded to a known key and should never
|
|
727
|
+
// legitimately redirect. Following redirects turned public verification into
|
|
728
|
+
// an outbound GET primitive that a third party could bounce inward, so a
|
|
729
|
+
// redirect is a verification failure rather than something to chase.
|
|
730
|
+
const response = await fetchImpl(url, {
|
|
731
|
+
redirect: 'error',
|
|
602
732
|
signal: AbortSignal.timeout(15_000),
|
|
603
733
|
});
|
|
604
734
|
return {
|
|
@@ -14,6 +14,7 @@ export async function planExplainerSet({
|
|
|
14
14
|
schemaVersion: 'explainer-kit.set-plan-request/v1',
|
|
15
15
|
recipe: structuredClone(recipe),
|
|
16
16
|
factBase: structuredClone(factBase),
|
|
17
|
+
sourceIds: structuredClone(sourceIds),
|
|
17
18
|
discovery: structuredClone(discovery),
|
|
18
19
|
});
|
|
19
20
|
const callback =
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { lstat, readFile, realpath } from 'node:fs/promises';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
|
|
5
|
+
import { canonicalHash, validateContract } from './contracts.mjs';
|
|
6
|
+
|
|
7
|
+
export const TERMINAL_EVIDENCE_VERSION = 'explainer-kit.terminal-evidence/v1';
|
|
8
|
+
export const VISUAL_REVIEW_EVIDENCE_VERSION =
|
|
9
|
+
'explainer-kit.visual-review-evidence/v1';
|
|
10
|
+
export const EVIDENCE_REASON_MAX_COUNT = 50;
|
|
11
|
+
|
|
12
|
+
export function createTerminalEvidence({
|
|
13
|
+
runId,
|
|
14
|
+
outcome,
|
|
15
|
+
manifest,
|
|
16
|
+
reasons,
|
|
17
|
+
evidenceDisposition,
|
|
18
|
+
supersededBy,
|
|
19
|
+
} = {}) {
|
|
20
|
+
const evidence = {
|
|
21
|
+
schemaVersion: TERMINAL_EVIDENCE_VERSION,
|
|
22
|
+
runId,
|
|
23
|
+
outcome,
|
|
24
|
+
...(manifest && { manifestHash: canonicalHash(manifest) }),
|
|
25
|
+
reasons: structuredClone(reasons),
|
|
26
|
+
evidenceDisposition,
|
|
27
|
+
...(supersededBy && {
|
|
28
|
+
supersededBy: structuredClone(supersededBy),
|
|
29
|
+
}),
|
|
30
|
+
};
|
|
31
|
+
assertTerminalEvidence(evidence, { manifest });
|
|
32
|
+
return evidence;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function assertTerminalEvidence(evidence, { manifest } = {}) {
|
|
36
|
+
const validation = validateContract('terminal-evidence', evidence, {
|
|
37
|
+
manifest,
|
|
38
|
+
});
|
|
39
|
+
if (!validation.valid) {
|
|
40
|
+
throw new Error(
|
|
41
|
+
`Invalid terminal evidence: ${validation.errors
|
|
42
|
+
.map(({ path, code }) => `${path} [${code}]`)
|
|
43
|
+
.join('; ')}`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
return evidence;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function createVisualReviewEvidence({
|
|
50
|
+
request,
|
|
51
|
+
attempt,
|
|
52
|
+
result,
|
|
53
|
+
failureKind,
|
|
54
|
+
} = {}) {
|
|
55
|
+
const evidence = {
|
|
56
|
+
schemaVersion: VISUAL_REVIEW_EVIDENCE_VERSION,
|
|
57
|
+
requestHash: request?.requestHash,
|
|
58
|
+
attempt,
|
|
59
|
+
disposition: failureKind
|
|
60
|
+
? 'failed'
|
|
61
|
+
: result?.disposition === 'pass'
|
|
62
|
+
? 'pass'
|
|
63
|
+
: 'correct',
|
|
64
|
+
reasons: failureKind
|
|
65
|
+
? [{ stage: 'visual-review', kind: failureKind, count: 1 }]
|
|
66
|
+
: result?.disposition === 'pass'
|
|
67
|
+
? []
|
|
68
|
+
: findingReasons(result?.findings),
|
|
69
|
+
};
|
|
70
|
+
assertVisualReviewEvidence(evidence, { request, attempt });
|
|
71
|
+
return evidence;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function assertVisualReviewEvidence(
|
|
75
|
+
evidence,
|
|
76
|
+
{ request, attempt } = {},
|
|
77
|
+
) {
|
|
78
|
+
const validation = validateContract('visual-review-evidence', evidence, {
|
|
79
|
+
visualReviewRequest: request,
|
|
80
|
+
attempt,
|
|
81
|
+
});
|
|
82
|
+
if (!validation.valid) {
|
|
83
|
+
throw new Error(
|
|
84
|
+
`Invalid retained visual evidence: ${validation.errors
|
|
85
|
+
.map(({ path, code }) => `${path} [${code}]`)
|
|
86
|
+
.join('; ')}`,
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return evidence;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function evidenceReason(stage, kind, { artifactId, count = 1 } = {}) {
|
|
93
|
+
return {
|
|
94
|
+
stage,
|
|
95
|
+
kind,
|
|
96
|
+
...(artifactId !== undefined && { artifactId }),
|
|
97
|
+
count,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function projectThrownReason(stage, kind = 'pipeline-failure') {
|
|
102
|
+
return evidenceReason(stage, kind);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function readTerminalEvidenceFile(
|
|
106
|
+
runRoot,
|
|
107
|
+
{ manifest, expectedBytes, expectedHash } = {},
|
|
108
|
+
) {
|
|
109
|
+
if (typeof runRoot !== 'string' || runRoot.length === 0) {
|
|
110
|
+
throw new TypeError('Terminal evidence run root must be a path string.');
|
|
111
|
+
}
|
|
112
|
+
const canonicalRunRoot = await realpath(runRoot);
|
|
113
|
+
const evidencePath = resolve(canonicalRunRoot, 'terminal-evidence.json');
|
|
114
|
+
const stats = await lstat(evidencePath);
|
|
115
|
+
if (stats.isSymbolicLink() || !stats.isFile()) {
|
|
116
|
+
throw new Error(
|
|
117
|
+
'Terminal evidence must be a regular file, not a symbolic link.',
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
const canonicalEvidencePath = await realpath(evidencePath);
|
|
121
|
+
if (canonicalEvidencePath !== evidencePath) {
|
|
122
|
+
throw new Error('Terminal evidence must remain within the run root.');
|
|
123
|
+
}
|
|
124
|
+
const bytes = await readFile(canonicalEvidencePath);
|
|
125
|
+
const hash = `sha256:${createHash('sha256').update(bytes).digest('hex')}`;
|
|
126
|
+
if (expectedHash !== undefined && hash !== expectedHash) {
|
|
127
|
+
throw new Error('Terminal evidence bytes changed while staging.');
|
|
128
|
+
}
|
|
129
|
+
if (
|
|
130
|
+
expectedBytes !== undefined &&
|
|
131
|
+
!bytes.equals(Buffer.from(expectedBytes))
|
|
132
|
+
) {
|
|
133
|
+
throw new Error('Terminal evidence bytes changed while staging.');
|
|
134
|
+
}
|
|
135
|
+
let evidence;
|
|
136
|
+
try {
|
|
137
|
+
evidence = JSON.parse(bytes.toString('utf8'));
|
|
138
|
+
} catch {
|
|
139
|
+
throw new Error('Terminal evidence must contain valid JSON.');
|
|
140
|
+
}
|
|
141
|
+
assertTerminalEvidence(evidence, { manifest });
|
|
142
|
+
return { evidence, bytes, hash };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function findingReasons(findings) {
|
|
146
|
+
const counts = new Map();
|
|
147
|
+
let total = 0;
|
|
148
|
+
for (const finding of Array.isArray(findings) ? findings : []) {
|
|
149
|
+
if (total >= EVIDENCE_REASON_MAX_COUNT) break;
|
|
150
|
+
const artifactId = finding?.artifactId;
|
|
151
|
+
counts.set(artifactId, (counts.get(artifactId) ?? 0) + 1);
|
|
152
|
+
total += 1;
|
|
153
|
+
}
|
|
154
|
+
return [...counts].map(([artifactId, count]) =>
|
|
155
|
+
evidenceReason('visual-review', 'finding', { artifactId, count }),
|
|
156
|
+
);
|
|
157
|
+
}
|