@ai-sdlc/orchestrator 0.10.0 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters.d.ts +42 -3
- package/dist/adapters.js +133 -3
- package/dist/admission-composite.d.ts +112 -1
- package/dist/admission-composite.js +85 -4
- package/dist/admission-enrichment.d.ts +12 -3
- package/dist/admission-enrichment.js +64 -13
- package/dist/admission-score.d.ts +30 -0
- package/dist/admission-score.js +4 -1
- package/dist/analysis/file-walker.js +5 -0
- package/dist/backlog-adapter.d.ts +45 -0
- package/dist/backlog-adapter.js +145 -1
- package/dist/calibration.d.ts +81 -0
- package/dist/calibration.js +76 -0
- package/dist/cli/commands/git-remote.js +1 -1
- package/dist/cli/commands/init-features.d.ts +301 -2
- package/dist/cli/commands/init-features.js +647 -7
- package/dist/cli/commands/init-templates.d.ts +198 -1
- package/dist/cli/commands/init-templates.js +943 -1
- package/dist/cli/commands/init.d.ts +45 -0
- package/dist/cli/commands/init.js +147 -5
- package/dist/cli/commands/run.js +9 -1
- package/dist/cli/index.d.ts +11 -0
- package/dist/cli/index.js +42 -8
- package/dist/compliance/composer.d.ts +79 -0
- package/dist/compliance/composer.js +258 -0
- package/dist/compliance/errors.d.ts +64 -0
- package/dist/compliance/errors.js +85 -0
- package/dist/compliance/loader.d.ts +52 -0
- package/dist/compliance/loader.js +124 -0
- package/dist/compliance/types.d.ts +184 -0
- package/dist/compliance/types.js +41 -0
- package/dist/compliance-clearance.d.ts +269 -0
- package/dist/compliance-clearance.js +269 -0
- package/dist/config.js +17 -0
- package/dist/cost-tracker.d.ts +22 -0
- package/dist/cost-tracker.js +41 -0
- package/dist/cycle-utils.js +14 -1
- package/dist/database/adapters/external.js +5 -1
- package/dist/embedding/adapters/openai-text-embedding-3-small.d.ts +71 -0
- package/dist/embedding/adapters/openai-text-embedding-3-small.js +190 -0
- package/dist/embedding/consumers/tessellation-drift.d.ts +74 -0
- package/dist/embedding/consumers/tessellation-drift.js +76 -0
- package/dist/embedding/cross-provider.d.ts +78 -0
- package/dist/embedding/cross-provider.js +75 -0
- package/dist/embedding/deprecation.d.ts +151 -0
- package/dist/embedding/deprecation.js +229 -0
- package/dist/embedding/errors.d.ts +90 -0
- package/dist/embedding/errors.js +150 -0
- package/dist/embedding/index.d.ts +29 -0
- package/dist/embedding/index.js +24 -0
- package/dist/embedding/pipeline-load.d.ts +146 -0
- package/dist/embedding/pipeline-load.js +178 -0
- package/dist/embedding/registry.d.ts +45 -0
- package/dist/embedding/registry.js +61 -0
- package/dist/embedding/stale-vector.d.ts +110 -0
- package/dist/embedding/stale-vector.js +92 -0
- package/dist/embedding/storage/index.d.ts +51 -0
- package/dist/embedding/storage/index.js +43 -0
- package/dist/embedding/storage/jsonl-backend.d.ts +150 -0
- package/dist/embedding/storage/jsonl-backend.js +332 -0
- package/dist/embedding/storage/types.d.ts +135 -0
- package/dist/embedding/storage/types.js +13 -0
- package/dist/embedding/types.d.ts +180 -0
- package/dist/embedding/types.js +10 -0
- package/dist/execute.d.ts +29 -2
- package/dist/execute.js +200 -38
- package/dist/fix-review.js +1 -1
- package/dist/index.d.ts +14 -2
- package/dist/index.js +19 -2
- package/dist/journey/inheritance-validator.d.ts +396 -0
- package/dist/journey/inheritance-validator.js +370 -0
- package/dist/journey/metric-snapshot.d.ts +403 -0
- package/dist/journey/metric-snapshot.js +370 -0
- package/dist/journey/state-id-drift-rule.d.ts +137 -0
- package/dist/journey/state-id-drift-rule.js +245 -0
- package/dist/journey-sa2-router.d.ts +395 -0
- package/dist/journey-sa2-router.js +308 -0
- package/dist/otel-exporter.js +0 -3
- package/dist/runners/claude-code.js +3 -3
- package/dist/runners/review-agent.js +6 -2
- package/dist/runners/runner-registry.d.ts +36 -0
- package/dist/runners/runner-registry.js +90 -0
- package/dist/runtime/attestations.d.ts +173 -13
- package/dist/runtime/attestations.js +265 -43
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +1 -1
- package/dist/sa-scoring/exemplar-bank.js +3 -1
- package/dist/sa-scoring/layer3-llm.js +6 -1
- package/dist/sa-scoring/revision-proposal-config.d.ts +178 -0
- package/dist/sa-scoring/revision-proposal-config.js +198 -0
- package/dist/sa-scoring/revision-proposal.d.ts +285 -0
- package/dist/sa-scoring/revision-proposal.js +417 -0
- package/dist/shared.d.ts +28 -0
- package/dist/shared.js +37 -0
- package/dist/signal-ingestion/adapters/community-thread.d.ts +43 -0
- package/dist/signal-ingestion/adapters/community-thread.js +55 -0
- package/dist/signal-ingestion/adapters/in-app-feedback.d.ts +67 -0
- package/dist/signal-ingestion/adapters/in-app-feedback.js +51 -0
- package/dist/signal-ingestion/adapters/manual.d.ts +78 -0
- package/dist/signal-ingestion/adapters/manual.js +112 -0
- package/dist/signal-ingestion/adapters/support-ticket.d.ts +47 -0
- package/dist/signal-ingestion/adapters/support-ticket.js +51 -0
- package/dist/signal-ingestion/classifier.d.ts +205 -0
- package/dist/signal-ingestion/classifier.js +494 -0
- package/dist/signal-ingestion/clustering-types.d.ts +36 -0
- package/dist/signal-ingestion/clustering-types.js +14 -0
- package/dist/signal-ingestion/clustering.d.ts +200 -0
- package/dist/signal-ingestion/clustering.js +413 -0
- package/dist/signal-ingestion/config.d.ts +351 -0
- package/dist/signal-ingestion/config.js +587 -0
- package/dist/signal-ingestion/d1.d.ts +252 -0
- package/dist/signal-ingestion/d1.js +235 -0
- package/dist/signal-ingestion/errors.d.ts +73 -0
- package/dist/signal-ingestion/errors.js +108 -0
- package/dist/signal-ingestion/governance-events.d.ts +181 -0
- package/dist/signal-ingestion/governance-events.js +189 -0
- package/dist/signal-ingestion/index.d.ts +35 -0
- package/dist/signal-ingestion/index.js +53 -0
- package/dist/signal-ingestion/manual-share-metric.d.ts +93 -0
- package/dist/signal-ingestion/manual-share-metric.js +106 -0
- package/dist/signal-ingestion/registry.d.ts +40 -0
- package/dist/signal-ingestion/registry.js +137 -0
- package/dist/signal-ingestion/residency.d.ts +227 -0
- package/dist/signal-ingestion/residency.js +238 -0
- package/dist/signal-ingestion/significance.d.ts +554 -0
- package/dist/signal-ingestion/significance.js +555 -0
- package/dist/signal-ingestion/types.d.ts +191 -0
- package/dist/signal-ingestion/types.js +8 -0
- package/dist/substrate/drift-composition.d.ts +270 -0
- package/dist/substrate/drift-composition.js +306 -0
- package/dist/substrate/drift-tui-surface.d.ts +61 -0
- package/dist/substrate/drift-tui-surface.js +102 -0
- package/dist/substrate/identity-class.d.ts +176 -0
- package/dist/substrate/identity-class.js +201 -0
- package/dist/tessellation/cross-soul-provenance-rule.d.ts +133 -0
- package/dist/tessellation/cross-soul-provenance-rule.js +171 -0
- package/dist/tessellation/inter-soul-embedding-distance-rule.d.ts +61 -0
- package/dist/tessellation/inter-soul-embedding-distance-rule.js +67 -0
- package/dist/tessellation/rule-registry.d.ts +269 -0
- package/dist/tessellation/rule-registry.js +92 -0
- package/dist/tessellation/soul-slug-ast-scan-rule.d.ts +90 -0
- package/dist/tessellation/soul-slug-ast-scan-rule.js +158 -0
- package/dist/tessellation-admission.d.ts +162 -0
- package/dist/tessellation-admission.js +146 -0
- package/dist/tessellation-drift.d.ts +246 -0
- package/dist/tessellation-drift.js +250 -0
- package/dist/validate-config.js +13 -0
- package/dist/validate-issue.js +2 -2
- package/dist/variant/cardinality-activation.d.ts +126 -0
- package/dist/variant/cardinality-activation.js +101 -0
- package/dist/variant/deprecation-lifecycle.d.ts +184 -0
- package/dist/variant/deprecation-lifecycle.js +208 -0
- package/dist/variant/drift-extension.d.ts +136 -0
- package/dist/variant/drift-extension.js +164 -0
- package/dist/variant/engineering-review.d.ts +185 -0
- package/dist/variant/engineering-review.js +142 -0
- package/dist/variant/index.d.ts +32 -0
- package/dist/variant/index.js +32 -0
- package/dist/variant/inheritance-validator.d.ts +165 -0
- package/dist/variant/inheritance-validator.js +139 -0
- package/dist/variant/internal-adopter/index.d.ts +11 -0
- package/dist/variant/internal-adopter/index.js +10 -0
- package/dist/variant/internal-adopter/products.d.ts +156 -0
- package/dist/variant/internal-adopter/products.js +366 -0
- package/dist/variant-admission.d.ts +316 -0
- package/dist/variant-admission.js +247 -0
- package/dist/webhook-manager.js +7 -1
- package/package.json +15 -12
|
@@ -47,9 +47,14 @@
|
|
|
47
47
|
* a v3 envelope smuggling either field is rejected by
|
|
48
48
|
* `validatePredicateShape`).
|
|
49
49
|
*
|
|
50
|
+
* AISDLC-362 (contentHashV5): adds `'v5'` to the allowlist. v5 envelopes
|
|
51
|
+
* carry `contentHashV5` AND `signedMergeBase` in addition to v3+v4 hashes
|
|
52
|
+
* (backward-compat dual-write). The schemaVersion field is bumped to `'v5'`
|
|
53
|
+
* on new envelopes so verifiers can detect the v5 fast-path immediately.
|
|
54
|
+
*
|
|
50
55
|
* Exported so the `verify-attestation` workflow can `import`/inline it.
|
|
51
56
|
*/
|
|
52
|
-
export declare const ACCEPTED_SCHEMA_VERSIONS: readonly ["v3"];
|
|
57
|
+
export declare const ACCEPTED_SCHEMA_VERSIONS: readonly ["v3", "v5"];
|
|
53
58
|
export type SchemaVersion = (typeof ACCEPTED_SCHEMA_VERSIONS)[number];
|
|
54
59
|
/**
|
|
55
60
|
* The DSSE PAE payload type for our predicate. DSSE spec mandates a payload
|
|
@@ -152,6 +157,45 @@ export interface AttestationPredicate {
|
|
|
152
157
|
* ancestor walk in that case.
|
|
153
158
|
*/
|
|
154
159
|
contentHashV4?: string;
|
|
160
|
+
/**
|
|
161
|
+
* Delta-hash with embedded frozen merge-base (AISDLC-362 — contentHashV5).
|
|
162
|
+
*
|
|
163
|
+
* SHA-256 of canonical JSON: `JSON.stringify({schemaVersion:'v5',
|
|
164
|
+
* signedMergeBase:'<40-char-sha>',files:[{path,blobSha}...]})` where:
|
|
165
|
+
* - `signedMergeBase` is `git merge-base origin/main HEAD` captured at
|
|
166
|
+
* sign time and embedded in BOTH the envelope predicate AND the v5 hash
|
|
167
|
+
* input so verifier can REPRODUCE the EXACT diff the signer used.
|
|
168
|
+
* - `files` is `git diff <signedMergeBase>..HEAD --name-only` with blob
|
|
169
|
+
* SHAs from HEAD — same approach as v4 `{path, headBlobSha}` but
|
|
170
|
+
* diffed against the FROZEN merge-base, not the moving `origin/main`.
|
|
171
|
+
*
|
|
172
|
+
* Why this is the ultimate fix:
|
|
173
|
+
* - v4 (`{path, headBlobSha}` from `origin/main..HEAD`) still invalidates
|
|
174
|
+
* when a sibling PR merges and its files overlap with our PR — the
|
|
175
|
+
* diff base moves from `merge-base-A` to `merge-base-B` and new files
|
|
176
|
+
* appear in the diff enumeration even though OUR blobs didn't change.
|
|
177
|
+
* - v5 FREEZES the diff base at `signedMergeBase`. On queue rebase:
|
|
178
|
+
* verifier uses `git diff <signedMergeBase>..<probe-HEAD>` which
|
|
179
|
+
* enumerates the SAME file set as the signer. Non-overlapping sibling
|
|
180
|
+
* merges don't add new files to the set → v5 matches.
|
|
181
|
+
* - Overlapping sibling merges (a sibling changed the SAME file our PR
|
|
182
|
+
* touches) → the head blob SHA of that file differs → v5 hash flips →
|
|
183
|
+
* verifier correctly rejects (operator must re-review).
|
|
184
|
+
*
|
|
185
|
+
* Optional (populated for v5 envelopes, absent for v3-only legacy envelopes).
|
|
186
|
+
* When present, the verifier prefers v5 over v4 over v3 (priority order).
|
|
187
|
+
*/
|
|
188
|
+
contentHashV5?: string;
|
|
189
|
+
/**
|
|
190
|
+
* The `git merge-base origin/main HEAD` SHA captured at sign time.
|
|
191
|
+
* Embedded in the envelope predicate so the verifier can reproduce the
|
|
192
|
+
* EXACT diff the signer used (`git diff <signedMergeBase>..<probe-HEAD>`).
|
|
193
|
+
*
|
|
194
|
+
* Only present when `contentHashV5` is populated (= v5 envelopes). Absent
|
|
195
|
+
* on legacy v3/v4 envelopes; the verifier falls back to v4 then v3 in
|
|
196
|
+
* those cases.
|
|
197
|
+
*/
|
|
198
|
+
signedMergeBase?: string;
|
|
155
199
|
/** sha256 of `.ai-sdlc/review-policy.md` at attestation time. */
|
|
156
200
|
policyHash: string;
|
|
157
201
|
/** Reviewer entries — typically 3 (code/test/security). */
|
|
@@ -218,7 +262,7 @@ export interface DsseSignature {
|
|
|
218
262
|
/**
|
|
219
263
|
* Identifier of the public key that produced this signature. Used to
|
|
220
264
|
* look up the trusted-reviewer entry. Free-form — typically `<identity>:
|
|
221
|
-
* <machine>` (e.g. `
|
|
265
|
+
* <machine>` (e.g. `contributor@example.com:laptop-2025`).
|
|
222
266
|
*/
|
|
223
267
|
keyid: string;
|
|
224
268
|
/** base64-encoded raw ed25519 signature (64 bytes → 88 chars b64). */
|
|
@@ -369,15 +413,15 @@ export declare const ATTESTATION_ENVELOPE_PATH_PATTERN: RegExp;
|
|
|
369
413
|
*/
|
|
370
414
|
export declare function isAttestationEnvelopePath(path: string): boolean;
|
|
371
415
|
/**
|
|
372
|
-
* The "shared churn" exclude list for
|
|
416
|
+
* The "shared churn" exclude list for content-hash computations (AISDLC-258,
|
|
417
|
+
* AISDLC-362). Applied to v3, v4, and v5 file collectors on BOTH the signer
|
|
418
|
+
* and verifier sides.
|
|
373
419
|
*
|
|
374
|
-
* Files in this list are EXCLUDED from
|
|
375
|
-
*
|
|
376
|
-
*
|
|
377
|
-
*
|
|
378
|
-
*
|
|
379
|
-
* `contentHashV4` to mismatch, so the operator is never asked to re-sign
|
|
380
|
-
* just because a shared tooling file was regenerated automatically.
|
|
420
|
+
* Files in this list are EXCLUDED from all file collectors. When a file
|
|
421
|
+
* appears in this list, changes to it after signing (e.g. from a merge-queue
|
|
422
|
+
* rebase that regenerated `pnpm-lock.yaml`) do NOT cause content-hash
|
|
423
|
+
* mismatches, so the operator is never asked to re-sign just because a shared
|
|
424
|
+
* tooling file was regenerated automatically.
|
|
381
425
|
*
|
|
382
426
|
* **Security trade-off (operator-approved, 2026-05-10):** An attacker
|
|
383
427
|
* COULD slip malicious changes through these files undetected (the
|
|
@@ -401,11 +445,18 @@ export declare function isAttestationEnvelopePath(path: string): boolean;
|
|
|
401
445
|
* Exported so `scripts/verify-attestation.mjs` can import it from the
|
|
402
446
|
* orchestrator barrel and apply the same exclusions on the verifier side.
|
|
403
447
|
*/
|
|
448
|
+
export declare const CONTENTHASH_SHARED_CHURN_FILES: readonly string[];
|
|
449
|
+
/**
|
|
450
|
+
* Backward-compatible alias for `CONTENTHASH_SHARED_CHURN_FILES` (renamed in
|
|
451
|
+
* AISDLC-362). Callers that imported the v4-specific name continue to work.
|
|
452
|
+
* @deprecated Use `CONTENTHASH_SHARED_CHURN_FILES` instead.
|
|
453
|
+
*/
|
|
404
454
|
export declare const CONTENTHASHV4_IGNORE_FILES: readonly string[];
|
|
405
455
|
/**
|
|
406
|
-
* Predicate to determine whether a file path should be excluded from
|
|
407
|
-
*
|
|
408
|
-
* `
|
|
456
|
+
* Predicate to determine whether a file path should be excluded from
|
|
457
|
+
* content-hash computations (v3/v4/v5) because it is a "shared churn" file
|
|
458
|
+
* (see `CONTENTHASH_SHARED_CHURN_FILES`). Defensive about backslash
|
|
459
|
+
* normalization.
|
|
409
460
|
*
|
|
410
461
|
* Note: this predicate is intentionally separate from
|
|
411
462
|
* `isAttestationEnvelopePath` because the two exclusions serve different
|
|
@@ -441,6 +492,88 @@ export interface ChangedFileHeadEntry {
|
|
|
441
492
|
path: string;
|
|
442
493
|
headBlobSha: string;
|
|
443
494
|
}
|
|
495
|
+
/**
|
|
496
|
+
* One entry in the v5 file set used by `computeContentHashV5` (AISDLC-362).
|
|
497
|
+
* Structurally identical to `ChangedFileHeadEntry` — `path` plus the HEAD
|
|
498
|
+
* blob SHA. The difference from v4 is that the FILE SET is enumerated via
|
|
499
|
+
* `git diff <signedMergeBase>..HEAD` (frozen diff base) instead of
|
|
500
|
+
* `git diff origin/main..HEAD` (moving diff base). Same fields, different
|
|
501
|
+
* enumeration semantics — separate type for clarity.
|
|
502
|
+
*/
|
|
503
|
+
export interface ChangedFileV5Entry {
|
|
504
|
+
path: string;
|
|
505
|
+
blobSha: string;
|
|
506
|
+
}
|
|
507
|
+
/**
|
|
508
|
+
* Return shape of `collectChangedFileEntriesForV5`. Bundles the frozen
|
|
509
|
+
* `signedMergeBase` SHA with the file entries so the signer can embed it in
|
|
510
|
+
* the predicate and the hash without a separate git call.
|
|
511
|
+
*/
|
|
512
|
+
export interface V5CollectResult {
|
|
513
|
+
/** Files changed between `signedMergeBase` and `headRef`. */
|
|
514
|
+
entries: ChangedFileV5Entry[];
|
|
515
|
+
/**
|
|
516
|
+
* The `git merge-base <baseRef> <headRef>` SHA captured at collection time.
|
|
517
|
+
* This is the FROZEN diff base embedded in the envelope predicate and the
|
|
518
|
+
* v5 hash. The verifier uses it to reproduce the exact file set the signer
|
|
519
|
+
* used: `git diff <signedMergeBase>..<probe-HEAD> --name-only`.
|
|
520
|
+
*/
|
|
521
|
+
signedMergeBase: string;
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Compute the rebase-stable `contentHashV5` (AISDLC-362) over a file set
|
|
525
|
+
* and a frozen merge-base SHA.
|
|
526
|
+
*
|
|
527
|
+
* Canonical form: `SHA-256(JSON.stringify({schemaVersion:'v5',
|
|
528
|
+
* signedMergeBase:'<40-char>', files:[{path,blobSha}...]}))` where `files`
|
|
529
|
+
* is sorted ascending by path.
|
|
530
|
+
*
|
|
531
|
+
* Why v5 beats v4 for rebase-stability:
|
|
532
|
+
* - v4 enumerates files via `git diff origin/main..HEAD`. When a sibling
|
|
533
|
+
* PR merges between sign-time and verify-time, `origin/main` moves
|
|
534
|
+
* forward. Files the sibling touched now appear in the v4 diff that
|
|
535
|
+
* weren't there at sign-time → v4 hash diverges even though OUR blobs
|
|
536
|
+
* are unchanged.
|
|
537
|
+
* - v5 enumerates files via `git diff <signedMergeBase>..HEAD`. The
|
|
538
|
+
* `signedMergeBase` is computed ONCE at sign time and frozen in the
|
|
539
|
+
* envelope. At verify time, the verifier recomputes
|
|
540
|
+
* `git diff <signedMergeBase>..<probe-HEAD>` using the SAME frozen
|
|
541
|
+
* base → same file set → same hash, regardless of how many sibling
|
|
542
|
+
* PRs merged on `main` in the interim.
|
|
543
|
+
* - Overlapping sibling merges (sibling changed a file OUR PR also touches)
|
|
544
|
+
* → the head blob SHA of that file differs from what we signed → v5
|
|
545
|
+
* hash flips → verifier correctly rejects (operator must re-review).
|
|
546
|
+
*
|
|
547
|
+
* Threat model preserved: any genuine post-sign content tampering flips the
|
|
548
|
+
* head blob SHA → v5 hash flips → verifier rejects.
|
|
549
|
+
*
|
|
550
|
+
* Pure function. Idempotent against double-enumeration via dedup-by-path.
|
|
551
|
+
*/
|
|
552
|
+
export declare function computeContentHashV5(entries: ChangedFileV5Entry[], signedMergeBase: string): string;
|
|
553
|
+
/**
|
|
554
|
+
* Collect the file set for `computeContentHashV5` (AISDLC-362).
|
|
555
|
+
*
|
|
556
|
+
* This is the LOAD-BEARING CHANGE vs v4:
|
|
557
|
+
* - v4: `git diff origin/main..HEAD` — the base moves as siblings merge.
|
|
558
|
+
* - v5: `git merge-base <baseRef> <headRef>` → frozen SHA, then
|
|
559
|
+
* `git diff <signedMergeBase>..HEAD` — the base is FROZEN.
|
|
560
|
+
*
|
|
561
|
+
* The frozen merge-base is returned alongside the entries so the signer can
|
|
562
|
+
* embed it in both the v5 hash and the predicate's `signedMergeBase` field.
|
|
563
|
+
*
|
|
564
|
+
* File enumeration applies the same exclusions as v3/v4:
|
|
565
|
+
* - Attestation envelope files (`ATTESTATION_ENVELOPE_PATH_PATTERN`)
|
|
566
|
+
* - Shared churn files (`CONTENTHASH_SHARED_CHURN_FILES`)
|
|
567
|
+
*
|
|
568
|
+
* Blob SHAs are resolved from `headRef` (= HEAD at sign time).
|
|
569
|
+
*
|
|
570
|
+
* @param repoRoot Absolute path to the git worktree root.
|
|
571
|
+
* @param baseRef Typically `'origin/main'`. Used ONLY for computing the
|
|
572
|
+
* merge-base — the diff itself is against the frozen SHA.
|
|
573
|
+
* @param headRef Typically `'HEAD'`.
|
|
574
|
+
* @param options Optional injection points for tests (stub `runGit`).
|
|
575
|
+
*/
|
|
576
|
+
export declare function collectChangedFileEntriesForV5(repoRoot: string, baseRef?: string, headRef?: string, options?: CollectChangedFileEntriesOptions): V5CollectResult;
|
|
444
577
|
/** Inputs for building an attestation predicate. */
|
|
445
578
|
export interface BuildPredicateInputs {
|
|
446
579
|
commitSha: string;
|
|
@@ -487,6 +620,19 @@ export interface BuildPredicateInputs {
|
|
|
487
620
|
* still verifiable).
|
|
488
621
|
*/
|
|
489
622
|
changedFileDeltas: ChangedFileDeltaEntry[];
|
|
623
|
+
/**
|
|
624
|
+
* v5 file entries from `collectChangedFileEntriesForV5` (AISDLC-362).
|
|
625
|
+
* Optional — when omitted, no `contentHashV5` or `signedMergeBase` fields
|
|
626
|
+
* are emitted in the predicate (= a legacy v3+v4 envelope). When provided
|
|
627
|
+
* alongside `v5MergeBase`, the predicate carries all three hashes (v3, v4,
|
|
628
|
+
* v5) for maximum backward + forward compatibility.
|
|
629
|
+
*/
|
|
630
|
+
v5Entries?: ChangedFileV5Entry[];
|
|
631
|
+
/**
|
|
632
|
+
* The frozen merge-base SHA from `collectChangedFileEntriesForV5`.
|
|
633
|
+
* Required when `v5Entries` is provided. Must be a 40-char hex SHA-1.
|
|
634
|
+
*/
|
|
635
|
+
v5MergeBase?: string;
|
|
490
636
|
}
|
|
491
637
|
/**
|
|
492
638
|
* Compute a sha256 hex digest. Single source of truth for the hashing
|
|
@@ -669,6 +815,10 @@ export declare function projectDeltaEntriesToHeadEntries(deltas: ChangedFileDelt
|
|
|
669
815
|
* MUST provide `changedFileDeltas` (use `[]` for no-op PRs); the legacy
|
|
670
816
|
* `diff` + `changedFiles` inputs were dropped along with the legacy
|
|
671
817
|
* `diffHash` + `contentHash` fields.
|
|
818
|
+
*
|
|
819
|
+
* AISDLC-362 (contentHashV5): when `v5Entries` + `v5MergeBase` are provided,
|
|
820
|
+
* also emits `contentHashV5` and `signedMergeBase` in the predicate and bumps
|
|
821
|
+
* `schemaVersion` to `'v5'`. The verifier prefers v5 when present.
|
|
672
822
|
*/
|
|
673
823
|
export declare function buildPredicate(inputs: BuildPredicateInputs): AttestationPredicate;
|
|
674
824
|
/**
|
|
@@ -738,6 +888,16 @@ export interface VerifyOptions {
|
|
|
738
888
|
* of whether `expected.contentHashV4` is supplied.
|
|
739
889
|
*/
|
|
740
890
|
contentHashV4?: string;
|
|
891
|
+
/**
|
|
892
|
+
* AISDLC-362 frozen-merge-base delta hash. Optional — callers that
|
|
893
|
+
* want v5-prefer behavior pass it; callers without v5 leave it
|
|
894
|
+
* undefined (fallback to v4 then v3).
|
|
895
|
+
*
|
|
896
|
+
* When BOTH this AND the envelope's `contentHashV5` are present,
|
|
897
|
+
* the verifier prefers v5 (highest rebase-stability). When v5
|
|
898
|
+
* matches, v4 and v3 are NOT consulted.
|
|
899
|
+
*/
|
|
900
|
+
contentHashV5?: string;
|
|
741
901
|
policyHash: string;
|
|
742
902
|
expectedAgentFileHashes: Record<string, string>;
|
|
743
903
|
};
|
|
@@ -50,9 +50,14 @@ import { cleanGitEnv } from './git-env.js';
|
|
|
50
50
|
* a v3 envelope smuggling either field is rejected by
|
|
51
51
|
* `validatePredicateShape`).
|
|
52
52
|
*
|
|
53
|
+
* AISDLC-362 (contentHashV5): adds `'v5'` to the allowlist. v5 envelopes
|
|
54
|
+
* carry `contentHashV5` AND `signedMergeBase` in addition to v3+v4 hashes
|
|
55
|
+
* (backward-compat dual-write). The schemaVersion field is bumped to `'v5'`
|
|
56
|
+
* on new envelopes so verifiers can detect the v5 fast-path immediately.
|
|
57
|
+
*
|
|
53
58
|
* Exported so the `verify-attestation` workflow can `import`/inline it.
|
|
54
59
|
*/
|
|
55
|
-
export const ACCEPTED_SCHEMA_VERSIONS = ['v3'];
|
|
60
|
+
export const ACCEPTED_SCHEMA_VERSIONS = ['v3', 'v5'];
|
|
56
61
|
/**
|
|
57
62
|
* The DSSE PAE payload type for our predicate. DSSE spec mandates a payload
|
|
58
63
|
* type URI — we use a project-controlled vendor URI rather than the
|
|
@@ -175,6 +180,23 @@ export function validatePredicateShape(parsed) {
|
|
|
175
180
|
return 'schema validation failed: contentHashV4 does not match pattern';
|
|
176
181
|
}
|
|
177
182
|
}
|
|
183
|
+
// contentHashV5 (AISDLC-362) — OPTIONAL. When present, MUST be a 64-char
|
|
184
|
+
// hex sha256. v5 envelopes also carry `signedMergeBase` (40-char SHA-1).
|
|
185
|
+
if (p['contentHashV5'] !== undefined) {
|
|
186
|
+
const ch5 = p['contentHashV5'];
|
|
187
|
+
if (typeof ch5 !== 'string' || !SHA256_HEX.test(ch5)) {
|
|
188
|
+
return 'schema validation failed: contentHashV5 does not match pattern';
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
// signedMergeBase (AISDLC-362) — OPTIONAL, accompanies contentHashV5.
|
|
192
|
+
// When present, MUST be a 40-char SHA-1 hex string (the frozen merge-base
|
|
193
|
+
// that the v5 diff was computed against at sign time).
|
|
194
|
+
if (p['signedMergeBase'] !== undefined) {
|
|
195
|
+
const smb = p['signedMergeBase'];
|
|
196
|
+
if (typeof smb !== 'string' || !SHA1_HEX.test(smb)) {
|
|
197
|
+
return 'schema validation failed: signedMergeBase does not match SHA-1 pattern';
|
|
198
|
+
}
|
|
199
|
+
}
|
|
178
200
|
// pluginVersion — short ID (no CR/LF, no `=`).
|
|
179
201
|
const pluginVersion = p['pluginVersion'];
|
|
180
202
|
if (typeof pluginVersion !== 'string' ||
|
|
@@ -361,15 +383,15 @@ export function isAttestationEnvelopePath(path) {
|
|
|
361
383
|
return ATTESTATION_ENVELOPE_PATH_PATTERN.test(normalized);
|
|
362
384
|
}
|
|
363
385
|
/**
|
|
364
|
-
* The "shared churn" exclude list for
|
|
386
|
+
* The "shared churn" exclude list for content-hash computations (AISDLC-258,
|
|
387
|
+
* AISDLC-362). Applied to v3, v4, and v5 file collectors on BOTH the signer
|
|
388
|
+
* and verifier sides.
|
|
365
389
|
*
|
|
366
|
-
* Files in this list are EXCLUDED from
|
|
367
|
-
*
|
|
368
|
-
*
|
|
369
|
-
*
|
|
370
|
-
*
|
|
371
|
-
* `contentHashV4` to mismatch, so the operator is never asked to re-sign
|
|
372
|
-
* just because a shared tooling file was regenerated automatically.
|
|
390
|
+
* Files in this list are EXCLUDED from all file collectors. When a file
|
|
391
|
+
* appears in this list, changes to it after signing (e.g. from a merge-queue
|
|
392
|
+
* rebase that regenerated `pnpm-lock.yaml`) do NOT cause content-hash
|
|
393
|
+
* mismatches, so the operator is never asked to re-sign just because a shared
|
|
394
|
+
* tooling file was regenerated automatically.
|
|
373
395
|
*
|
|
374
396
|
* **Security trade-off (operator-approved, 2026-05-10):** An attacker
|
|
375
397
|
* COULD slip malicious changes through these files undetected (the
|
|
@@ -393,22 +415,33 @@ export function isAttestationEnvelopePath(path) {
|
|
|
393
415
|
* Exported so `scripts/verify-attestation.mjs` can import it from the
|
|
394
416
|
* orchestrator barrel and apply the same exclusions on the verifier side.
|
|
395
417
|
*/
|
|
396
|
-
export const
|
|
418
|
+
export const CONTENTHASH_SHARED_CHURN_FILES = Object.freeze([
|
|
397
419
|
'pnpm-lock.yaml',
|
|
398
420
|
'CHANGELOG.md',
|
|
399
421
|
'pipeline-cli/CHANGELOG.md',
|
|
400
422
|
'orchestrator/CHANGELOG.md',
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
//
|
|
404
|
-
//
|
|
405
|
-
//
|
|
406
|
-
//
|
|
423
|
+
// AISDLC-342 — re-added after PR #498 was kicked from the merge queue 3+
|
|
424
|
+
// times in 2 hours with `contentHashV4 mismatch` triggered by sibling PRs
|
|
425
|
+
// touching this file. The earlier AISDLC-258 concern (attacker hand-edits
|
|
426
|
+
// the generated file to bypass attestation) is mitigated by keeping the
|
|
427
|
+
// SOURCE-of-truth in the hash: every byte in this file is derived from
|
|
428
|
+
// `spec/schemas/*.schema.json` via `pnpm build`, and those schema JSONs
|
|
429
|
+
// remain in v4/v5. An attacker who hand-edits generated-schemas.ts without
|
|
430
|
+
// also editing a source schema produces output that the next `pnpm build`
|
|
431
|
+
// regenerates away — the change is non-load-bearing.
|
|
432
|
+
'reference/src/core/generated-schemas.ts',
|
|
407
433
|
]);
|
|
408
434
|
/**
|
|
409
|
-
*
|
|
410
|
-
*
|
|
411
|
-
* `
|
|
435
|
+
* Backward-compatible alias for `CONTENTHASH_SHARED_CHURN_FILES` (renamed in
|
|
436
|
+
* AISDLC-362). Callers that imported the v4-specific name continue to work.
|
|
437
|
+
* @deprecated Use `CONTENTHASH_SHARED_CHURN_FILES` instead.
|
|
438
|
+
*/
|
|
439
|
+
export const CONTENTHASHV4_IGNORE_FILES = CONTENTHASH_SHARED_CHURN_FILES;
|
|
440
|
+
/**
|
|
441
|
+
* Predicate to determine whether a file path should be excluded from
|
|
442
|
+
* content-hash computations (v3/v4/v5) because it is a "shared churn" file
|
|
443
|
+
* (see `CONTENTHASH_SHARED_CHURN_FILES`). Defensive about backslash
|
|
444
|
+
* normalization.
|
|
412
445
|
*
|
|
413
446
|
* Note: this predicate is intentionally separate from
|
|
414
447
|
* `isAttestationEnvelopePath` because the two exclusions serve different
|
|
@@ -419,7 +452,171 @@ export function isIgnoredForContentHash(path) {
|
|
|
419
452
|
if (typeof path !== 'string')
|
|
420
453
|
return false;
|
|
421
454
|
const normalized = path.replace(/\\/g, '/');
|
|
422
|
-
return
|
|
455
|
+
return CONTENTHASH_SHARED_CHURN_FILES.includes(normalized);
|
|
456
|
+
}
|
|
457
|
+
/**
|
|
458
|
+
* Compute the rebase-stable `contentHashV5` (AISDLC-362) over a file set
|
|
459
|
+
* and a frozen merge-base SHA.
|
|
460
|
+
*
|
|
461
|
+
* Canonical form: `SHA-256(JSON.stringify({schemaVersion:'v5',
|
|
462
|
+
* signedMergeBase:'<40-char>', files:[{path,blobSha}...]}))` where `files`
|
|
463
|
+
* is sorted ascending by path.
|
|
464
|
+
*
|
|
465
|
+
* Why v5 beats v4 for rebase-stability:
|
|
466
|
+
* - v4 enumerates files via `git diff origin/main..HEAD`. When a sibling
|
|
467
|
+
* PR merges between sign-time and verify-time, `origin/main` moves
|
|
468
|
+
* forward. Files the sibling touched now appear in the v4 diff that
|
|
469
|
+
* weren't there at sign-time → v4 hash diverges even though OUR blobs
|
|
470
|
+
* are unchanged.
|
|
471
|
+
* - v5 enumerates files via `git diff <signedMergeBase>..HEAD`. The
|
|
472
|
+
* `signedMergeBase` is computed ONCE at sign time and frozen in the
|
|
473
|
+
* envelope. At verify time, the verifier recomputes
|
|
474
|
+
* `git diff <signedMergeBase>..<probe-HEAD>` using the SAME frozen
|
|
475
|
+
* base → same file set → same hash, regardless of how many sibling
|
|
476
|
+
* PRs merged on `main` in the interim.
|
|
477
|
+
* - Overlapping sibling merges (sibling changed a file OUR PR also touches)
|
|
478
|
+
* → the head blob SHA of that file differs from what we signed → v5
|
|
479
|
+
* hash flips → verifier correctly rejects (operator must re-review).
|
|
480
|
+
*
|
|
481
|
+
* Threat model preserved: any genuine post-sign content tampering flips the
|
|
482
|
+
* head blob SHA → v5 hash flips → verifier rejects.
|
|
483
|
+
*
|
|
484
|
+
* Pure function. Idempotent against double-enumeration via dedup-by-path.
|
|
485
|
+
*/
|
|
486
|
+
export function computeContentHashV5(entries, signedMergeBase) {
|
|
487
|
+
if (typeof signedMergeBase !== 'string' || !/^[0-9a-f]{40}$/i.test(signedMergeBase)) {
|
|
488
|
+
throw new Error(`computeContentHashV5: signedMergeBase must be a 40-char hex SHA-1, got ${JSON.stringify(signedMergeBase)}`);
|
|
489
|
+
}
|
|
490
|
+
// Dedup by path (last entry wins) — mirrors computeContentHashV4 and
|
|
491
|
+
// computeContentHashV3 for idempotency.
|
|
492
|
+
const byPath = new Map();
|
|
493
|
+
for (const e of entries) {
|
|
494
|
+
if (typeof e?.path !== 'string' || e.path.length === 0) {
|
|
495
|
+
throw new Error(`computeContentHashV5: entry path must be a non-empty string`);
|
|
496
|
+
}
|
|
497
|
+
if (typeof e.blobSha !== 'string') {
|
|
498
|
+
throw new Error(`computeContentHashV5: entry blobSha must be a string for path ${e.path}`);
|
|
499
|
+
}
|
|
500
|
+
// Reject path entries containing JSON-control characters.
|
|
501
|
+
if (e.path.includes('\t') || e.path.includes('\n')) {
|
|
502
|
+
throw new Error(`computeContentHashV5: entry path must not contain tab or newline characters (got ${JSON.stringify(e.path)})`);
|
|
503
|
+
}
|
|
504
|
+
const normalizedPath = e.path.replace(/\\/g, '/');
|
|
505
|
+
byPath.set(normalizedPath, e.blobSha.toLowerCase());
|
|
506
|
+
}
|
|
507
|
+
const sortedFiles = [...byPath.entries()]
|
|
508
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
509
|
+
.map(([path, blobSha]) => ({ path, blobSha }));
|
|
510
|
+
const canonical = JSON.stringify({
|
|
511
|
+
schemaVersion: 'v5',
|
|
512
|
+
signedMergeBase: signedMergeBase.toLowerCase(),
|
|
513
|
+
files: sortedFiles,
|
|
514
|
+
});
|
|
515
|
+
return sha256Hex(canonical);
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Collect the file set for `computeContentHashV5` (AISDLC-362).
|
|
519
|
+
*
|
|
520
|
+
* This is the LOAD-BEARING CHANGE vs v4:
|
|
521
|
+
* - v4: `git diff origin/main..HEAD` — the base moves as siblings merge.
|
|
522
|
+
* - v5: `git merge-base <baseRef> <headRef>` → frozen SHA, then
|
|
523
|
+
* `git diff <signedMergeBase>..HEAD` — the base is FROZEN.
|
|
524
|
+
*
|
|
525
|
+
* The frozen merge-base is returned alongside the entries so the signer can
|
|
526
|
+
* embed it in both the v5 hash and the predicate's `signedMergeBase` field.
|
|
527
|
+
*
|
|
528
|
+
* File enumeration applies the same exclusions as v3/v4:
|
|
529
|
+
* - Attestation envelope files (`ATTESTATION_ENVELOPE_PATH_PATTERN`)
|
|
530
|
+
* - Shared churn files (`CONTENTHASH_SHARED_CHURN_FILES`)
|
|
531
|
+
*
|
|
532
|
+
* Blob SHAs are resolved from `headRef` (= HEAD at sign time).
|
|
533
|
+
*
|
|
534
|
+
* @param repoRoot Absolute path to the git worktree root.
|
|
535
|
+
* @param baseRef Typically `'origin/main'`. Used ONLY for computing the
|
|
536
|
+
* merge-base — the diff itself is against the frozen SHA.
|
|
537
|
+
* @param headRef Typically `'HEAD'`.
|
|
538
|
+
* @param options Optional injection points for tests (stub `runGit`).
|
|
539
|
+
*/
|
|
540
|
+
export function collectChangedFileEntriesForV5(repoRoot, baseRef = 'origin/main', headRef = 'HEAD', options = {}) {
|
|
541
|
+
const runGit = options.runGit ??
|
|
542
|
+
((args, cwd) => execFileSync('git', args, {
|
|
543
|
+
cwd,
|
|
544
|
+
env: cleanGitEnv(),
|
|
545
|
+
encoding: 'utf-8',
|
|
546
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
547
|
+
}));
|
|
548
|
+
// Step 1: compute and FREEZE the merge-base. This is the key invariant:
|
|
549
|
+
// sign once, freeze the base, verify against the frozen base — NOT against
|
|
550
|
+
// the moving `origin/main`.
|
|
551
|
+
let signedMergeBase;
|
|
552
|
+
try {
|
|
553
|
+
signedMergeBase = runGit(['merge-base', baseRef, headRef], repoRoot).trim();
|
|
554
|
+
}
|
|
555
|
+
catch (err) {
|
|
556
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
557
|
+
throw new Error(`collectChangedFileEntriesForV5: git merge-base failed: ${msg}`, {
|
|
558
|
+
cause: err,
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
if (!/^[0-9a-f]{40}$/i.test(signedMergeBase)) {
|
|
562
|
+
throw new Error(`collectChangedFileEntriesForV5: git merge-base returned non-SHA output: ${JSON.stringify(signedMergeBase)}`);
|
|
563
|
+
}
|
|
564
|
+
// Step 2: enumerate files changed between the FROZEN merge-base and HEAD.
|
|
565
|
+
// Using two-dot range (`<signedMergeBase>..<headRef>`) — NOT three-dot —
|
|
566
|
+
// because the merge-base is already resolved. Three-dot would re-compute
|
|
567
|
+
// merge-base(mergeBase, headRef) = mergeBase itself, which is fine, but
|
|
568
|
+
// two-dot is more explicit and avoids any ambiguity.
|
|
569
|
+
let nameOnly;
|
|
570
|
+
try {
|
|
571
|
+
nameOnly = runGit([
|
|
572
|
+
'-c',
|
|
573
|
+
'core.quotepath=false',
|
|
574
|
+
'diff',
|
|
575
|
+
'--name-only',
|
|
576
|
+
'--no-renames',
|
|
577
|
+
`${signedMergeBase}..${headRef}`,
|
|
578
|
+
], repoRoot);
|
|
579
|
+
}
|
|
580
|
+
catch (err) {
|
|
581
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
582
|
+
throw new Error(`collectChangedFileEntriesForV5: git diff --name-only failed: ${msg}`, {
|
|
583
|
+
cause: err,
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
const paths = nameOnly.split('\n').filter((p) => p.length > 0);
|
|
587
|
+
const entries = [];
|
|
588
|
+
/**
|
|
589
|
+
* Resolve a file's blob SHA at `headRef` via `git ls-tree -r`. Returns the
|
|
590
|
+
* empty string when the path doesn't exist at the ref (= deleted file).
|
|
591
|
+
*/
|
|
592
|
+
const resolveBlobSha = (path) => {
|
|
593
|
+
try {
|
|
594
|
+
const lsOut = runGit(['-c', 'core.quotepath=false', 'ls-tree', '-r', headRef, '--', path], repoRoot);
|
|
595
|
+
const line = lsOut.split('\n').find((l) => l.length > 0);
|
|
596
|
+
if (line) {
|
|
597
|
+
const m = line.match(/^[0-9]+\s+blob\s+([0-9a-f]{40})\t/);
|
|
598
|
+
if (m)
|
|
599
|
+
return m[1];
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
catch {
|
|
603
|
+
// ls-tree failed → treat as deleted.
|
|
604
|
+
}
|
|
605
|
+
return '';
|
|
606
|
+
};
|
|
607
|
+
for (const path of paths) {
|
|
608
|
+
if (path.includes('\t') || path.includes('\n')) {
|
|
609
|
+
throw new Error(`collectChangedFileEntriesForV5: path must not contain tab or newline characters (got ${JSON.stringify(path)})`);
|
|
610
|
+
}
|
|
611
|
+
// Exclude the attestation envelope itself (chore-commit pattern).
|
|
612
|
+
if (isAttestationEnvelopePath(path))
|
|
613
|
+
continue;
|
|
614
|
+
// Exclude shared-churn files (same set as v3/v4).
|
|
615
|
+
if (isIgnoredForContentHash(path))
|
|
616
|
+
continue;
|
|
617
|
+
entries.push({ path, blobSha: resolveBlobSha(path) });
|
|
618
|
+
}
|
|
619
|
+
return { entries, signedMergeBase: signedMergeBase.toLowerCase() };
|
|
423
620
|
}
|
|
424
621
|
/**
|
|
425
622
|
* Compute a sha256 hex digest. Single source of truth for the hashing
|
|
@@ -529,7 +726,9 @@ export function collectChangedFileEntries(baseRef, headRef, repoRoot, options =
|
|
|
529
726
|
}
|
|
530
727
|
catch (err) {
|
|
531
728
|
const msg = err instanceof Error ? err.message : String(err);
|
|
532
|
-
throw new Error(`collectChangedFileEntries: git diff --name-only failed: ${msg}
|
|
729
|
+
throw new Error(`collectChangedFileEntries: git diff --name-only failed: ${msg}`, {
|
|
730
|
+
cause: err,
|
|
731
|
+
});
|
|
533
732
|
}
|
|
534
733
|
const paths = nameOnly.split('\n').filter((p) => p.length > 0);
|
|
535
734
|
const entries = [];
|
|
@@ -736,7 +935,9 @@ export function collectChangedFileDeltaEntries(baseRef, headRef, repoRoot, optio
|
|
|
736
935
|
}
|
|
737
936
|
catch (err) {
|
|
738
937
|
const msg = err instanceof Error ? err.message : String(err);
|
|
739
|
-
throw new Error(`collectChangedFileDeltaEntries: git merge-base failed: ${msg}
|
|
938
|
+
throw new Error(`collectChangedFileDeltaEntries: git merge-base failed: ${msg}`, {
|
|
939
|
+
cause: err,
|
|
940
|
+
});
|
|
740
941
|
}
|
|
741
942
|
if (!/^[0-9a-f]{40}$/.test(mergeBase)) {
|
|
742
943
|
throw new Error(`collectChangedFileDeltaEntries: git merge-base returned non-SHA output: ${JSON.stringify(mergeBase)}`);
|
|
@@ -754,7 +955,9 @@ export function collectChangedFileDeltaEntries(baseRef, headRef, repoRoot, optio
|
|
|
754
955
|
}
|
|
755
956
|
catch (err) {
|
|
756
957
|
const msg = err instanceof Error ? err.message : String(err);
|
|
757
|
-
throw new Error(`collectChangedFileDeltaEntries: git diff --name-only failed: ${msg}
|
|
958
|
+
throw new Error(`collectChangedFileDeltaEntries: git diff --name-only failed: ${msg}`, {
|
|
959
|
+
cause: err,
|
|
960
|
+
});
|
|
758
961
|
}
|
|
759
962
|
const paths = nameOnly.split('\n').filter((p) => p.length > 0);
|
|
760
963
|
const entries = [];
|
|
@@ -837,6 +1040,10 @@ export function projectDeltaEntriesToHeadEntries(deltas) {
|
|
|
837
1040
|
* MUST provide `changedFileDeltas` (use `[]` for no-op PRs); the legacy
|
|
838
1041
|
* `diff` + `changedFiles` inputs were dropped along with the legacy
|
|
839
1042
|
* `diffHash` + `contentHash` fields.
|
|
1043
|
+
*
|
|
1044
|
+
* AISDLC-362 (contentHashV5): when `v5Entries` + `v5MergeBase` are provided,
|
|
1045
|
+
* also emits `contentHashV5` and `signedMergeBase` in the predicate and bumps
|
|
1046
|
+
* `schemaVersion` to `'v5'`. The verifier prefers v5 when present.
|
|
840
1047
|
*/
|
|
841
1048
|
export function buildPredicate(inputs) {
|
|
842
1049
|
if (!/^[0-9a-f]{40}$/i.test(inputs.commitSha)) {
|
|
@@ -869,8 +1076,14 @@ export function buildPredicate(inputs) {
|
|
|
869
1076
|
// Producers therefore can't accidentally compute v3 over one file
|
|
870
1077
|
// set and v4 over another.
|
|
871
1078
|
const headEntries = projectDeltaEntriesToHeadEntries(inputs.changedFileDeltas);
|
|
1079
|
+
// AISDLC-362: emit v5 when the caller provided v5 collection results.
|
|
1080
|
+
const hasV5 = Array.isArray(inputs.v5Entries) &&
|
|
1081
|
+
typeof inputs.v5MergeBase === 'string' &&
|
|
1082
|
+
/^[0-9a-f]{40}$/i.test(inputs.v5MergeBase);
|
|
872
1083
|
const predicate = {
|
|
873
|
-
schemaVersion
|
|
1084
|
+
// Bump schemaVersion to 'v5' when v5 data is present. Backward-compat:
|
|
1085
|
+
// the verifier's ACCEPTED_SCHEMA_VERSIONS now includes both 'v3' and 'v5'.
|
|
1086
|
+
schemaVersion: hasV5 ? 'v5' : 'v3',
|
|
874
1087
|
subject: { digest: { sha1: inputs.commitSha.toLowerCase() } },
|
|
875
1088
|
contentHashV3: computeContentHashV3(inputs.changedFileDeltas),
|
|
876
1089
|
contentHashV4: computeContentHashV4(headEntries),
|
|
@@ -887,6 +1100,11 @@ export function buildPredicate(inputs) {
|
|
|
887
1100
|
harnessNote: inputs.harnessNote,
|
|
888
1101
|
signedAt: inputs.signedAt ?? new Date().toISOString(),
|
|
889
1102
|
};
|
|
1103
|
+
// AISDLC-362: embed the frozen merge-base and v5 hash when available.
|
|
1104
|
+
if (hasV5) {
|
|
1105
|
+
predicate.signedMergeBase = inputs.v5MergeBase.toLowerCase();
|
|
1106
|
+
predicate.contentHashV5 = computeContentHashV5(inputs.v5Entries, inputs.v5MergeBase);
|
|
1107
|
+
}
|
|
890
1108
|
// AISDLC-100.6: include `pipelineVersion` only when the caller provided
|
|
891
1109
|
// it. Omitted otherwise so envelopes signed in environments without
|
|
892
1110
|
// pipeline-cli installed still round-trip identically through
|
|
@@ -1051,28 +1269,32 @@ export function verifyAttestation(opts) {
|
|
|
1051
1269
|
reason: `subject digest mismatch (envelope was signed for a different commit)`,
|
|
1052
1270
|
};
|
|
1053
1271
|
}
|
|
1054
|
-
// AISDLC-
|
|
1055
|
-
// `contentHashV4` (base-independent — survives queue rebases) when
|
|
1056
|
-
// BOTH the envelope and the expected state carry it. v3 is only
|
|
1057
|
-
// consulted when the envelope is legacy v3-only OR when v4 is
|
|
1058
|
-
// present but doesn't match (unusual — implies the head blobs
|
|
1059
|
-
// genuinely changed, which we still reject below).
|
|
1272
|
+
// AISDLC-362: v5-prefer, v4-fallback, v3-last-resort.
|
|
1060
1273
|
//
|
|
1061
|
-
//
|
|
1062
|
-
//
|
|
1063
|
-
//
|
|
1064
|
-
//
|
|
1065
|
-
//
|
|
1066
|
-
//
|
|
1067
|
-
// content at head_blob; whatever the rebase did to base, v4
|
|
1068
|
-
// stays valid.
|
|
1274
|
+
// Priority order (highest rebase-stability first):
|
|
1275
|
+
// 1. v5 (frozen merge-base delta hash) — prefers when BOTH envelope AND
|
|
1276
|
+
// expected carry contentHashV5. Survives non-overlapping sibling merges.
|
|
1277
|
+
// 2. v4 (base-independent head-blob hash) — prefers when BOTH carry v4
|
|
1278
|
+
// but not v5. Survives queue rebases when files don't overlap.
|
|
1279
|
+
// 3. v3 (base+head blob-pair delta hash) — legacy fallback only.
|
|
1069
1280
|
//
|
|
1070
|
-
//
|
|
1071
|
-
// (
|
|
1072
|
-
//
|
|
1281
|
+
// When a higher-priority hash matches, lower-priority hashes are NOT
|
|
1282
|
+
// consulted (the merge-base shift that would invalidate them is exactly
|
|
1283
|
+
// what the higher-priority hash was designed to survive).
|
|
1284
|
+
const envelopeHasV5 = typeof predicate.contentHashV5 === 'string';
|
|
1285
|
+
const expectedHasV5 = typeof opts.expected.contentHashV5 === 'string';
|
|
1073
1286
|
const envelopeHasV4 = typeof predicate.contentHashV4 === 'string';
|
|
1074
1287
|
const expectedHasV4 = typeof opts.expected.contentHashV4 === 'string';
|
|
1075
|
-
if (
|
|
1288
|
+
if (envelopeHasV5 && expectedHasV5) {
|
|
1289
|
+
if (predicate.contentHashV5 !== opts.expected.contentHashV5) {
|
|
1290
|
+
return {
|
|
1291
|
+
valid: false,
|
|
1292
|
+
reason: 'contentHashV5 mismatch (PR content changed since attestation)',
|
|
1293
|
+
};
|
|
1294
|
+
}
|
|
1295
|
+
// v5 matched → skip v4 and v3 entirely.
|
|
1296
|
+
}
|
|
1297
|
+
else if (envelopeHasV4 && expectedHasV4) {
|
|
1076
1298
|
if (predicate.contentHashV4 !== opts.expected.contentHashV4) {
|
|
1077
1299
|
return {
|
|
1078
1300
|
valid: false,
|
|
@@ -1084,7 +1306,7 @@ export function verifyAttestation(opts) {
|
|
|
1084
1306
|
// rebase, sibling overlap); the v4 match is the source of truth.
|
|
1085
1307
|
}
|
|
1086
1308
|
else {
|
|
1087
|
-
// Legacy v3-only envelope OR caller did not supply expected.contentHashV4
|
|
1309
|
+
// Legacy v3-only envelope OR caller did not supply expected.contentHashV4/V5
|
|
1088
1310
|
// → fall back to v3. Same as pre-AISDLC-193.1 behavior.
|
|
1089
1311
|
if (predicate.contentHashV3 !== opts.expected.contentHashV3) {
|
|
1090
1312
|
return {
|