feat(ql3): make release evidence replay deterministic

This commit is contained in:
whyour
2026-08-18 02:17:26 +08:00
parent 3e7bfa8087
commit cc6d6b7b48
13 changed files with 196 additions and 31 deletions
+18
View File
@@ -11,6 +11,24 @@
最新增量证据(2026-08-18):
- D-347/ADR-0439(已接受;首份真实线上重放待实际 release tag):修正 D-346 收据把私有 runner `validatedAt` wall-clock 写入 durable
JSON 导致同一 source/report 在 workflow 重跑时产生第二个 release identity 的问题。私有收据升为
`qinglong/private-release-evidence-receipt@v2`:创建时仍以当前私有时钟执行 source-aware gate 和 24 小时 freshness 双重验证,但 durable
projection 只保留不可变报告自身的 `observedAt`、report/static-lock/self digest,并显式声明 `freshnessValidatedAtCreation=true`
`durableValidationClockPublished=false` 与公开 consumer 不能重放现场报告。相同 release identity、相同私有报告字节和相同 source-controlled
static locks 在不同有效运行时间必须生成逐字节相同收据。由于嵌入协议发生不兼容变化,release-set/OCI artifact media type 同步升为
`qinglong/release-set@v3`/`application/vnd.qinglong.release-set.v3+json`,不在 v2 下偷换语义;catalog recovery 收紧为
`republish_deterministic_content_then_verify_digest`,只允许相同不可变输入的同内容重放。Local 仍为零私有收据,新增工作只在短生命周期
release runner 发生,不新增 package、依赖、数据库、migration、Pod、controller、listener、timer、设备工具或稳态资源。定向发布链
135/135;完整 backend 1,363 项为 1,361 pass/2 条件 skip/0 fail18-package clean build/test 退出 0。12 项 package boundary、Cluster
dependency、Edge import、Cluster/Worker/CloudNativePG 部署、backup、Barman/cert-manager selection、image release、deployment-lock
surface 与 release-version 审计全部 compatible14 档 Local artifact 与既有字节基线完全一致:默认 Edge/Standalone 为
2,589,890/2,589,968 bytesadopted 为 2,809,185/2,809,308 bytesapplication 为 3,632,769/3,632,889 bytesapplication-api 为
3,800,322/3,800,466 bytesAI 为 3,069,143/3,069,233 bytesapplication+AI 为 4,493,043/4,493,175 bytesMCP 为
7,315,930/7,316,038 bytes。阶段完整性 PostgreSQL HA 以 18.6/arm64 重跑 142/142 gates、timeline `1→2`,独立 evidence audit
compatiblemode-0600 报告 SHA-256 为 `db72086e971fc91c25a4a482af6634acbf4d463e567c44f42af0e7d0cc24fca2`
container/network/volume 残留为 0。真实线上 response-loss 重放仍只能由实际受保护 `v3` release tag 取得。
- D-346/ADR-0438(已接受;首份真实公开收据待实际 release tag):Cluster/All 发布不再只通过 GitHub `needs` 保存“两个私有 evidence job
成功”的瞬时调度事实。Worker management 与 CloudNativePG DR 私有 runner 在 source-aware gate 成功后,各自产生一个
`qinglong/private-release-evidence-receipt@v1`:只投影 exact version/source tag/revision/scope、evidence kind、24 小时 freshness、
@@ -0,0 +1,76 @@
# ADR-0439:确定性私有证据收据与 Release-set 重放
- 状态:Accepted
- 日期:2026-08-18
- 关联 RFCQL-RFC-0001 D-03、D-14、D-335、D-336、D-346、D-347
- 关联 ADRADR-0427、ADR-0428、ADR-0430、ADR-0438
- SupersedesADR-0438 的 `qinglong/private-release-evidence-receipt@v1``qinglong/release-set@v2` 聚合版本
## 上下文
ADR-0438 把 Worker management 与 CloudNativePG disaster-recovery 私有报告投影为 content-free 收据,并将两份收据纳入 durable
release-set。v1 收据同时持久化 `observedAt` 和私有 job 实际运行时的 `validatedAt`。后者来自 runner wall-clock,不属于不可变 source
evidence。
因此,同一 tag、同一 source revision、同一报告与同一静态 lock 只要在不同时间重跑,就会产生不同 receipt digest、release-set digest 和 OCI
catalog manifest digest。这与 catalog plan 已冻结的 `republish_same_content_then_verify_digest` 恢复语义矛盾:一次合法的 workflow response-loss
恢复可能生成第二个 release identity,甚至让无 authority 的 discovery tag 指向不同内容。
## 决策
1. 私有收据升级为 `qinglong/private-release-evidence-receipt@v2``schemaVersion=2`。收据继续绑定报告自身的 `observedAt`、24 小时最大年龄、
report digest、release identity、evidence kind、静态审计摘要和 self digest,但不持久化 runner validation wall-clock。
2. 创建路径仍必须取得当前私有 runner 时钟,并由既有 source-aware gate 以及 receipt assembler 双重验证:报告不得超过 24 小时,最多只允许五分钟
未来时钟偏差。移除 durable clock 绝不等于移除 freshness gate。
3. 收据显式声明 `freshnessValidatedAtCreation=true``durableValidationClockPublished=false`。公开消费者可验证结构、source/report equality 和
digest 闭包,但仍必须保持 `publicConsumerReplay=not_possible_without_private_reports`,不能把布尔声明冒充现场重放。
4. 相同 release identity、相同私有报告字节和相同 source-controlled static locks,无论私有 job 在有效窗口内何时重跑,必须生成逐字节相同的
receipt。报告自身使用 owner-private、mode `0600`、commit-scoped、no-replace 发布路径;不同报告 digest 不是同一次恢复输入。
5. release-set 升级为 `qinglong/release-set@v3`OCI artifact/file media type 同步升级为
`application/vnd.qinglong.release-set.v3+json`。不能在 v2 release-set 或 media type 下静默替换嵌入收据的 schema 与字段。
6. catalog recovery 语义收紧为 `republish_deterministic_content_then_verify_digest`:只允许相同 source evidence 的确定性内容重发并回读同一 digest;
source/report/static-lock 发生变化时必须作为新输入失败关闭,不能解释为同一发布的 response-loss replay。
7. 不新增 package、production dependency、数据库、migration、Pod、controller、listener、timer、设备工具或稳态资源。变化只影响短生命周期
release runner 的 JSON 字段和 SHA-256Local scope 仍为零私有收据。
## 失败与恢复
- 当前时钟超出 freshness window:创建在写文件前失败;不得因 durable JSON 不含 wall-clock 而放宽。
- 同一 source report 重跑:v2 收据、v3 release-set、catalog plan 与 manifest 内容必须逐字节一致,可安全重发并验证同 digest。
- 同一 source 路径内容被替换:私有 no-replace/stable descriptor 边界或 report digest 闭包失败;不能覆盖既有发布身份。
- v1 receipt、v2 release-set 或 v2 media type3.0 尚未正式发布,不做隐式迁移;从原始私有证据重新运行受保护 release workflow。
- 历史 catalog consumer:只接受 v3 release-set;不会把 v1/v2 解释成当前 schema。
## 部署与资源影响
- Edge/Standalone 设备下载的仍是一个 Local image digest 和小型 selection;不安装证据工具,不增加常驻 CPU/RSS、I/O 或连接。
- Cluster 节点仍只消费 catalog-bound image references;确定性发生在发布工作站,不引入 Kubernetes workload 或运行期控制面。
- v2 收据比 v1 少一个时间字符串,并增加两个固定 boolean;差异不改变制品预算或包拓扑。
## 被拒绝的替代方案
### 保留 `validatedAt` 并允许重跑产生新 catalog
拒绝。同一不可变 release source 将拥有多个仅由 runner 时钟区分的 durable identityresponse-loss 无法与新发布区分。
### 把 `validatedAt` 固定为 `observedAt`
拒绝。两个字段相同会伪称验证与观察发生在同一时刻。删除未发布的 ephemeral clock,并明确声明其未持久化,更符合证据强度。
### 删除 freshness 检查以换取确定性
拒绝。确定性只约束 durable projection;私有创建仍必须以当前时钟重放 source-aware gate。
### 保持 release-set v2 与 OCI v2 media type
拒绝。嵌入对象从 receipt v1 变为 v2 是消费者可观察的协议变化,复用旧 schema/media type 会隐藏不兼容语义。
## 验证
- receipt contract 必须证明相同报告在两个不同有效 validation clock 下生成相同对象和相同 canonical bytes,并拒绝重新注入 `validatedAt`
- 既有 source/scope/report/static-lock/self-digest、私有字段、mode `0600`、stable read 与 no-replace 负向门全部保留;
- release-set、catalog、consumption、deployment-lock 与 workflow 审计必须只接受 receipt v2、release-set v3 和 OCI v3 media type
- 定向发布链 135/135,完整 backend 1,363 项为 1,361 pass/2 条件 skip/0 fail18-package clean build/test 退出 012 项静态审计与 14 档
Local artifact 均 compatible 且制品字节基线不变;PostgreSQL 18.6 arm64 HA 为 142/142、timeline `1→2`,独立报告审计 compatible,且
Docker container/network/volume 零残留;
- 完整测试、制品与运行门结果记录于 QL-RFC-0001 D-347;首份真实线上重放仍须由受保护 `v3` release tag 产生。
+1
View File
@@ -442,6 +442,7 @@
| [ADR-0436](./ADR-0436-post-publication-catalog-bound-kubernetes-release-gate.md) | 发布后 Catalog-bound Kubernetes Release Gate | Accepted(首份真实公开 catalog evidence 待实际 release tag |
| [ADR-0437](./ADR-0437-post-publication-catalog-bound-local-release-gate.md) | 发布后 Catalog-bound Local Release Gate | Accepted(首份真实公开 catalog evidence 待实际 release tag |
| [ADR-0438](./ADR-0438-content-free-private-release-evidence-receipt-chain.md) | 内容无关的私有发布证据收据链 | Accepted(首份真实公开收据待实际 release tag |
| [ADR-0439](./ADR-0439-deterministic-private-evidence-receipts-and-release-set-replay.md) | 确定性私有证据收据与 Release-set 重放 | Accepted(首份真实线上重放待实际 release tag |
## 规则
@@ -15,14 +15,16 @@ ghcr.io/<owner>/qinglong3-release-catalog:v<version>-<scope>
## 私有发布证据收据链
当前长期 authority 是 `qinglong/release-set@v2``local` scope 的 `evidenceReceipts` 必须为空;`cluster|all` 必须按顺序恰好包含
`worker-management``cloudnativepg-disaster-recovery` 两份 `qinglong/private-release-evidence-receipt@v1`。每份收据绑定同一
当前长期 authority 是 `qinglong/release-set@v3``local` scope 的 `evidenceReceipts` 必须为空;`cluster|all` 必须按顺序恰好包含
`worker-management``cloudnativepg-disaster-recovery` 两份 `qinglong/private-release-evidence-receipt@v2`。每份收据绑定同一
version/source tag/revision/scope、24 小时 freshness、私有报告 digest 和自身 digestDR 收据还绑定 CloudNativePG backup、Barman Cloud 与
cert-manager 三项静态审计摘要。
cert-manager 三项静态审计摘要。v2 收据不持久化私有 runner 的 wall-clock:创建时仍必须以当前时钟完成 freshness gate,但 durable JSON 只绑定
不可变报告的 `observedAt`。因此相同 source/report/static-lock 在 workflow 重跑时逐字节相同,不会仅因重跑时间变化而生成第二个 catalog identity。
这些收据不包含原始生产报告、路径、credential、token、Kubernetes object 或 transcript。公开 consumer 可以重算收据和 release-set digest
但必须保持 `publicConsumerReplay=not_possible_without_private_reports`;它不能声称重放了私有现场结果。原始报告不上传,只有收据以 1 天 artifact
从私有 job 交给 release-set job,随后完整嵌入 release-set v2 并由 durable catalog 长期保护。
从私有 job 交给 release-set job,随后完整嵌入 release-set v3 并由 durable catalog 长期保护。公开收据同时声明
`freshnessValidatedAtCreation=true``durableValidationClockPublished=false`,避免把未发布的临时时钟伪装成可离线重放的现场证据。
## 发布流水线内置 Local 与 Cluster 下游门
@@ -404,7 +406,7 @@ SHA-256`receipt.expectedDigest` 是 receipt 内的 `receiptDigest`。审计
1. 只接受已验证 Cosign exact workflow identity 与 GitHub source tag/revision provenance 的 catalog immutable
referencediscovery tag 无 authority。
2. materializer 只能接受完整 `qinglong/release-catalog-consumption-ceremony@v1` bundle,不能接受旧的松散 `--release-set`;其中
`qinglong/release-set@v2``release.version``release.sourceRef``release.sourceRevision``release.scope` 必须与变更单一致。
`qinglong/release-set@v3``release.version``release.sourceRef``release.sourceRevision``release.scope` 必须与变更单一致。
3. 镜像集合必须与上表精确相等;每个 `reference` 必须是 digest reference,且 owner/repository 与部署目标一致。
4. Local scope 必须为零私有收据;Cluster/All 必须精确包含两份同 source、同 scope、自摘要有效且 freshness 闭合的 content-free 收据。
static lock compatible 不等于现场证据已公开重放,任何 consumer 都必须保留该限制。
+2 -1
View File
@@ -1303,6 +1303,7 @@ function auditReleaseWorkflow(source) {
sourceDerived: true,
sameRunRecords: true,
sameRunPrivateEvidenceReceipts: true,
deterministicPrivateEvidenceReceipts: true,
exactScopeClosure: true,
standaloneInspection: true,
tagPromotionAuthority: 'complete_verified_release_set',
@@ -1312,7 +1313,7 @@ function auditReleaseWorkflow(source) {
},
durableCatalog: {
repository: 'qinglong3-release-catalog',
artifactType: 'application/vnd.qinglong.release-set.v2+json',
artifactType: 'application/vnd.qinglong.release-set.v3+json',
basenameOnly: true,
crossRunnerDeterministic: true,
byteExactRoundTrip: true,
@@ -21,7 +21,7 @@ const {
auditCertManagerSelection,
} = require('./ql3-cert-manager-selection-audit.cjs');
const RECEIPT_SCHEMA = 'qinglong/private-release-evidence-receipt@v1';
const RECEIPT_SCHEMA = 'qinglong/private-release-evidence-receipt@v2';
const EVIDENCE_KINDS = Object.freeze([
'worker-management',
'cloudnativepg-disaster-recovery',
@@ -182,12 +182,12 @@ function assembleReceipt(options) {
fail('private evidence report digest is invalid');
}
const observedAtMs = Date.parse(options.observedAt);
const validatedAtMs = options.nowMs;
const validationClockMs = options.nowMs;
if (
!Number.isSafeInteger(validatedAtMs) ||
!Number.isSafeInteger(validationClockMs) ||
!Number.isFinite(observedAtMs) ||
observedAtMs > validatedAtMs + MAX_FUTURE_SKEW_MS ||
validatedAtMs - observedAtMs > MAX_EVIDENCE_AGE_SECONDS * 1000
observedAtMs > validationClockMs + MAX_FUTURE_SKEW_MS ||
validationClockMs - observedAtMs > MAX_EVIDENCE_AGE_SECONDS * 1000
) {
fail('private evidence is outside the release freshness window');
}
@@ -196,7 +196,7 @@ function assembleReceipt(options) {
? staticAuditReceipts(options.root)
: Object.freeze([]);
const unsigned = {
schemaVersion: 1,
schemaVersion: 2,
schema: RECEIPT_SCHEMA,
release: {
version: options.version,
@@ -208,7 +208,6 @@ function assembleReceipt(options) {
evidence: {
fixture: options.fixture,
observedAt: options.observedAt,
validatedAt: new Date(validatedAtMs).toISOString(),
maximumAgeSeconds: MAX_EVIDENCE_AGE_SECONDS,
reportDigest: options.reportDigest,
sourceReportsUploaded: false,
@@ -217,6 +216,8 @@ function assembleReceipt(options) {
verification: {
sourceAwareAudit: true,
privateEvidenceReplayed: true,
freshnessValidatedAtCreation: true,
durableValidationClockPublished: false,
publicConsumerReplay: 'not_possible_without_private_reports',
privateReportContentPublished: false,
compatible: true,
@@ -323,7 +324,7 @@ function inspectPrivateReleaseEvidenceReceipt(actual, options) {
'verification',
'receiptDigest',
]) ||
actual.schemaVersion !== 1 ||
actual.schemaVersion !== 2 ||
actual.schema !== RECEIPT_SCHEMA ||
!exactKeys(actual.release, [
'version',
@@ -342,7 +343,6 @@ function inspectPrivateReleaseEvidenceReceipt(actual, options) {
!exactKeys(actual.evidence, [
'fixture',
'observedAt',
'validatedAt',
'maximumAgeSeconds',
'reportDigest',
'sourceReportsUploaded',
@@ -363,6 +363,8 @@ function inspectPrivateReleaseEvidenceReceipt(actual, options) {
!exactKeys(actual.verification, [
'sourceAwareAudit',
'privateEvidenceReplayed',
'freshnessValidatedAtCreation',
'durableValidationClockPublished',
'publicConsumerReplay',
'privateReportContentPublished',
'compatible',
@@ -371,6 +373,8 @@ function inspectPrivateReleaseEvidenceReceipt(actual, options) {
JSON.stringify({
sourceAwareAudit: true,
privateEvidenceReplayed: true,
freshnessValidatedAtCreation: true,
durableValidationClockPublished: false,
publicConsumerReplay: 'not_possible_without_private_reports',
privateReportContentPublished: false,
compatible: true,
@@ -378,14 +382,7 @@ function inspectPrivateReleaseEvidenceReceipt(actual, options) {
) {
fail('receipt shape or release binding is invalid');
}
const observedAtMs = Date.parse(actual.evidence.observedAt);
const validatedAtMs = Date.parse(actual.evidence.validatedAt);
if (
!Number.isFinite(observedAtMs) ||
!Number.isFinite(validatedAtMs) ||
observedAtMs > validatedAtMs + MAX_FUTURE_SKEW_MS ||
validatedAtMs - observedAtMs > MAX_EVIDENCE_AGE_SECONDS * 1000
) {
if (!Number.isFinite(Date.parse(actual.evidence.observedAt))) {
fail('receipt freshness binding is invalid');
}
const { receiptDigest, ...unsigned } = actual;
+2 -2
View File
@@ -12,7 +12,7 @@ const {
const CATALOG_PLAN_SCHEMA = 'qinglong/release-catalog-plan@v1';
const CATALOG_RECEIPT_SCHEMA = 'qinglong/release-catalog-receipt@v1';
const ARTIFACT_TYPE = 'application/vnd.qinglong.release-set.v2+json';
const ARTIFACT_TYPE = 'application/vnd.qinglong.release-set.v3+json';
const FILE_MEDIA_TYPE = ARTIFACT_TYPE;
const OCI_MANIFEST_MEDIA_TYPE = 'application/vnd.oci.image.manifest.v1+json';
const OCI_EMPTY_CONFIG_MEDIA_TYPE = 'application/vnd.oci.empty.v1+json';
@@ -173,7 +173,7 @@ function createCatalogPlan(releaseSet, options) {
discoveryTagAuthority: 'none',
immutableDigestAuthority: 'required',
roundTrip: 'byte_exact',
recovery: 'republish_same_content_then_verify_digest',
recovery: 'republish_deterministic_content_then_verify_digest',
},
};
return Object.freeze({
+1 -1
View File
@@ -20,7 +20,7 @@ const { VERSION_PATTERN } = require('./lib/ql3-release-identity.cjs');
const DEFAULT_ROOT = path.resolve(__dirname, '..');
const IMAGE_RECORD_SCHEMA = 'qinglong/release-set-image-record@v1';
const RELEASE_SET_SCHEMA = 'qinglong/release-set@v2';
const RELEASE_SET_SCHEMA = 'qinglong/release-set@v3';
const MAX_JSON_BYTES = 1024 * 1024;
const DIGEST_PATTERN = /^sha256:[a-f0-9]{64}$/u;
const OWNER_PATTERN = /^[a-z0-9](?:[a-z0-9-]{0,37}[a-z0-9])?$/u;
@@ -99,6 +99,7 @@ test('accepts the reviewed native CI and digest release contracts', () => {
sourceDerived: true,
sameRunRecords: true,
sameRunPrivateEvidenceReceipts: true,
deterministicPrivateEvidenceReceipts: true,
exactScopeClosure: true,
standaloneInspection: true,
tagPromotionAuthority: 'complete_verified_release_set',
@@ -108,7 +109,7 @@ test('accepts the reviewed native CI and digest release contracts', () => {
},
durableCatalog: {
repository: 'qinglong3-release-catalog',
artifactType: 'application/vnd.qinglong.release-set.v2+json',
artifactType: 'application/vnd.qinglong.release-set.v3+json',
basenameOnly: true,
crossRunnerDeterministic: true,
byteExactRoundTrip: true,
@@ -119,6 +119,70 @@ test('rejects publishing private material in an otherwise re-digested receipt',
);
});
test('omits the ephemeral validation clock and replays identical source evidence byte-for-byte', (t) => {
const directory = temporaryDirectory(t);
const reportPath = path.join(directory, 'private-report.json');
fs.writeFileSync(
reportPath,
`${JSON.stringify({ observedAt: '2026-08-18T00:00:00.000Z' })}\n`,
{ mode: 0o600 },
);
const createAt = (validationTime, outputFile) =>
createWorkerReceipt(
{
root: path.resolve(__dirname, '../..'),
version: RELEASE.version,
sourceRevision: RELEASE.sourceRevision,
sourceRef: RELEASE.sourceRef,
releaseScope: RELEASE.scope,
reportFile: reportPath,
ceremonyReportFile: path.join(directory, 'private-ceremony.json'),
durableAuditReportFile: path.join(directory, 'private-durable.json'),
pkiRotationReportFile: path.join(directory, 'private-pki.json'),
caRolloverReportFile: path.join(directory, 'private-ca.json'),
outputFile,
},
{
now: () => Date.parse(validationTime),
auditGate(_options, dependencies) {
assert.equal(dependencies.now(), Date.parse(validationTime));
return {
fixture: 'qinglong/worker-credential-management-release-gate@v1',
evidenceReportSha256: `sha256:${'5'.repeat(64)}`,
};
},
},
);
const firstPath = path.join(directory, 'first.json');
const secondPath = path.join(directory, 'second.json');
const first = createAt('2026-08-18T00:05:00.000Z', firstPath);
const second = createAt('2026-08-18T00:15:00.000Z', secondPath);
assert.deepEqual(second, first);
assert.equal(
fs.readFileSync(secondPath, 'utf8'),
fs.readFileSync(firstPath, 'utf8'),
);
assert.equal(Object.hasOwn(first.evidence, 'validatedAt'), false);
assert.equal(first.verification.freshnessValidatedAtCreation, true);
assert.equal(first.verification.durableValidationClockPublished, false);
assert.throws(
() =>
createAt('2026-08-19T00:00:01.000Z', path.join(directory, 'stale.json')),
/freshness window/,
);
const widened = JSON.parse(JSON.stringify(first));
widened.evidence.validatedAt = '2026-08-18T00:05:00.000Z';
assert.throws(
() =>
inspectPrivateReleaseEvidenceReceipt(
widened,
options('worker-management'),
),
/receipt shape/,
);
});
test('audit CLI accepts a canonical receipt and rejects open argument shapes', (t) => {
const directory = temporaryDirectory(t);
const receiptPath = path.join(directory, 'worker-management.json');
@@ -79,7 +79,7 @@ function releaseSet(scope) {
const names = images.map((entry) => entry.name);
const unsigned = {
schemaVersion: 1,
schema: 'qinglong/release-set@v2',
schema: 'qinglong/release-set@v3',
release,
candidate: {
schema: 'qinglong/release-candidate-contract@v1',
@@ -121,6 +121,10 @@ test('plans deterministic Local, Cluster and All OCI catalog entries', () => {
`ghcr.io/qinglong-release/qinglong3-release-catalog:v${version}-${scope}`,
);
assert.equal(plan.catalog.artifactType, ARTIFACT_TYPE);
assert.equal(
plan.publicationPolicy.recovery,
'republish_deterministic_content_then_verify_digest',
);
assert.equal(
plan.releaseSet.bytes,
Buffer.byteLength(`${JSON.stringify(set)}\n`),
+4 -3
View File
@@ -2,7 +2,7 @@
const crypto = require('node:crypto');
const RECEIPT_SCHEMA = 'qinglong/private-release-evidence-receipt@v1';
const RECEIPT_SCHEMA = 'qinglong/private-release-evidence-receipt@v2';
const FIXTURES = Object.freeze({
'worker-management': 'qinglong/worker-credential-management-release-gate@v1',
'cloudnativepg-disaster-recovery':
@@ -28,14 +28,13 @@ function privateReleaseEvidenceReceipt(release, evidenceKind) {
}))
: [];
const unsigned = {
schemaVersion: 1,
schemaVersion: 2,
schema: RECEIPT_SCHEMA,
release: { ...release },
evidenceKind,
evidence: {
fixture: FIXTURES[evidenceKind],
observedAt: '2026-08-18T00:00:00.000Z',
validatedAt: '2026-08-18T00:05:00.000Z',
maximumAgeSeconds: 86_400,
reportDigest: `sha256:${
evidenceKind === 'worker-management' ? '5' : '6'
@@ -46,6 +45,8 @@ function privateReleaseEvidenceReceipt(release, evidenceKind) {
verification: {
sourceAwareAudit: true,
privateEvidenceReplayed: true,
freshnessValidatedAtCreation: true,
durableValidationClockPublished: false,
publicConsumerReplay: 'not_possible_without_private_reports',
privateReportContentPublished: false,
compatible: true,