@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
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { readFileSync } from 'node:fs';
|
|
3
3
|
|
|
4
|
+
import {
|
|
5
|
+
composePublicationTarget,
|
|
6
|
+
normalizePublishRoots,
|
|
7
|
+
} from './s3-roots.mjs';
|
|
4
8
|
import { validatePortablePath } from './safe-paths.mjs';
|
|
5
9
|
import {
|
|
6
10
|
parseCanonicalGithubBlobUrl,
|
|
@@ -14,17 +18,24 @@ const SCHEMA_FILES = {
|
|
|
14
18
|
manifest: 'manifest.schema.json',
|
|
15
19
|
'build-record': 'build-record.schema.json',
|
|
16
20
|
'durability-evidence': 'durability-evidence.schema.json',
|
|
17
|
-
'publish-request': 'publish-request.schema.json',
|
|
18
|
-
'publish-
|
|
21
|
+
'publish-request/v1': 'publish-request.v1.schema.json',
|
|
22
|
+
'publish-request/v2': 'publish-request.v2.schema.json',
|
|
23
|
+
'publish-receipt/v1': 'publish-receipt.v1.schema.json',
|
|
24
|
+
'publish-receipt/v2': 'publish-receipt.v2.schema.json',
|
|
19
25
|
'author-request/v2': 'author-request.v2.schema.json',
|
|
26
|
+
'author-request/v3': 'author-request.v3.schema.json',
|
|
20
27
|
'author-result/v2': 'author-result.v2.schema.json',
|
|
21
28
|
'set-plan': 'set-plan.v1.schema.json',
|
|
22
29
|
'visual-review-request': 'visual-review-request.v1.schema.json',
|
|
23
30
|
'visual-review-result': 'visual-review-result.v1.schema.json',
|
|
31
|
+
'visual-review-evidence': 'visual-review-evidence.v1.schema.json',
|
|
32
|
+
'terminal-evidence': 'terminal-evidence.v1.schema.json',
|
|
24
33
|
};
|
|
25
34
|
const DEFAULT_SCHEMA_KEYS = {
|
|
26
|
-
'author-request': 'author-request/
|
|
35
|
+
'author-request': 'author-request/v3',
|
|
27
36
|
'author-result': 'author-result/v2',
|
|
37
|
+
'publish-request': 'publish-request/v2',
|
|
38
|
+
'publish-receipt': 'publish-receipt/v2',
|
|
28
39
|
};
|
|
29
40
|
|
|
30
41
|
const SCHEMAS = Object.fromEntries(
|
|
@@ -38,6 +49,34 @@ const SCHEMAS = Object.fromEntries(
|
|
|
38
49
|
const SCHEMAS_BY_ID = new Map(
|
|
39
50
|
Object.values(SCHEMAS).map((schema) => [schema.$id, schema]),
|
|
40
51
|
);
|
|
52
|
+
// `validateContract` accepts a schema `$id` as its `kind`, but every downstream
|
|
53
|
+
// gate matches on the short registry form. `'explainer-kit.publish-request/v1'`
|
|
54
|
+
// does not start with `'publish-request'`, so an `$id`-form call skipped the
|
|
55
|
+
// default-deny publication-root gate entirely and answered `valid: true` for a
|
|
56
|
+
// credential-bearing root. Normalize once, here, so no individual gate has to
|
|
57
|
+
// remember to handle both spellings.
|
|
58
|
+
const KIND_BY_SCHEMA_ID = new Map(
|
|
59
|
+
Object.entries(SCHEMAS).map(([kind, schema]) => [schema.$id, kind]),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
function canonicalContractKind(kind) {
|
|
63
|
+
return KIND_BY_SCHEMA_ID.get(kind) ?? kind;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const LEGACY_PUBLISH_RECEIPT = 'explainer-kit.publish-receipt/v1';
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* True for every publish-receipt shape except the retained v1 replay form.
|
|
70
|
+
*
|
|
71
|
+
* Stated as "not v1" rather than "=== v2" on purpose: keying verification to an
|
|
72
|
+
* exact version string means a future `publish-receipt/v3` silently skips
|
|
73
|
+
* sentinel verification and per-artifact validation, which is the same
|
|
74
|
+
* fail-open shape the publication-root gate was rewritten to eliminate. v1 is
|
|
75
|
+
* the only version that legitimately lacks these facts.
|
|
76
|
+
*/
|
|
77
|
+
export function isVerifiablePublishReceipt(value) {
|
|
78
|
+
return value?.schemaVersion !== LEGACY_PUBLISH_RECEIPT;
|
|
79
|
+
}
|
|
41
80
|
const RAW_SECRET_KEYS = new Set([
|
|
42
81
|
'accesskey',
|
|
43
82
|
'accesskeyid',
|
|
@@ -78,11 +117,15 @@ export function validateContract(kind, value, context = {}) {
|
|
|
78
117
|
}
|
|
79
118
|
|
|
80
119
|
const errors = [];
|
|
120
|
+
// Every gate below matches on the short registry form, so an `$id`-form kind
|
|
121
|
+
// is normalized before dispatch rather than in each gate.
|
|
122
|
+
const canonicalKind = canonicalContractKind(kind);
|
|
81
123
|
findRawSecrets(value, '$', errors);
|
|
82
124
|
validateSchema(schema, value, '$', schema, errors);
|
|
83
|
-
validateContractPaths(
|
|
84
|
-
|
|
85
|
-
|
|
125
|
+
validateContractPaths(canonicalKind, value, errors);
|
|
126
|
+
validatePublicationRoots(canonicalKind, value, errors);
|
|
127
|
+
validateCrossRecord(canonicalKind, value, context, errors);
|
|
128
|
+
validateSourceBacklinks(canonicalKind, value, errors);
|
|
86
129
|
return { valid: errors.length === 0, errors };
|
|
87
130
|
}
|
|
88
131
|
|
|
@@ -438,6 +481,107 @@ function validateContractPaths(kind, value, errors) {
|
|
|
438
481
|
}
|
|
439
482
|
}
|
|
440
483
|
|
|
484
|
+
function validatePublicationRoots(kind, value, errors, path = '$') {
|
|
485
|
+
if (!isObject(value)) return;
|
|
486
|
+
|
|
487
|
+
if (kind === 'run-request') {
|
|
488
|
+
const publish = value.durability?.publish;
|
|
489
|
+
if (isObject(publish)) {
|
|
490
|
+
validatePublicationRoots(
|
|
491
|
+
'publish-request',
|
|
492
|
+
publish,
|
|
493
|
+
errors,
|
|
494
|
+
`${path}.durability.publish`,
|
|
495
|
+
);
|
|
496
|
+
}
|
|
497
|
+
return;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Default-deny: every publish-request shape is validated, regardless of its
|
|
501
|
+
// declared schemaVersion. Keying this gate to an exact version string is what
|
|
502
|
+
// let `publish-request/v1` reach `initializeRun` with credential-bearing roots,
|
|
503
|
+
// and would let any future version reintroduce the same bypass.
|
|
504
|
+
if (kind.startsWith('publish-request')) {
|
|
505
|
+
try {
|
|
506
|
+
normalizePublishRoots(value.s3Uri, value.publicBaseUrl);
|
|
507
|
+
} catch {
|
|
508
|
+
add(
|
|
509
|
+
errors,
|
|
510
|
+
path,
|
|
511
|
+
'publish-roots',
|
|
512
|
+
'Publish request roots must be credential-free and canonical.',
|
|
513
|
+
);
|
|
514
|
+
}
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (kind.startsWith('publish-receipt')) {
|
|
519
|
+
// Same default-deny rule as the request branch above: every receipt shape
|
|
520
|
+
// has its roots screened, whatever version it declares. Pinning this to the
|
|
521
|
+
// exact v2 string let a `publish-receipt/v1` carry credential-bearing and
|
|
522
|
+
// internal-address roots straight through to `publish-receipt.json`, a
|
|
523
|
+
// retained hash-covered member of the run package, and on into the
|
|
524
|
+
// `publish-summary/v1` projection.
|
|
525
|
+
const receiptV2 = isVerifiablePublishReceipt(value);
|
|
526
|
+
let roots;
|
|
527
|
+
try {
|
|
528
|
+
roots = normalizePublishRoots(
|
|
529
|
+
value.roots?.s3Uri,
|
|
530
|
+
value.roots?.publicBaseUrl,
|
|
531
|
+
);
|
|
532
|
+
// The stricter *canonical-form* assertion stays v2-only: v1 is retained
|
|
533
|
+
// purely for replay and may legitimately carry non-canonical historical
|
|
534
|
+
// forms such as trailing slashes. Credential and address screening above
|
|
535
|
+
// applies to both, because no replay need justifies either.
|
|
536
|
+
if (
|
|
537
|
+
receiptV2 &&
|
|
538
|
+
(roots.s3Uri !== value.roots.s3Uri ||
|
|
539
|
+
roots.publicBaseUrl !== value.roots.publicBaseUrl)
|
|
540
|
+
) {
|
|
541
|
+
throw new Error('noncanonical');
|
|
542
|
+
}
|
|
543
|
+
} catch {
|
|
544
|
+
add(
|
|
545
|
+
errors,
|
|
546
|
+
`${path}.roots`,
|
|
547
|
+
'publish-roots',
|
|
548
|
+
'Publish receipt roots must be credential-free and canonical.',
|
|
549
|
+
);
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (!receiptV2) return;
|
|
554
|
+
|
|
555
|
+
for (const [index, artifact] of (Array.isArray(value.artifacts)
|
|
556
|
+
? value.artifacts
|
|
557
|
+
: []
|
|
558
|
+
).entries()) {
|
|
559
|
+
if (!isObject(artifact) || typeof artifact.relativePath !== 'string') {
|
|
560
|
+
continue;
|
|
561
|
+
}
|
|
562
|
+
const publishPath = artifact.relativePath.startsWith('site/')
|
|
563
|
+
? artifact.relativePath.slice('site/'.length)
|
|
564
|
+
: artifact.relativePath;
|
|
565
|
+
try {
|
|
566
|
+
const expected = composePublicationTarget(publishPath, roots);
|
|
567
|
+
if (
|
|
568
|
+
artifact.s3Uri !== expected.s3Uri ||
|
|
569
|
+
artifact.publicUrl !== expected.publicUrl
|
|
570
|
+
) {
|
|
571
|
+
throw new Error('destination mismatch');
|
|
572
|
+
}
|
|
573
|
+
} catch {
|
|
574
|
+
add(
|
|
575
|
+
errors,
|
|
576
|
+
`${path}.artifacts[${index}]`,
|
|
577
|
+
'receipt-artifact-destination',
|
|
578
|
+
'Publish receipt artifact destinations must be canonical children of the validated roots.',
|
|
579
|
+
);
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
441
585
|
function addLexicalPathErrors(value, path, errors, allowAbsolute) {
|
|
442
586
|
if (value === undefined) {
|
|
443
587
|
return;
|
|
@@ -547,12 +691,20 @@ function validateCrossRecord(kind, value, context, errors) {
|
|
|
547
691
|
}
|
|
548
692
|
|
|
549
693
|
if (
|
|
550
|
-
['author-request', 'author-request/v2'].includes(
|
|
551
|
-
|
|
694
|
+
['author-request', 'author-request/v2', 'author-request/v3'].includes(
|
|
695
|
+
kind,
|
|
696
|
+
) ||
|
|
697
|
+
[
|
|
698
|
+
'explainer-kit.author-request/v2',
|
|
699
|
+
'explainer-kit.author-request/v3',
|
|
700
|
+
].includes(value.schemaVersion)
|
|
552
701
|
) {
|
|
553
702
|
validateAuthorSetContext(value, errors);
|
|
554
703
|
validateVisualAuthoringGuidance(value, errors);
|
|
555
704
|
validateAuthorGraphSemantics(value, errors);
|
|
705
|
+
if (value.schemaVersion === 'explainer-kit.author-request/v3') {
|
|
706
|
+
validateAuthorArtifactLinks(value, errors);
|
|
707
|
+
}
|
|
556
708
|
}
|
|
557
709
|
|
|
558
710
|
if (kind === 'visual-review-request') {
|
|
@@ -787,6 +939,19 @@ function validateCrossRecord(kind, value, context, errors) {
|
|
|
787
939
|
}
|
|
788
940
|
}
|
|
789
941
|
|
|
942
|
+
if (kind === 'terminal-evidence') {
|
|
943
|
+
validateTerminalEvidence(value, context.manifest, errors);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
if (kind === 'visual-review-evidence') {
|
|
947
|
+
validateVisualReviewEvidence(
|
|
948
|
+
value,
|
|
949
|
+
context.visualReviewRequest,
|
|
950
|
+
context.attempt,
|
|
951
|
+
errors,
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
|
|
790
955
|
if (kind === 'manifest') {
|
|
791
956
|
const paths = [];
|
|
792
957
|
for (const artifact of Array.isArray(value.artifacts)
|
|
@@ -972,6 +1137,34 @@ function validateCrossRecord(kind, value, context, errors) {
|
|
|
972
1137
|
}
|
|
973
1138
|
}
|
|
974
1139
|
|
|
1140
|
+
if (kind.startsWith('publish-receipt') && isVerifiablePublishReceipt(value)) {
|
|
1141
|
+
const sentinel = value.sentinel;
|
|
1142
|
+
if (
|
|
1143
|
+
sentinel?.objectVerification?.status !== 'verified' ||
|
|
1144
|
+
sentinel.objectVerification.hash === undefined
|
|
1145
|
+
) {
|
|
1146
|
+
add(
|
|
1147
|
+
errors,
|
|
1148
|
+
'$.sentinel.objectVerification',
|
|
1149
|
+
'receipt-object-verification',
|
|
1150
|
+
'Publish receipt sentinel requires exact authenticated object verification.',
|
|
1151
|
+
);
|
|
1152
|
+
}
|
|
1153
|
+
const validPublic =
|
|
1154
|
+
value.publicAccess === 'public'
|
|
1155
|
+
? sentinel?.publicVerification?.status === 'verified' &&
|
|
1156
|
+
sentinel.publicVerification.hash === sentinel.objectVerification?.hash
|
|
1157
|
+
: sentinel?.publicVerification?.status === 'skipped-protected';
|
|
1158
|
+
if (!validPublic) {
|
|
1159
|
+
add(
|
|
1160
|
+
errors,
|
|
1161
|
+
'$.sentinel.publicVerification',
|
|
1162
|
+
'receipt-public-verification',
|
|
1163
|
+
'Publish receipt sentinel public verification must match the declared public-access mode.',
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
975
1168
|
if (
|
|
976
1169
|
kind === 'publish-receipt' &&
|
|
977
1170
|
isObject(context.manifest) &&
|
|
@@ -984,24 +1177,25 @@ function validateCrossRecord(kind, value, context, errors) {
|
|
|
984
1177
|
artifact.status === 'built' &&
|
|
985
1178
|
typeof artifact.renderedPath === 'string'
|
|
986
1179
|
) {
|
|
987
|
-
expected.set(artifact.renderedPath,
|
|
1180
|
+
expected.set(artifact.renderedPath, {
|
|
1181
|
+
hash: artifact.hash,
|
|
1182
|
+
source: { kind: 'manifest', artifactId: artifact.id },
|
|
1183
|
+
});
|
|
988
1184
|
}
|
|
989
1185
|
}
|
|
990
1186
|
if (
|
|
991
1187
|
isObject(context.catalogArtifact) &&
|
|
992
1188
|
typeof context.catalogArtifact.relativePath === 'string'
|
|
993
1189
|
) {
|
|
994
|
-
expected.set(
|
|
995
|
-
context.catalogArtifact.
|
|
996
|
-
|
|
997
|
-
);
|
|
1190
|
+
expected.set(context.catalogArtifact.relativePath, {
|
|
1191
|
+
hash: context.catalogArtifact.hash,
|
|
1192
|
+
source: { kind: 'auxiliary', name: 'catalog' },
|
|
1193
|
+
});
|
|
998
1194
|
}
|
|
999
1195
|
const received = new Set();
|
|
1000
1196
|
for (const artifact of value.artifacts) {
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
expected.get(artifact.relativePath) !== artifact.hash
|
|
1004
|
-
) {
|
|
1197
|
+
const expectedArtifact = expected.get(artifact?.relativePath);
|
|
1198
|
+
if (isObject(artifact) && expectedArtifact?.hash !== artifact.hash) {
|
|
1005
1199
|
add(
|
|
1006
1200
|
errors,
|
|
1007
1201
|
'$.artifacts',
|
|
@@ -1009,6 +1203,14 @@ function validateCrossRecord(kind, value, context, errors) {
|
|
|
1009
1203
|
'Publish receipt artifact does not match the manifest.',
|
|
1010
1204
|
);
|
|
1011
1205
|
}
|
|
1206
|
+
if (isVerifiablePublishReceipt(value) && isObject(artifact)) {
|
|
1207
|
+
validatePublishReceiptV2Artifact(
|
|
1208
|
+
value,
|
|
1209
|
+
artifact,
|
|
1210
|
+
expectedArtifact,
|
|
1211
|
+
errors,
|
|
1212
|
+
);
|
|
1213
|
+
}
|
|
1012
1214
|
if (received.has(artifact?.relativePath)) {
|
|
1013
1215
|
add(
|
|
1014
1216
|
errors,
|
|
@@ -1033,6 +1235,259 @@ function validateCrossRecord(kind, value, context, errors) {
|
|
|
1033
1235
|
}
|
|
1034
1236
|
}
|
|
1035
1237
|
|
|
1238
|
+
function validateTerminalEvidence(value, manifest, errors) {
|
|
1239
|
+
const reasons = Array.isArray(value.reasons) ? value.reasons : [];
|
|
1240
|
+
const artifactIds = isObject(manifest)
|
|
1241
|
+
? new Set(
|
|
1242
|
+
(Array.isArray(manifest.artifacts) ? manifest.artifacts : []).map(
|
|
1243
|
+
({ id }) => id,
|
|
1244
|
+
),
|
|
1245
|
+
)
|
|
1246
|
+
: null;
|
|
1247
|
+
validateReasonSet(reasons, artifactIds, '$.reasons', errors);
|
|
1248
|
+
|
|
1249
|
+
if (isObject(manifest)) {
|
|
1250
|
+
if (value.runId !== manifest.runId) {
|
|
1251
|
+
add(
|
|
1252
|
+
errors,
|
|
1253
|
+
'$.runId',
|
|
1254
|
+
'terminal-run-mismatch',
|
|
1255
|
+
'Terminal evidence run identity must match its manifest.',
|
|
1256
|
+
);
|
|
1257
|
+
}
|
|
1258
|
+
if (value.outcome !== manifest.outcome) {
|
|
1259
|
+
add(
|
|
1260
|
+
errors,
|
|
1261
|
+
'$.outcome',
|
|
1262
|
+
'terminal-outcome-mismatch',
|
|
1263
|
+
'Terminal evidence outcome must match its manifest.',
|
|
1264
|
+
);
|
|
1265
|
+
}
|
|
1266
|
+
if (value.manifestHash !== canonicalHash(manifest)) {
|
|
1267
|
+
add(
|
|
1268
|
+
errors,
|
|
1269
|
+
'$.manifestHash',
|
|
1270
|
+
'terminal-manifest-mismatch',
|
|
1271
|
+
'Terminal evidence manifest hash must match its manifest.',
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
if (value.evidenceDisposition === 'superseded') {
|
|
1277
|
+
const reason = reasons[0];
|
|
1278
|
+
if (
|
|
1279
|
+
reasons.length !== 1 ||
|
|
1280
|
+
reason?.stage !== 'finalization' ||
|
|
1281
|
+
reason?.kind !== 'superseded' ||
|
|
1282
|
+
reason?.count !== 1 ||
|
|
1283
|
+
reason?.artifactId !== undefined
|
|
1284
|
+
) {
|
|
1285
|
+
add(
|
|
1286
|
+
errors,
|
|
1287
|
+
'$.reasons',
|
|
1288
|
+
'supersession-reason',
|
|
1289
|
+
'Superseded evidence requires exactly one count-1 finalization/superseded reason without an artifact ID.',
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
if (
|
|
1293
|
+
!isObject(value.supersededBy) ||
|
|
1294
|
+
value.supersededBy.runId === value.runId
|
|
1295
|
+
) {
|
|
1296
|
+
add(
|
|
1297
|
+
errors,
|
|
1298
|
+
'$.supersededBy',
|
|
1299
|
+
'supersession-binding',
|
|
1300
|
+
'Superseded evidence requires a distinct replacement run identity.',
|
|
1301
|
+
);
|
|
1302
|
+
}
|
|
1303
|
+
if (typeof value.manifestHash !== 'string') {
|
|
1304
|
+
add(
|
|
1305
|
+
errors,
|
|
1306
|
+
'$.manifestHash',
|
|
1307
|
+
'supersession-manifest',
|
|
1308
|
+
'Superseded evidence requires the original manifest hash.',
|
|
1309
|
+
);
|
|
1310
|
+
}
|
|
1311
|
+
return;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
if (value.supersededBy !== undefined) {
|
|
1315
|
+
add(
|
|
1316
|
+
errors,
|
|
1317
|
+
'$.supersededBy',
|
|
1318
|
+
'unexpected-supersession',
|
|
1319
|
+
'Only superseded evidence may identify a replacement run.',
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
if (reasons.some((reason) => reason?.kind === 'superseded')) {
|
|
1323
|
+
add(
|
|
1324
|
+
errors,
|
|
1325
|
+
'$.reasons',
|
|
1326
|
+
'unexpected-supersession',
|
|
1327
|
+
'Only superseded evidence may contain a superseded reason.',
|
|
1328
|
+
);
|
|
1329
|
+
}
|
|
1330
|
+
const allowedKinds =
|
|
1331
|
+
value.outcome === 'failed'
|
|
1332
|
+
? new Set(['provider-failure', 'pipeline-failure'])
|
|
1333
|
+
: new Set(['finding', 'provider-failure', 'pipeline-failure']);
|
|
1334
|
+
if (!reasons.some((reason) => allowedKinds.has(reason?.kind))) {
|
|
1335
|
+
add(
|
|
1336
|
+
errors,
|
|
1337
|
+
'$.reasons',
|
|
1338
|
+
'terminal-outcome-reasons',
|
|
1339
|
+
'Terminal evidence reasons do not satisfy the terminal outcome.',
|
|
1340
|
+
);
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
function validateVisualReviewEvidence(value, request, attempt, errors) {
|
|
1345
|
+
if (!isObject(request)) {
|
|
1346
|
+
add(
|
|
1347
|
+
errors,
|
|
1348
|
+
'$',
|
|
1349
|
+
'review-request-required',
|
|
1350
|
+
'Retained visual evidence requires its adjacent reviewed request.',
|
|
1351
|
+
);
|
|
1352
|
+
return;
|
|
1353
|
+
}
|
|
1354
|
+
const requestValidation = validateContract('visual-review-request', request);
|
|
1355
|
+
if (!requestValidation.valid) {
|
|
1356
|
+
add(
|
|
1357
|
+
errors,
|
|
1358
|
+
'$',
|
|
1359
|
+
'invalid-review-request',
|
|
1360
|
+
'Retained visual evidence cannot bind to an invalid reviewed request.',
|
|
1361
|
+
);
|
|
1362
|
+
}
|
|
1363
|
+
if (value.requestHash !== request.requestHash) {
|
|
1364
|
+
add(
|
|
1365
|
+
errors,
|
|
1366
|
+
'$.requestHash',
|
|
1367
|
+
'review-binding-mismatch',
|
|
1368
|
+
'Retained visual evidence must bind the adjacent request hash.',
|
|
1369
|
+
);
|
|
1370
|
+
}
|
|
1371
|
+
if (![1, 2].includes(attempt) || value.attempt !== attempt) {
|
|
1372
|
+
add(
|
|
1373
|
+
errors,
|
|
1374
|
+
'$.attempt',
|
|
1375
|
+
'review-attempt-mismatch',
|
|
1376
|
+
'Retained visual evidence attempt must match its attempt directory.',
|
|
1377
|
+
);
|
|
1378
|
+
}
|
|
1379
|
+
|
|
1380
|
+
const reasons = Array.isArray(value.reasons) ? value.reasons : [];
|
|
1381
|
+
const artifactIds = new Set(
|
|
1382
|
+
(Array.isArray(request.renderedArtifacts)
|
|
1383
|
+
? request.renderedArtifacts
|
|
1384
|
+
: []
|
|
1385
|
+
).map(({ artifactId }) => artifactId),
|
|
1386
|
+
);
|
|
1387
|
+
validateReasonSet(reasons, artifactIds, '$.reasons', errors);
|
|
1388
|
+
if (reasons.some((reason) => reason?.stage !== 'visual-review')) {
|
|
1389
|
+
add(
|
|
1390
|
+
errors,
|
|
1391
|
+
'$.reasons',
|
|
1392
|
+
'visual-reason-stage',
|
|
1393
|
+
'Retained visual evidence reasons must use the visual-review stage.',
|
|
1394
|
+
);
|
|
1395
|
+
}
|
|
1396
|
+
const validDisposition =
|
|
1397
|
+
(value.disposition === 'pass' && reasons.length === 0) ||
|
|
1398
|
+
(value.disposition === 'correct' &&
|
|
1399
|
+
reasons.length > 0 &&
|
|
1400
|
+
reasons.every((reason) => reason?.kind === 'finding')) ||
|
|
1401
|
+
(value.disposition === 'failed' &&
|
|
1402
|
+
reasons.length > 0 &&
|
|
1403
|
+
reasons.every((reason) =>
|
|
1404
|
+
['provider-failure', 'pipeline-failure'].includes(reason?.kind),
|
|
1405
|
+
));
|
|
1406
|
+
if (!validDisposition) {
|
|
1407
|
+
add(
|
|
1408
|
+
errors,
|
|
1409
|
+
'$.reasons',
|
|
1410
|
+
'visual-disposition-reasons',
|
|
1411
|
+
'Retained visual evidence reasons do not satisfy its disposition.',
|
|
1412
|
+
);
|
|
1413
|
+
}
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
function validateReasonSet(reasons, artifactIds, path, errors) {
|
|
1417
|
+
let total = 0;
|
|
1418
|
+
const tuples = new Set();
|
|
1419
|
+
for (const [index, reason] of reasons.entries()) {
|
|
1420
|
+
if (!isObject(reason)) continue;
|
|
1421
|
+
if (Number.isInteger(reason.count)) total += reason.count;
|
|
1422
|
+
const tuple = `${reason.stage}\0${reason.kind}\0${reason.artifactId ?? ''}`;
|
|
1423
|
+
if (tuples.has(tuple)) {
|
|
1424
|
+
add(
|
|
1425
|
+
errors,
|
|
1426
|
+
`${path}[${index}]`,
|
|
1427
|
+
'duplicate-reason',
|
|
1428
|
+
'Retained reason tuples must be unique.',
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
1431
|
+
tuples.add(tuple);
|
|
1432
|
+
if (
|
|
1433
|
+
reason.artifactId !== undefined &&
|
|
1434
|
+
artifactIds &&
|
|
1435
|
+
!artifactIds.has(reason.artifactId)
|
|
1436
|
+
) {
|
|
1437
|
+
add(
|
|
1438
|
+
errors,
|
|
1439
|
+
`${path}[${index}].artifactId`,
|
|
1440
|
+
'foreign-artifact',
|
|
1441
|
+
'Retained reason artifact IDs must belong to the bound run.',
|
|
1442
|
+
);
|
|
1443
|
+
}
|
|
1444
|
+
}
|
|
1445
|
+
if (total > 50) {
|
|
1446
|
+
add(
|
|
1447
|
+
errors,
|
|
1448
|
+
path,
|
|
1449
|
+
'reason-total',
|
|
1450
|
+
'Retained reason counts must total at most 50.',
|
|
1451
|
+
);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
function validatePublishReceiptV2Artifact(receipt, artifact, expected, errors) {
|
|
1456
|
+
if (!expected || !deepEqual(artifact.source, expected.source)) {
|
|
1457
|
+
add(
|
|
1458
|
+
errors,
|
|
1459
|
+
'$.artifacts',
|
|
1460
|
+
'receipt-artifact-source',
|
|
1461
|
+
'Publish receipt source identity does not match the manifest or generated auxiliary object.',
|
|
1462
|
+
);
|
|
1463
|
+
}
|
|
1464
|
+
if (
|
|
1465
|
+
artifact.objectVerification?.status !== 'verified' ||
|
|
1466
|
+
artifact.objectVerification?.hash !== artifact.hash
|
|
1467
|
+
) {
|
|
1468
|
+
add(
|
|
1469
|
+
errors,
|
|
1470
|
+
'$.artifacts',
|
|
1471
|
+
'receipt-object-verification',
|
|
1472
|
+
'Publish receipt object verification must prove the exact artifact hash.',
|
|
1473
|
+
);
|
|
1474
|
+
}
|
|
1475
|
+
const publicVerification = artifact.publicVerification;
|
|
1476
|
+
const validPublic =
|
|
1477
|
+
receipt.publicAccess === 'public'
|
|
1478
|
+
? publicVerification?.status === 'verified' &&
|
|
1479
|
+
publicVerification?.hash === artifact.hash
|
|
1480
|
+
: publicVerification?.status === 'skipped-protected';
|
|
1481
|
+
if (!validPublic) {
|
|
1482
|
+
add(
|
|
1483
|
+
errors,
|
|
1484
|
+
'$.artifacts',
|
|
1485
|
+
'receipt-public-verification',
|
|
1486
|
+
'Publish receipt public verification must match the declared public-access mode.',
|
|
1487
|
+
);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1036
1491
|
export function visualReviewRequestPayload(request) {
|
|
1037
1492
|
const {
|
|
1038
1493
|
requestId: _requestId,
|
|
@@ -1175,6 +1630,84 @@ function validateAuthorSetContext(value, errors) {
|
|
|
1175
1630
|
}
|
|
1176
1631
|
}
|
|
1177
1632
|
|
|
1633
|
+
function validateAuthorArtifactLinks(value, errors) {
|
|
1634
|
+
if (!Array.isArray(value.artifactLinks) || !isObject(value.setContext)) {
|
|
1635
|
+
return;
|
|
1636
|
+
}
|
|
1637
|
+
const planned = Array.isArray(value.setContext.portfolio)
|
|
1638
|
+
? value.setContext.portfolio
|
|
1639
|
+
: [];
|
|
1640
|
+
const linksById = new Map();
|
|
1641
|
+
for (const [index, link] of value.artifactLinks.entries()) {
|
|
1642
|
+
if (!isObject(link)) continue;
|
|
1643
|
+
if (linksById.has(link.artifactId)) {
|
|
1644
|
+
add(
|
|
1645
|
+
errors,
|
|
1646
|
+
`$.artifactLinks[${index}].artifactId`,
|
|
1647
|
+
'artifact-link-parity',
|
|
1648
|
+
'Canonical artifact link IDs must be unique.',
|
|
1649
|
+
);
|
|
1650
|
+
}
|
|
1651
|
+
linksById.set(link.artifactId, link);
|
|
1652
|
+
}
|
|
1653
|
+
if (
|
|
1654
|
+
linksById.size !== planned.length ||
|
|
1655
|
+
planned.some(
|
|
1656
|
+
({ artifactId, artifactType }) =>
|
|
1657
|
+
linksById.get(artifactId)?.artifactType !== artifactType,
|
|
1658
|
+
)
|
|
1659
|
+
) {
|
|
1660
|
+
add(
|
|
1661
|
+
errors,
|
|
1662
|
+
'$.artifactLinks',
|
|
1663
|
+
'artifact-link-parity',
|
|
1664
|
+
'Canonical artifact links must exactly cover the planned portfolio.',
|
|
1665
|
+
);
|
|
1666
|
+
return;
|
|
1667
|
+
}
|
|
1668
|
+
|
|
1669
|
+
const current = linksById.get(value.artifactId);
|
|
1670
|
+
if (!current || typeof current.sitePath !== 'string') return;
|
|
1671
|
+
const base = new URL(current.sitePath, 'https://explainer.invalid/');
|
|
1672
|
+
for (const [index, link] of value.artifactLinks.entries()) {
|
|
1673
|
+
if (
|
|
1674
|
+
!isObject(link) ||
|
|
1675
|
+
typeof link.href !== 'string' ||
|
|
1676
|
+
typeof link.sitePath !== 'string'
|
|
1677
|
+
) {
|
|
1678
|
+
continue;
|
|
1679
|
+
}
|
|
1680
|
+
let resolved;
|
|
1681
|
+
try {
|
|
1682
|
+
resolved = new URL(link.href, base);
|
|
1683
|
+
} catch {
|
|
1684
|
+
add(
|
|
1685
|
+
errors,
|
|
1686
|
+
`$.artifactLinks[${index}].href`,
|
|
1687
|
+
'artifact-link-resolution',
|
|
1688
|
+
'Canonical artifact href must be a valid relative reference.',
|
|
1689
|
+
);
|
|
1690
|
+
continue;
|
|
1691
|
+
}
|
|
1692
|
+
if (
|
|
1693
|
+
resolved.origin !== base.origin ||
|
|
1694
|
+
resolved.protocol !== 'https:' ||
|
|
1695
|
+
resolved.username ||
|
|
1696
|
+
resolved.password ||
|
|
1697
|
+
resolved.search ||
|
|
1698
|
+
resolved.hash ||
|
|
1699
|
+
resolved.pathname !== `/${link.sitePath}`
|
|
1700
|
+
) {
|
|
1701
|
+
add(
|
|
1702
|
+
errors,
|
|
1703
|
+
`$.artifactLinks[${index}].href`,
|
|
1704
|
+
'artifact-link-resolution',
|
|
1705
|
+
'Canonical artifact href must resolve exactly to its declared site path.',
|
|
1706
|
+
);
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
}
|
|
1710
|
+
|
|
1178
1711
|
function validateVisualAuthoringGuidance(value, errors) {
|
|
1179
1712
|
if (typeof value.visualAuthoringGuidance !== 'string') {
|
|
1180
1713
|
return;
|