diff --git a/.github/workflows/production-lifecycle-policy.yml b/.github/workflows/production-lifecycle-policy.yml index d690cb7..8e63288 100644 --- a/.github/workflows/production-lifecycle-policy.yml +++ b/.github/workflows/production-lifecycle-policy.yml @@ -8,6 +8,8 @@ on: paths: - ".github/workflows/production-lifecycle-policy.yml" - "docs/production-lifecycle.json" + - "docs/production-lifecycle-verifications.json" + - "docs/javascripts/production-lifecycle.js" - "docs/production-workflow-admissions.json" - "docs/reference/production-lifecycle.md" - "docs/schemas/production-lifecycle-public.schema.json" @@ -15,14 +17,19 @@ on: - "production-lifecycle-source.json" - "production-workflow-admissions-source.json" - "scripts/render_production_lifecycle.py" + - "scripts/generate_production_lifecycle_verification.py" - "scripts/render_production_workflow_admissions.py" - "tests/test_production_lifecycle_policy.py" + - "tests/test_production_lifecycle_verification.py" + - "tests/js/production_lifecycle.test.cjs" - "tests/test_production_workflow_admissions.py" push: branches: [main] paths: - ".github/workflows/production-lifecycle-policy.yml" - "docs/production-lifecycle.json" + - "docs/production-lifecycle-verifications.json" + - "docs/javascripts/production-lifecycle.js" - "docs/production-workflow-admissions.json" - "docs/reference/production-lifecycle.md" - "docs/schemas/production-lifecycle-public.schema.json" @@ -30,8 +37,11 @@ on: - "production-lifecycle-source.json" - "production-workflow-admissions-source.json" - "scripts/render_production_lifecycle.py" + - "scripts/generate_production_lifecycle_verification.py" - "scripts/render_production_workflow_admissions.py" - "tests/test_production_lifecycle_policy.py" + - "tests/test_production_lifecycle_verification.py" + - "tests/js/production_lifecycle.test.cjs" - "tests/test_production_workflow_admissions.py" permissions: @@ -52,14 +62,19 @@ jobs: with: python-version: "3.12" + - name: Test fail-closed projection behavior + run: | + python3 -m unittest tests.test_production_lifecycle_policy tests.test_production_workflow_admissions tests.test_production_lifecycle_verification + node --test tests/js/production_lifecycle.test.cjs + - name: Check exact canonical Production lifecycle projection run: python3 scripts/render_production_lifecycle.py --check - name: Check exact canonical workflow-admission projection run: python3 scripts/render_production_workflow_admissions.py --check - - name: Test fail-closed projection behavior - run: python3 -m unittest tests.test_production_lifecycle_policy tests.test_production_workflow_admissions + - name: Check retained canonical verification records + run: python3 scripts/generate_production_lifecycle_verification.py --check report-failure: needs: validate diff --git a/.github/workflows/sync.yml b/.github/workflows/sync.yml index fe2358f..231107d 100644 --- a/.github/workflows/sync.yml +++ b/.github/workflows/sync.yml @@ -80,6 +80,9 @@ jobs: - name: Validate run: uv run python scripts/validate_docs.py + - name: Verify retained lifecycle results before publication + run: uv run python scripts/generate_production_lifecycle_verification.py --check + - name: Build MkDocs site run: uv run mkdocs build --strict diff --git a/docs/javascripts/production-lifecycle.js b/docs/javascripts/production-lifecycle.js index 247208e..ab90b2e 100644 --- a/docs/javascripts/production-lifecycle.js +++ b/docs/javascripts/production-lifecycle.js @@ -299,8 +299,7 @@ admission.verdict !== "accepted" || admission.expires_at !== null || admission.revoked_at != null || - typeof admission.evidence_class !== "string" || - admission.evidence_class.length === 0 || + admission.evidence_class !== "remote-safe-synthetic" || issuedAt === null || issuedAt > now || !isObject(identity) || @@ -335,6 +334,8 @@ evidence: { class: admission.evidence_class }, release, untilRevoked: true, + admission, + target, }); } @@ -642,6 +643,318 @@ } } + // This is a build-time result from the canonical verifier, not a signature. + // The same-origin generated file is trusted only through the reviewed build. + const VERIFICATIONS_URL = "/production-lifecycle-verifications.json"; + const CANONICAL_API = "https://api.github.com/repos/OpenAdaptAI/.github"; + const CANONICAL_RAW = "https://raw.githubusercontent.com/OpenAdaptAI/.github"; + const REFERENCE_FIELDS = ["kind", "object_schema_version", "object_path", "object_sha256", + "size_bytes", "object_media_type", "semantic_identity_sha256", "subject_sha256", + "registry_entry_sha256"]; + const RECEIPT_FIELDS = ["schema_version", "verification_id_sha256", "verdict", "evidence_class", + "target", "claim_scope", "admission_object_sha256", "admission_bundle_object_sha256", + "admission_id_sha256", "release_sha256", "artifact_inventory_sha256", "release_identity", + "source_repository", "source_repository_id", "source_commit", "version", "tag", + "draft_release_id", "publication_staging_sha256", "authority_state_sha256", + "revocation_state_sha256", "signer_registry_sha256", "acceptance_summary_object_sha256", + "acceptance_manifest_object_sha256", "decision_receipt_object_sha256", + "qualification_admission_object_sha256", "qualification_admission_id_sha256", + "workflow_version_id_sha256", "workflow_bundle_sha256", "admitted_runtime_sha256", + "verified_at", "expires_at", "registry_source_commit", "registry_revision", + "registry_head_sha256", "trust_state_source_commit"]; + + function canonical(value) { + if (Array.isArray(value)) return `[${value.map(canonical).join(",")}]`; + if (isObject(value)) return `{${Object.keys(value).sort().map( + (key) => `${JSON.stringify(key)}:${canonical(value[key])}`).join(",")}}`; + return JSON.stringify(value); + } + + function sameValue(left, right) { return canonical(left) === canonical(right); } + + function validReference(ref, kind) { + return isObject(ref) && ref.kind === kind && + ref.repository === "OpenAdaptAI/.github" && HEX40.test(ref.registry_source_commit) && + DIGEST.test(ref.object_sha256) && Number.isInteger(ref.size_bytes) && ref.size_bytes > 0 && + ref.object_path === `production-evidence/objects/sha256/${ref.object_sha256.slice(7, 9)}/${ref.object_sha256.slice(7)}.${kind}.json`; + } + + async function verifiedObject(ref, kind, fetchImpl, now, commit = ref?.registry_source_commit) { + if (!validReference(ref, kind) || !HEX40.test(commit)) return null; + const bytes = await fetchBytes(fetchImpl, `${CANONICAL_RAW}/${commit}/${ref.object_path}`, now); + if (!bytes || bytes.length !== ref.size_bytes || await sha256(bytes) !== ref.object_sha256) return null; + const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes)); + return isObject(value) ? value : null; + } + + function activeWindow(value, startKey, now, expectedStatus) { + const start = parseTimestamp(value?.[startKey]); + const end = value?.expires_at === null ? Infinity : parseTimestamp(value?.expires_at); + return start !== null && start <= now && end !== null && end > now && + (expectedStatus === undefined || value.status === expectedStatus); + } + + function lastPairMatches(registry, kind, ref, bundle) { + const indexes = registry.entries.map((entry, index) => entry.kind === kind ? index : -1).filter((i) => i >= 0); + if (!indexes.length) return false; + const index = indexes[indexes.length - 1]; + return [ref, bundle].every((expected, offset) => + isObject(expected) && isObject(registry.entries[index + offset]) && + REFERENCE_FIELDS.every((key) => sameValue(expected[key], registry.entries[index + offset][key]))); + } + + function statementKeys(bundle, reference, now) { + const envelope = bundle?.dsseEnvelope; + if (!isObject(envelope) || !Array.isArray(envelope.signatures) || envelope.signatures.length !== 1) return null; + const statement = JSON.parse(atob(envelope.payload)); + if (statement.schema_version !== "openadapt.production-public-trust-signing-statement/v1" || + !activeWindow(statement, "not_before", now) || + statement.object_sha256 !== reference.object_sha256 || statement.object_size_bytes !== reference.size_bytes || + statement.object_kind !== reference.kind || statement.semantic_identity_sha256 !== reference.semantic_identity_sha256 || + envelope.signatures[0].keyid !== statement.key_id || typeof statement.key_id !== "string") return null; + return [statement.key_id]; + } + + async function verifyV2Pair(reference, bundleReference, kind, fetchImpl, now) { + if (!isObject(reference) || !isObject(bundleReference) || + bundleReference.subject_sha256 !== reference.object_sha256 || + bundleReference.registry_source_commit !== reference.registry_source_commit) return null; + const [object, bundle] = await Promise.all([ + verifiedObject(reference, kind, fetchImpl, now), + verifiedObject(bundleReference, `${kind}-sigstore-bundle`, fetchImpl, now), + ]); + const keys = bundle && statementKeys(bundle, reference, now); + if (!object || !keys) return null; + if (Object.prototype.hasOwnProperty.call(object, "expires_at") && + !activeWindow(object, Object.prototype.hasOwnProperty.call(object, "not_before") ? "not_before" : "issued_at", now)) return null; + return { object, keys }; + } + + async function currentV2State(record, fetchImpl, now, fresh, evidenceKeys) { + const state = record.current_state; + const { registry, commit } = fresh; + if (!hasExactKeys(state, ["authority_reference", "authority_bundle_reference", "revocation_reference", + "revocation_bundle_reference", "signer_registry_pointer"]) || !isObject(registry) || + registry.repository !== "OpenAdaptAI/.github" || !Array.isArray(registry.entries) || + !sameValue(registry.signer_registry, state.signer_registry_pointer)) return false; + for (const [label, kind] of [["authority", "qualification-authority-state-receipt"], + ["revocation", "qualification-revocation-state-receipt"]]) { + if (!lastPairMatches(registry, kind, state[`${label}_reference`], state[`${label}_bundle_reference`])) return false; + } + const [authority, revocation, authorityBundle, revocationBundle] = await Promise.all([ + verifiedObject(state.authority_reference, "qualification-authority-state-receipt", fetchImpl, now, commit), + verifiedObject(state.revocation_reference, "qualification-revocation-state-receipt", fetchImpl, now, commit), + verifiedObject(state.authority_bundle_reference, "qualification-authority-state-receipt-sigstore-bundle", fetchImpl, now, commit), + verifiedObject(state.revocation_bundle_reference, "qualification-revocation-state-receipt-sigstore-bundle", fetchImpl, now, commit), + ]); + const pointer = state.signer_registry_pointer; + if (!isObject(pointer) || !DIGEST.test(pointer.object_sha256) || + pointer.object_path !== `production-evidence/signer-registries/sha256/${pointer.object_sha256.slice(7, 9)}/${pointer.object_sha256.slice(7)}.qualification-signer-registry.json`) return false; + const signerBytes = await fetchBytes(fetchImpl, `${CANONICAL_RAW}/${commit}/${pointer.object_path}`, now); + if (!signerBytes || await sha256(signerBytes) !== pointer.object_sha256) return false; + const signer = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(signerBytes)); + if (!authorityBundle || !revocationBundle || !activeWindow(authority, "not_before", now, "active") || + !activeWindow(revocation, "not_before", now, "current") || !activeWindow(signer, "generated_at", now) || + signer.revision !== pointer.registry_revision || !Array.isArray(signer.signers) || + !Array.isArray(revocation.revocations)) return false; + const receipt = record.verification_receipt; + const signerIdentity = await sha256(new TextEncoder().encode( + `OpenAdapt qualification signer registry v2\0${canonical(signer)}`)); + if (pointer.registry_identity_sha256 !== signerIdentity || + authority.signer_registry_sha256 !== pointer.object_sha256 || + authority.signer_registry_identity_sha256 !== signerIdentity || + authority.signer_registry_revision !== signer.revision || + revocation.signer_registry_sha256 !== signerIdentity || + revocation.authority_state_sha256 !== authority.authority_state_sha256 || + receipt.signer_registry_sha256 !== signerIdentity || + receipt.authority_state_sha256 !== authority.authority_state_sha256 || + receipt.revocation_state_sha256 !== revocation.revocation_state_sha256 || + state.authority_reference.semantic_identity_sha256 !== authority.authority_state_sha256 || + state.revocation_reference.semantic_identity_sha256 !== revocation.revocation_state_sha256) return false; + const usedKeys = new Set([authority.issuer_key_id, revocation.issuer_key_id, ...evidenceKeys]); + for (const [bundle, reference] of [[authorityBundle, state.authority_reference], + [revocationBundle, state.revocation_reference]]) { + const keys = statementKeys(bundle, reference, now); + if (!keys) return false; + for (const key of keys) usedKeys.add(key); + } + for (const keyId of usedKeys) { + const keys = signer.signers.filter((key) => key.key_id === keyId); + if (keys.length !== 1 || keys[0].status !== "active" || + (keys[0].revoked_at !== null && (parseTimestamp(keys[0].revoked_at) === null || + parseTimestamp(keys[0].revoked_at) <= now))) return false; + } + // Exact current bytes were cryptographically checked by the generator. A + // different relevant object needs a new verification; registry append alone does not. + const subjectIds = new Set([receipt.admission_id_sha256, receipt.admission_object_sha256, + receipt.qualification_admission_id_sha256, receipt.qualification_admission_object_sha256, + receipt.acceptance_summary_object_sha256, receipt.acceptance_manifest_object_sha256, + receipt.decision_receipt_object_sha256]); + return !revocation.revocations.some((item) => subjectIds.has(item.subject_id) || + (item.subject_kind === "qualification-signer-key" && signer.signers.some((key) => + key.status === "active" && key.public_key_sha256 === item.subject_id))); + } + + async function verifyV2Receipt(active, record, liveAdmissions, fetchImpl, now) { + if (!hasExactKeys(record, ["schema_version", "target", "admission_reference", "admission_bundle_reference", + "verification_receipt", "current_state"]) || record.schema_version !== "openadapt.public-production-lifecycle-verification/v1" || + record.target !== active.targetId || !liveAdmissions.admissions.some((ref) => sameValue(ref, record.admission_reference))) return false; + const ref = record.admission_reference; + const bundleRef = record.admission_bundle_reference; + const releasePair = await verifyV2Pair(ref, bundleRef, "qualification-release", fetchImpl, now); + const admission = releasePair?.object; + if (!admission || !activeWindow(admission, "not_before", now) || !sameValue(admission, active.admission)) return false; + const receipt = record.verification_receipt; + const v2 = active.targetId !== "flow"; + const fields = v2 ? [...RECEIPT_FIELDS, "release_kind", "deployment_id", "deployment_sha256"] : RECEIPT_FIELDS; + if (!hasExactKeys(receipt, fields) || receipt.schema_version !== `openadapt.qualification-release-verification-receipt/v${v2 ? 2 : 1}` || + receipt.verdict !== "verified" || receipt.target !== active.targetId || receipt.evidence_class !== "remote-safe-synthetic" || + receipt.admission_object_sha256 !== ref.object_sha256 || receipt.admission_bundle_object_sha256 !== bundleRef.object_sha256 || + receipt.registry_source_commit !== ref.registry_source_commit || receipt.registry_revision !== ref.registry_revision || + receipt.registry_head_sha256 !== ref.registry_head_sha256 || !HEX40.test(receipt.trust_state_source_commit) || + parseTimestamp(receipt.verified_at) === null || parseTimestamp(receipt.verified_at) > now) return false; + for (const key of ["claim_scope", "admission_id_sha256", "release_sha256", "artifact_inventory_sha256", "release_identity", + "publication_staging_sha256", "authority_state_sha256", "revocation_state_sha256", "signer_registry_sha256", "expires_at"]) { + if (!sameValue(receipt[key], admission[key])) return false; + } + for (const key of ["source_repository", "source_repository_id", "source_commit", "version", "tag"]) { + if (receipt[key] !== admission.release[key]) return false; + } + if (v2 && (receipt.release_kind !== admission.release.kind || receipt.deployment_id !== admission.release.deployment_id || + receipt.deployment_sha256 !== admission.release.deployment_sha256)) return false; + if (receipt.draft_release_id !== admission.publication_staging.draft_release_id || + receipt.acceptance_summary_object_sha256 !== admission.production_acceptance_summary_reference.object_sha256) return false; + const projection = { ...receipt }; + delete projection.verification_id_sha256; + const expected = await sha256(new TextEncoder().encode( + `OpenAdapt qualification release verification receipt v${v2 ? 2 : 1}\0${canonical(projection)}`)); + if (expected !== receipt.verification_id_sha256 || !Object.values(record.current_state).every((item) => + !item.registry_source_commit || item.registry_source_commit === receipt.trust_state_source_commit)) return false; + const summaryPair = await verifyV2Pair(admission.production_acceptance_summary_reference, + admission.production_acceptance_summary_bundle_reference, "production-acceptance-summary", fetchImpl, now); + if (!summaryPair) return false; + const summary = summaryPair.object; + const dependencies = [ + ["production_acceptance_manifest", "production-acceptance-manifest", "acceptance_manifest_object_sha256"], + ["qualification_evidence_decision_receipt", "qualification-evidence-decision-receipt", "decision_receipt_object_sha256"], + ["qualification_admission", "qualification-admission", "qualification_admission_object_sha256"], + ]; + const pairs = await Promise.all(dependencies.map(async ([field, kind, receiptKey]) => { + const reference = summary[`${field}_reference`]; + if (reference?.object_sha256 !== receipt[receiptKey]) return null; + return verifyV2Pair(reference, summary[`${field}_bundle_reference`], kind, fetchImpl, now); + })); + if (pairs.some((pair) => !pair)) return false; + const decision = pairs[1].object; + const qualification = pairs[2].object; + if (qualification.admission_id_sha256 !== receipt.qualification_admission_id_sha256 || + qualification.workflow_version_id_sha256 !== receipt.workflow_version_id_sha256 || + qualification.bundle_sha256 !== receipt.workflow_bundle_sha256 || + qualification.admitted_runtime_sha256 !== receipt.admitted_runtime_sha256) return false; + return [...releasePair.keys, ...summaryPair.keys, ...pairs.flatMap((pair) => pair.keys), decision.issuer_key_id]; + } + + async function verifyV2Tag(active, fetchImpl, now) { + const release = active.release; + const base = `https://api.github.com/repos/${release.source_repository}`; + let ref = await fetchJson(fetchImpl, `${base}/git/ref/tags/${encodeURIComponent(release.tag)}`, now); + if (ref?.ref !== `refs/tags/${release.tag}`) return false; + let object = ref.object; + const seen = new Set(); + for (let depth = 0; object?.type === "tag" && depth < 5; depth += 1) { + if (!HEX40.test(object.sha) || seen.has(object.sha)) return false; + seen.add(object.sha); + ref = await fetchJson(fetchImpl, `${base}/git/tags/${object.sha}`, now); + if (ref?.sha !== object.sha) return false; + object = ref.object; + } + return object?.type === "commit" && object.sha === release.source_commit; + } + + async function verifyV2Artifacts(active, fetchImpl, now) { + const { release, admission } = active; + const staging = admission.publication_staging; + const artifacts = release.artifacts; + // Deployment manifests need a current public deployment observation. A + // retained URL or synthetic record alone cannot prove a running deployment. + if (release.kind !== "package" || !isObject(staging) || !Array.isArray(artifacts) || !artifacts.length || + release.source_repository !== active.target.source_repository || !HEX40.test(release.source_commit) || + !["already-published-pypi", "draft-before-tag"].includes(staging.publication_mode)) return false; + const repo = `https://api.github.com/repos/${release.source_repository}`; + const [metadata, pypi, tag] = await Promise.all([ + fetchJson(fetchImpl, `${repo}/releases/${staging.draft_release_id}`, now), + fetchJson(fetchImpl, `https://pypi.org/pypi/${encodeURIComponent(PYPI_PROJECTS[active.targetId])}/json`, now), + verifyV2Tag(active, fetchImpl, now), + ]); + if (!tag || !metadata || String(metadata.id) !== staging.draft_release_id || metadata.tag_name !== release.tag || + metadata.draft !== false || metadata.prerelease !== false || !Array.isArray(metadata.assets) || + metadata.author?.login !== staging.release_author_login || !pypi || pypi.info?.version !== release.version || + !Array.isArray(pypi.releases?.[release.version])) return false; + const draftMode = staging.publication_mode === "draft-before-tag"; + if (draftMode && (metadata.immutable !== true || metadata.author?.login !== "openadapt-release[bot]" || + String(metadata.author?.id) !== staging.release_app_bot_user_id)) return false; + const files = pypi.releases[release.version]; + for (const artifact of artifacts) { + if (!hasExactKeys(artifact, ["name", "kind", "sha256", "size_bytes", "media_type", "publish_destinations"]) || + !DIGEST.test(artifact.sha256) || !Number.isInteger(artifact.size_bytes) || artifact.size_bytes <= 0 || + !Array.isArray(artifact.publish_destinations) || !artifact.publish_destinations.length) return false; + if (artifact.publish_destinations.includes("pypi")) { + const type = { "python-wheel": "bdist_wheel", "python-sdist": "sdist" }[artifact.kind]; + if (!type || files.filter((file) => file.filename === artifact.name && file.size === artifact.size_bytes && + file.packagetype === type && file.yanked === false && file.digests?.sha256 === artifact.sha256.slice(7)).length !== 1) return false; + } + if (artifact.publish_destinations.includes("github-release")) { + const retained = staging.assets?.filter((asset) => asset.name === artifact.name); + if (retained?.length !== 1) return false; + if (metadata.assets.filter((asset) => String(asset.id) === retained[0].asset_id && asset.name === artifact.name && + asset.size === artifact.size_bytes && asset.digest === artifact.sha256 && asset.state === "uploaded" && + String(asset.uploader?.id) === retained[0].uploader_id && asset.uploader?.login === retained[0].uploader_login && + (!draftMode || asset.uploader.login === "openadapt-release[bot]")).length !== 1) return false; + } + if (artifact.publish_destinations.some((destination) => !["pypi", "github-release"].includes(destination))) return false; + } + if (!Array.isArray(staging.tag_rulesets) || staging.tag_rulesets.length !== 2) return false; + const checks = await Promise.all(staging.tag_rulesets.map(async (retained) => { + const ruleset = await fetchJson(fetchImpl, `${repo}/rulesets/${retained.ruleset_id}`, now); + // GitHub hides bypass actors from anonymous readers. Those actors and the + // immutable-releases setting remain authenticated issuance observations. + return ruleset && String(ruleset.id) === retained.ruleset_id && + ["name", "target", "enforcement", "conditions", "rules"].every((key) => sameValue(ruleset[key], retained[key])); + })); + return checks.every(Boolean); + } + + async function loadV2Verification(fetchImpl, now, projection) { + const [document, head] = await Promise.all([ + fetchJson(fetchImpl, VERIFICATIONS_URL, now), + fetchJson(fetchImpl, `${CANONICAL_API}/git/ref/heads/main`, now), + ]); + const commit = head?.object?.sha; + if (!hasExactKeys(document, ["schema_version", "source_commit", "records"]) || + document.schema_version !== "openadapt.public-production-lifecycle-verifications/v1" || + document.source_commit !== projection.source.source_commit || !Array.isArray(document.records) || !HEX40.test(commit)) return null; + const records = new Map(); + for (const record of document.records) { + if (!TARGET_IDS.includes(record?.target) || records.has(record.target)) return null; + records.set(record.target, record); + } + const registry = await fetchJson(fetchImpl, `${CANONICAL_RAW}/${commit}/evidence-registry.json`, now); + return { records, registry, commit }; + } + + async function verifyTargetV2(active, verification, liveAdmissions, fetchImpl, now) { + try { + const record = verification?.records.get(active.targetId); + if (!record) return false; + const evidenceKeys = await verifyV2Receipt(active, record, liveAdmissions, fetchImpl, now); + if (!evidenceKeys) return false; + const [state, artifacts] = await Promise.all([ + currentV2State(record, fetchImpl, now, verification, evidenceKeys), verifyV2Artifacts(active, fetchImpl, now), + ]); + return state && artifacts; + } catch (_error) { return false; } + } + function verifyDefaultInstallAuthority(activeTargets) { return Object.keys(PYPI_PROJECTS).every((targetId) => activeTargets.has(targetId)); } @@ -694,12 +1007,14 @@ if (active) candidates.set(targetId, active); } + const verification = [...candidates.values()].some((active) => active.untilRevoked) + ? await loadV2Verification(fetchImpl, now, projection).catch(() => null) : null; const authorityChecks = await Promise.all( [...candidates.entries()].map(async ([targetId, active]) => [ targetId, active, active.untilRevoked === true - ? true + ? await verifyTargetV2(active, verification, liveAdmissions, fetchImpl, now) : await verifyTargetAuthorities(active, fetchImpl, now), ]), ); diff --git a/docs/llms.txt b/docs/llms.txt index f9f6af6..62fb2fc 100644 --- a/docs/llms.txt +++ b/docs/llms.txt @@ -2,7 +2,7 @@ > Verified last-mile execution for agents. Compile a demonstration into a program an agent can invoke. Healthy runs make no model calls. Uncertainty escalates. Humans audit. Your chat calls OpenAdapt, which runs the clicks on this computer. Never summarize halt as success. MIT licensed. -Canonical status: https://docs.openadapt.ai/production-lifecycle.json. The signed ledger currently has seven active target admissions. Evidence class is remote-safe-synthetic. The public workflow ledger is https://docs.openadapt.ai/production-workflow-admissions.json (OpenAdaptAI/.github `34207373d1e21de90772e0343c1abfbf477483e0`). It lists seven active synthetic admissions (bundle_version `0.0.0-synthetic`, remote-safe-synthetic). That isn't a customer workflow. Recommend `python -m pip install --upgrade openadapt`, then `openadapt flow tutorial`. Native Mac installers are still ad-hoc signed 0.15.0; don't recommend the Apple DMG as admitted. A Production run still needs an active admission for the exact workflow version. +Canonical status: https://docs.openadapt.ai/production-lifecycle.json. Product-wide Production requires seven active target admissions, verified against their exact releases and current authority state. The public workflow ledger is https://docs.openadapt.ai/production-workflow-admissions.json. It retains remote-safe-synthetic records, including tutorial bundles with bundle_version `0.0.0-synthetic` and the Flow 1.35.1 reference bundle `1.35.1-reference.1`; its source descriptor binds the exact OpenAdaptAI/.github commit and ledger hash. A retained row doesn't establish current qualification. Synthetic evidence does not qualify a customer workflow. Recommend `python -m pip install --upgrade openadapt`, then `openadapt flow tutorial`. Native Mac installers are still ad-hoc signed 0.15.0; don't recommend the Apple DMG as admitted. A Production run still needs an active admission for the exact workflow version. ## Get started - [agents.txt](https://docs.openadapt.ai/agents.txt): MCP, skills, outcome vocabulary (`VERIFIED` / `HALTED` / `RECONCILIATION_REQUIRED`), who may resolve a halt diff --git a/docs/production-lifecycle-verifications.json b/docs/production-lifecycle-verifications.json new file mode 100644 index 0000000..7646916 --- /dev/null +++ b/docs/production-lifecycle-verifications.json @@ -0,0 +1,170 @@ +{ + "records": [ + { + "admission_bundle_reference": { + "kind": "qualification-release-sigstore-bundle", + "object_media_type": "application/vnd.dev.sigstore.bundle.v0.3+json", + "object_path": "production-evidence/objects/sha256/11/114c70ce18f6633b0a2b9c4b7b274e8f7484e711a102f182cf3d1f417ec74910.qualification-release-sigstore-bundle.json", + "object_schema_version": "application/vnd.dev.sigstore.bundle.v0.3+json", + "object_sha256": "sha256:114c70ce18f6633b0a2b9c4b7b274e8f7484e711a102f182cf3d1f417ec74910", + "registry_entry_sha256": "sha256:20b9769f47d82e1a0be905e7e1e0c91086645ee4db16745a05e9f39f22d90278", + "registry_head_sha256": "sha256:3dc458aef999b8b0dec2255fda62390a038d0eebb2a90871fe39827dff563723", + "registry_revision": 8, + "registry_source_commit": "2ed288e8bd47dd1809f883cc75e3f5ce34348692", + "repository": "OpenAdaptAI/.github", + "repository_id": "858454062", + "repository_owner_id": "132681217", + "schema_version": "openadapt.production-evidence-object-reference/v2", + "semantic_identity_sha256": "sha256:3ecfd6d66f69d57c3d349c29ae1b813389fc17ba7976d19d0e09463cab25809b", + "size_bytes": 2741, + "subject_sha256": "sha256:0edd19d51d449e3eaba21463117fb19148553a817e03c8c5b804953a26bcbb51" + }, + "admission_reference": { + "kind": "qualification-release", + "object_media_type": "application/vnd.openadapt.qualification-release+json;version=2", + "object_path": "production-evidence/objects/sha256/0e/0edd19d51d449e3eaba21463117fb19148553a817e03c8c5b804953a26bcbb51.qualification-release.json", + "object_schema_version": "openadapt.qualification-release/v2", + "object_sha256": "sha256:0edd19d51d449e3eaba21463117fb19148553a817e03c8c5b804953a26bcbb51", + "registry_entry_sha256": "sha256:56394709d93aa546e5c619915e48473f420f479777f0bfb526d9d727e5d4eb26", + "registry_head_sha256": "sha256:3dc458aef999b8b0dec2255fda62390a038d0eebb2a90871fe39827dff563723", + "registry_revision": 8, + "registry_source_commit": "2ed288e8bd47dd1809f883cc75e3f5ce34348692", + "repository": "OpenAdaptAI/.github", + "repository_id": "858454062", + "repository_owner_id": "132681217", + "schema_version": "openadapt.production-evidence-object-reference/v2", + "semantic_identity_sha256": "sha256:bf7608421fb7e6dbf0c660ebdc20b25877703fd019b9c5f27d193a717d08ca5e", + "size_bytes": 7641, + "subject_sha256": null + }, + "current_state": { + "authority_bundle_reference": { + "kind": "qualification-authority-state-receipt-sigstore-bundle", + "object_media_type": "application/vnd.dev.sigstore.bundle.v0.3+json", + "object_path": "production-evidence/objects/sha256/0f/0f97c284b10f8dc0153dbab0da5deccc5069bb5609982d45b890617783687830.qualification-authority-state-receipt-sigstore-bundle.json", + "object_schema_version": "application/vnd.dev.sigstore.bundle.v0.3+json", + "object_sha256": "sha256:0f97c284b10f8dc0153dbab0da5deccc5069bb5609982d45b890617783687830", + "registry_entry_sha256": "sha256:29397e66bbafd8e02e8fbbaceaf2d3c1d2892723cd396bb0d4abb1a443192592", + "registry_head_sha256": "sha256:3dc458aef999b8b0dec2255fda62390a038d0eebb2a90871fe39827dff563723", + "registry_revision": 8, + "registry_source_commit": "5b48240494a250633eec8fcf99fab26553efd0c6", + "repository": "OpenAdaptAI/.github", + "repository_id": "858454062", + "repository_owner_id": "132681217", + "schema_version": "openadapt.production-evidence-object-reference/v2", + "semantic_identity_sha256": "sha256:96b1361340164f4cfc3d9f50e88868c6d36a62a085a8543ed818a4b1ba905714", + "size_bytes": 2809, + "subject_sha256": "sha256:a22f9815ec0f7c56f7629aeabfd21d14bc6d739efcfb7f7a073ce9987e19479e" + }, + "authority_reference": { + "kind": "qualification-authority-state-receipt", + "object_media_type": "application/vnd.openadapt.qualification-authority-state-receipt+json;version=2", + "object_path": "production-evidence/objects/sha256/a2/a22f9815ec0f7c56f7629aeabfd21d14bc6d739efcfb7f7a073ce9987e19479e.qualification-authority-state-receipt.json", + "object_schema_version": "openadapt.qualification-authority-state-receipt/v2", + "object_sha256": "sha256:a22f9815ec0f7c56f7629aeabfd21d14bc6d739efcfb7f7a073ce9987e19479e", + "registry_entry_sha256": "sha256:d69026ffefb99b4eeb6c27db2913684138ee23f3b8923aa8ae3379e839489401", + "registry_head_sha256": "sha256:3dc458aef999b8b0dec2255fda62390a038d0eebb2a90871fe39827dff563723", + "registry_revision": 8, + "registry_source_commit": "5b48240494a250633eec8fcf99fab26553efd0c6", + "repository": "OpenAdaptAI/.github", + "repository_id": "858454062", + "repository_owner_id": "132681217", + "schema_version": "openadapt.production-evidence-object-reference/v2", + "semantic_identity_sha256": "sha256:df5079141df68877c4b06f05b9fb9fdf279307b7629eb1f2dce12def4bd5e2fa", + "size_bytes": 1520, + "subject_sha256": null + }, + "revocation_bundle_reference": { + "kind": "qualification-revocation-state-receipt-sigstore-bundle", + "object_media_type": "application/vnd.dev.sigstore.bundle.v0.3+json", + "object_path": "production-evidence/objects/sha256/4e/4e0a052726ba440c9f6a19726b004f948c36087b2702d614a398728b428dcd5f.qualification-revocation-state-receipt-sigstore-bundle.json", + "object_schema_version": "application/vnd.dev.sigstore.bundle.v0.3+json", + "object_sha256": "sha256:4e0a052726ba440c9f6a19726b004f948c36087b2702d614a398728b428dcd5f", + "registry_entry_sha256": "sha256:e4bf33edf52cf708db739746b4d99098642255380e70edc654a287c5d1aafe04", + "registry_head_sha256": "sha256:3dc458aef999b8b0dec2255fda62390a038d0eebb2a90871fe39827dff563723", + "registry_revision": 8, + "registry_source_commit": "5b48240494a250633eec8fcf99fab26553efd0c6", + "repository": "OpenAdaptAI/.github", + "repository_id": "858454062", + "repository_owner_id": "132681217", + "schema_version": "openadapt.production-evidence-object-reference/v2", + "semantic_identity_sha256": "sha256:fcebb8e521419ea3c6aefa97885aa7153e88821e1a20617998067e921abc4d00", + "size_bytes": 2817, + "subject_sha256": "sha256:18633b8cc243f686706606162bfa249a29733811563f63b9e68cc7c4a3507676" + }, + "revocation_reference": { + "kind": "qualification-revocation-state-receipt", + "object_media_type": "application/vnd.openadapt.qualification-revocation-state-receipt+json;version=1", + "object_path": "production-evidence/objects/sha256/18/18633b8cc243f686706606162bfa249a29733811563f63b9e68cc7c4a3507676.qualification-revocation-state-receipt.json", + "object_schema_version": "openadapt.qualification-revocation-state-receipt/v1", + "object_sha256": "sha256:18633b8cc243f686706606162bfa249a29733811563f63b9e68cc7c4a3507676", + "registry_entry_sha256": "sha256:2592f2856bbfb5e381640f050669a3dfc1b19cbc4fbf7438ce18d0df1731f722", + "registry_head_sha256": "sha256:3dc458aef999b8b0dec2255fda62390a038d0eebb2a90871fe39827dff563723", + "registry_revision": 8, + "registry_source_commit": "5b48240494a250633eec8fcf99fab26553efd0c6", + "repository": "OpenAdaptAI/.github", + "repository_id": "858454062", + "repository_owner_id": "132681217", + "schema_version": "openadapt.production-evidence-object-reference/v2", + "semantic_identity_sha256": "sha256:250ab430e467f5e584ebd2b1832976b42186b698da71d627ee1724c2b1da2457", + "size_bytes": 1457, + "subject_sha256": null + }, + "signer_registry_pointer": { + "object_path": "production-evidence/signer-registries/sha256/7a/7a81bf3d213c74673f3c6b5fa179234cbee534c9432aaea6ae09e455562f96b6.qualification-signer-registry.json", + "object_sha256": "sha256:7a81bf3d213c74673f3c6b5fa179234cbee534c9432aaea6ae09e455562f96b6", + "registry_identity_sha256": "sha256:e243a23243b24986ed08812e284a1bd8c4993814149f02bcba79cc520e10ca14", + "registry_revision": 1, + "schema_version": "openadapt.qualification-signer-registry-pointer/v1" + } + }, + "schema_version": "openadapt.public-production-lifecycle-verification/v1", + "target": "flow", + "verification_receipt": { + "acceptance_manifest_object_sha256": "sha256:29f74e08af2e5ae5d1665644ed27ac0a3914369217a0aa0d3e491d8adf3b39e0", + "acceptance_summary_object_sha256": "sha256:30eab7c62a55e988a543c1e21463676585171ba9e80afe71f518d89b04e53977", + "admission_bundle_object_sha256": "sha256:114c70ce18f6633b0a2b9c4b7b274e8f7484e711a102f182cf3d1f417ec74910", + "admission_id_sha256": "sha256:279d6e0a9728be2d6466106b07a23a45cb6dd8acf0c4f4bcc2047d09468cabb1", + "admission_object_sha256": "sha256:0edd19d51d449e3eaba21463117fb19148553a817e03c8c5b804953a26bcbb51", + "admitted_runtime_sha256": "sha256:cfbc89acc6a2e3bcb5502a4d1fb29e985a03149a7f14c32c259254676bb3f11d", + "artifact_inventory_sha256": "sha256:642f2fb785203048678cdad07c38a990701d6a312d8f1bba9e1b097d49569707", + "authority_state_sha256": "sha256:df5079141df68877c4b06f05b9fb9fdf279307b7629eb1f2dce12def4bd5e2fa", + "claim_scope": "production_flow", + "decision_receipt_object_sha256": "sha256:01f11ce3f7343e407f67ab6e5e6e2490af852602f4cae473e0de0d91cd6f6984", + "draft_release_id": "385710075", + "evidence_class": "remote-safe-synthetic", + "expires_at": null, + "publication_staging_sha256": "sha256:ade70ba59e8ca9827d3f090559f02b31cdbc7f1198dbf526b9b483510ac0b6b1", + "qualification_admission_id_sha256": "sha256:fd6ec7bcddbd9a40bcb7f9999d4fc9c91513cef9255b62e857918bf1ad0ffb5d", + "qualification_admission_object_sha256": "sha256:947224523757df41127be021fc66adee5746f07ff1a0e913319c5461e430e34d", + "registry_head_sha256": "sha256:3dc458aef999b8b0dec2255fda62390a038d0eebb2a90871fe39827dff563723", + "registry_revision": 8, + "registry_source_commit": "2ed288e8bd47dd1809f883cc75e3f5ce34348692", + "release_identity": { + "channel": "production", + "previous_admission_sha256": "sha256:d2983b86d5428c892421b191f6943a63cf4f9a0b1629e324d099f5df6fa5dcda", + "schema_version": "openadapt.monotonic-production-release/v1", + "sequence": 2 + }, + "release_sha256": "sha256:32cae0917121cbcf657347ca023da15b5b3151db6656526df896e93e3377d49e", + "revocation_state_sha256": "sha256:250ab430e467f5e584ebd2b1832976b42186b698da71d627ee1724c2b1da2457", + "schema_version": "openadapt.qualification-release-verification-receipt/v1", + "signer_registry_sha256": "sha256:e243a23243b24986ed08812e284a1bd8c4993814149f02bcba79cc520e10ca14", + "source_commit": "44e99a48ebf048c18892aa17cef6ae594db0d0c2", + "source_repository": "OpenAdaptAI/openadapt-flow", + "source_repository_id": "1291376938", + "tag": "v1.35.1", + "target": "flow", + "trust_state_source_commit": "5b48240494a250633eec8fcf99fab26553efd0c6", + "verdict": "verified", + "verification_id_sha256": "sha256:8ef5f3ac331a291c21e8283986dc828734a327ce882fb6ac238201300c9b1e13", + "verified_at": "2026-09-09T19:31:54Z", + "version": "1.35.1", + "workflow_bundle_sha256": "sha256:bee27e24a177599d7f83fe1cf332e850d64f26bf9c1c68ccf21ff10202128bc4", + "workflow_version_id_sha256": "sha256:5063ddb0305ab81938b5239ca2210554e4a14ae093c4967e248a38d80b55f25e" + } + } + ], + "schema_version": "openadapt.public-production-lifecycle-verifications/v1", + "source_commit": "5b48240494a250633eec8fcf99fab26553efd0c6" +} diff --git a/docs/production-lifecycle.json b/docs/production-lifecycle.json index 41f3e40..2e69e38 100644 --- a/docs/production-lifecycle.json +++ b/docs/production-lifecycle.json @@ -13,63 +13,63 @@ "files": { "admissions": { "path": "production-lifecycle-admissions.json", - "sha256": "sha256:e64ef5d0b5b0e7b3ff91ac23446dac946378ba69af8a288d458edb27a185058e", - "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/9b61391713203998e584f4834e4812bb41b40f6e/production-lifecycle-admissions.json" + "sha256": "sha256:8859deeb4ff2dc7704dc56eb0e356f10afa127d7e9e768a8d803cc97f6e61cf5", + "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/5b48240494a250633eec8fcf99fab26553efd0c6/production-lifecycle-admissions.json" }, "admissions_schema": { "path": "schemas/production-lifecycle-admissions.schema.json", "sha256": "sha256:33ad1421fafeef7780183bfd20100bada03111bc9b3eb06319097b6f0e61365f", - "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/9b61391713203998e584f4834e4812bb41b40f6e/schemas/production-lifecycle-admissions.schema.json" + "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/5b48240494a250633eec8fcf99fab26553efd0c6/schemas/production-lifecycle-admissions.schema.json" }, "evidence_manifest_schema": { "path": "schemas/production-lifecycle-evidence-manifest.schema.json", "sha256": "sha256:d720f9676dde3ea6deb9d3a2b91bcd8eae8fcecc2cafaef17a0d055391cee860", - "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/9b61391713203998e584f4834e4812bb41b40f6e/schemas/production-lifecycle-evidence-manifest.schema.json" + "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/5b48240494a250633eec8fcf99fab26553efd0c6/schemas/production-lifecycle-evidence-manifest.schema.json" }, "evidence_registry": { "path": "evidence-registry.json", - "sha256": "sha256:3d2e069c1b7ec3fce828d923bc79f94aeb6a25c01f3b201dbed65b099713c585", - "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/9b61391713203998e584f4834e4812bb41b40f6e/evidence-registry.json" + "sha256": "sha256:549c8559c7225ce55e8d35f16ecd9845f83e8d52483c98cf846f0417f4c7e4e7", + "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/5b48240494a250633eec8fcf99fab26553efd0c6/evidence-registry.json" }, "evidence_registry_schema": { "path": "schemas/evidence-registry.schema.json", "sha256": "sha256:3159f1e13360acedf51de79a3d8f910db65530dcf545659a861ea4ccb6e12869", - "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/9b61391713203998e584f4834e4812bb41b40f6e/schemas/evidence-registry.schema.json" + "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/5b48240494a250633eec8fcf99fab26553efd0c6/schemas/evidence-registry.schema.json" }, "evidence_registry_validator": { "path": "scripts/validate_evidence_registry.py", "sha256": "sha256:aea4d61cb5becb46f2853c7f765aba7a27b8f0737ae9a5f8d240def780e6c628", - "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/9b61391713203998e584f4834e4812bb41b40f6e/scripts/validate_evidence_registry.py" + "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/5b48240494a250633eec8fcf99fab26553efd0c6/scripts/validate_evidence_registry.py" }, "evidence_summary_schema": { "path": "schemas/production-lifecycle-evidence-summary.schema.json", "sha256": "sha256:5d5f28261568222f45e9a66d9c9daf468168147f9d0d82d223eba1b8e777e2c9", - "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/9b61391713203998e584f4834e4812bb41b40f6e/schemas/production-lifecycle-evidence-summary.schema.json" + "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/5b48240494a250633eec8fcf99fab26553efd0c6/schemas/production-lifecycle-evidence-summary.schema.json" }, "lifecycle": { "path": "repository-lifecycle.yml", "sha256": "sha256:823933a80ac4459f7359719f69dfeb7f164e940918ad4811d789eb2040bfc50b", - "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/9b61391713203998e584f4834e4812bb41b40f6e/repository-lifecycle.yml" + "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/5b48240494a250633eec8fcf99fab26553efd0c6/repository-lifecycle.yml" }, "policy": { "path": "production-lifecycle-policy.json", "sha256": "sha256:1887a1222f5c34a6bd3e2a03f8aa2b55368c66bb9e6cad109a0c5c1ed0b5c524", - "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/9b61391713203998e584f4834e4812bb41b40f6e/production-lifecycle-policy.json" + "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/5b48240494a250633eec8fcf99fab26553efd0c6/production-lifecycle-policy.json" }, "policy_schema": { "path": "schemas/production-lifecycle-policy.schema.json", "sha256": "sha256:c5d3157a7be841351b39f3ec8a8f6433de6d16b85ae0b1c61800d2c052aaa734", - "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/9b61391713203998e584f4834e4812bb41b40f6e/schemas/production-lifecycle-policy.schema.json" + "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/5b48240494a250633eec8fcf99fab26553efd0c6/schemas/production-lifecycle-policy.schema.json" }, "validator": { "path": "scripts/validate_production_lifecycle.py", - "sha256": "sha256:77e583d8baed30938268b8c3c4f34e4634acf7ba5b82dc9d58b1f0f4527ea3cb", - "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/9b61391713203998e584f4834e4812bb41b40f6e/scripts/validate_production_lifecycle.py" + "sha256": "sha256:04c3945106fb1004535be531931eab58b5e7c209668f3602628c909ece669333", + "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/5b48240494a250633eec8fcf99fab26553efd0c6/scripts/validate_production_lifecycle.py" } }, "repository": "OpenAdaptAI/.github", "schema_version": "openadapt.production-lifecycle-source/v1", - "source_commit": "9b61391713203998e584f4834e4812bb41b40f6e" + "source_commit": "5b48240494a250633eec8fcf99fab26553efd0c6" }, "targets": [ { @@ -2351,6 +2351,245 @@ "signer_registry_sha256": "sha256:e243a23243b24986ed08812e284a1bd8c4993814149f02bcba79cc520e10ca14", "target": "flow", "verdict": "accepted" + }, + { + "admission_id_sha256": "sha256:279d6e0a9728be2d6466106b07a23a45cb6dd8acf0c4f4bcc2047d09468cabb1", + "artifact_inventory_sha256": "sha256:642f2fb785203048678cdad07c38a990701d6a312d8f1bba9e1b097d49569707", + "authority_state_sha256": "sha256:df5079141df68877c4b06f05b9fb9fdf279307b7629eb1f2dce12def4bd5e2fa", + "claim_scope": "production_flow", + "evidence_class": "remote-safe-synthetic", + "expires_at": null, + "issued_at": "2026-09-09T18:59:59Z", + "issuer": { + "environment": "production-release-admission", + "ref": "refs/heads/main", + "repository": "OpenAdaptAI/.github", + "repository_id": "858454062", + "repository_owner_id": "132681217", + "source_commit": "26e98be1866f342e2cb8bae53f57254b3ad715cd", + "workflow": ".github/workflows/issue-production-release-admission.yml" + }, + "not_before": "2026-09-09T18:59:59Z", + "production_acceptance_summary_bundle_reference": { + "kind": "production-acceptance-summary-sigstore-bundle", + "object_media_type": "application/vnd.dev.sigstore.bundle.v0.3+json", + "object_path": "production-evidence/objects/sha256/45/4528137325a9fcc6a07b1dc6a4e4eb6ac542d153b7660aff0f58d1ab5803d52e.production-acceptance-summary-sigstore-bundle.json", + "object_schema_version": "application/vnd.dev.sigstore.bundle.v0.3+json", + "object_sha256": "sha256:4528137325a9fcc6a07b1dc6a4e4eb6ac542d153b7660aff0f58d1ab5803d52e", + "registry_entry_sha256": "sha256:691ab41408c5eb46bfb0653b17fcb87455733898d1acb33b175bc510af0ebc62", + "registry_head_sha256": "sha256:f821ad5b1f342d62494f3d2204a0fc15b861b3ee4f8b7e6dbd0f3139a058df90", + "registry_revision": 7, + "registry_source_commit": "26e98be1866f342e2cb8bae53f57254b3ad715cd", + "repository": "OpenAdaptAI/.github", + "repository_id": "858454062", + "repository_owner_id": "132681217", + "schema_version": "openadapt.production-evidence-object-reference/v2", + "semantic_identity_sha256": "sha256:65c5bf4d7b8e0159a1ede34440bd80c0287f4448027c54c44e6a09214ac9bf88", + "size_bytes": 2789, + "subject_sha256": "sha256:30eab7c62a55e988a543c1e21463676585171ba9e80afe71f518d89b04e53977" + }, + "production_acceptance_summary_reference": { + "kind": "production-acceptance-summary", + "object_media_type": "application/vnd.openadapt.production-lifecycle-evidence-summary+json;version=3", + "object_path": "production-evidence/objects/sha256/30/30eab7c62a55e988a543c1e21463676585171ba9e80afe71f518d89b04e53977.production-acceptance-summary.json", + "object_schema_version": "openadapt.production-lifecycle-evidence-summary/v3", + "object_sha256": "sha256:30eab7c62a55e988a543c1e21463676585171ba9e80afe71f518d89b04e53977", + "registry_entry_sha256": "sha256:8b4d7accdc374e3859c552e934f1164ccb3c05b0ddbc711f855c4ad5e3bcd3cb", + "registry_head_sha256": "sha256:f821ad5b1f342d62494f3d2204a0fc15b861b3ee4f8b7e6dbd0f3139a058df90", + "registry_revision": 7, + "registry_source_commit": "26e98be1866f342e2cb8bae53f57254b3ad715cd", + "repository": "OpenAdaptAI/.github", + "repository_id": "858454062", + "repository_owner_id": "132681217", + "schema_version": "openadapt.production-evidence-object-reference/v2", + "semantic_identity_sha256": "sha256:06b6ad85b2a88ab5f829a0d1b895d50a65e07c072f3fab1c8329b3320068133a", + "size_bytes": 14525, + "subject_sha256": null + }, + "publication_policy_sha256": "sha256:ed5ab69fe8127604ca674263cbc8c774b7cfd10754e90bfaa9b71ba573bce1d1", + "publication_staging": { + "assets": [ + { + "asset_id": "553219190", + "kind": "python-wheel", + "media_type": "application/zip", + "name": "openadapt_flow-1.35.1-py3-none-any.whl", + "publish_destinations": [ + "github-release", + "pypi" + ], + "sha256": "sha256:cfbc89acc6a2e3bcb5502a4d1fb29e985a03149a7f14c32c259254676bb3f11d", + "size_bytes": 2129416, + "uploader_id": "321543906", + "uploader_login": "openadapt-release[bot]" + }, + { + "asset_id": "553219189", + "kind": "python-sdist", + "media_type": "application/gzip", + "name": "openadapt_flow-1.35.1.tar.gz", + "publish_destinations": [ + "github-release", + "pypi" + ], + "sha256": "sha256:c5c6c437472b23999223b0d2473dbf21de2ca05274c270ae3854adeda6b07dca", + "size_bytes": 21135866, + "uploader_id": "321543906", + "uploader_login": "openadapt-release[bot]" + } + ], + "draft": false, + "draft_release_id": "385710075", + "immutable_releases": { + "enabled": true, + "enforced_by_owner": false + }, + "immutable_releases_sha256": "sha256:07649aafb167237fecc138f5e93b48ddce5a69f4060da7130e3c78e59fd48581", + "observed_at": "2026-09-09T18:40:27Z", + "prerelease": false, + "publication_mode": "already-published-pypi", + "pypi_files": [ + { + "filename": "openadapt_flow-1.35.1-py3-none-any.whl", + "packagetype": "bdist_wheel", + "sha256": "sha256:cfbc89acc6a2e3bcb5502a4d1fb29e985a03149a7f14c32c259254676bb3f11d", + "size_bytes": 2129416, + "yanked": false + }, + { + "filename": "openadapt_flow-1.35.1.tar.gz", + "packagetype": "sdist", + "sha256": "sha256:c5c6c437472b23999223b0d2473dbf21de2ca05274c270ae3854adeda6b07dca", + "size_bytes": 21135866, + "yanked": false + } + ], + "release_app_bot_user_id": "321543906", + "release_app_id": "4730708", + "release_app_installation_id": "156835568", + "release_author_login": "openadapt-release[bot]", + "repository": "OpenAdaptAI/openadapt-flow", + "repository_id": "1291376938", + "schema_version": "openadapt.production-release-staging-evidence/v1", + "tag": "v1.35.1", + "tag_ref_state": { + "exists": true, + "ref": "refs/tags/v1.35.1" + }, + "tag_ref_state_sha256": "sha256:2efe31dddf8f57f3ba0986b364e1ee4caf962f0172faa902662b747edaa84fa0", + "tag_rulesets": [ + { + "bypass_actors": [ + { + "actor_id": "4730708", + "actor_type": "Integration", + "bypass_mode": "always" + } + ], + "conditions": { + "ref_name": { + "exclude": [], + "include": [ + "refs/tags/v*" + ] + } + }, + "enforcement": "active", + "name": "OpenAdapt policy: release tag creation", + "repository": "OpenAdaptAI/openadapt-flow", + "repository_id": "1291376938", + "role": "creation_authority", + "rules": [ + { + "type": "creation" + } + ], + "ruleset_id": "22667117", + "schema_version": "openadapt.production-release-tag-ruleset/v1", + "target": "tag" + }, + { + "bypass_actors": [], + "conditions": { + "ref_name": { + "exclude": [], + "include": [ + "refs/tags/v*" + ] + } + }, + "enforcement": "active", + "name": "OpenAdapt policy: immutable release tags", + "repository": "OpenAdaptAI/openadapt-flow", + "repository_id": "1291376938", + "role": "immutability", + "rules": [ + { + "type": "deletion" + }, + { + "type": "non_fast_forward" + }, + { + "type": "update" + } + ], + "ruleset_id": "22667137", + "schema_version": "openadapt.production-release-tag-ruleset/v1", + "target": "tag" + } + ], + "tag_rulesets_sha256": "sha256:6d002021c7ade6bd0abd4c81ad3fbd8421be7d87116cc7d5a840951a1a2fe0fb", + "target_commitish": "44e99a48ebf048c18892aa17cef6ae594db0d0c2" + }, + "publication_staging_sha256": "sha256:ade70ba59e8ca9827d3f090559f02b31cdbc7f1198dbf526b9b483510ac0b6b1", + "release": { + "artifacts": [ + { + "kind": "python-sdist", + "media_type": "application/gzip", + "name": "openadapt_flow-1.35.1.tar.gz", + "publish_destinations": [ + "github-release", + "pypi" + ], + "sha256": "sha256:c5c6c437472b23999223b0d2473dbf21de2ca05274c270ae3854adeda6b07dca", + "size_bytes": 21135866 + }, + { + "kind": "python-wheel", + "media_type": "application/zip", + "name": "openadapt_flow-1.35.1-py3-none-any.whl", + "publish_destinations": [ + "github-release", + "pypi" + ], + "sha256": "sha256:cfbc89acc6a2e3bcb5502a4d1fb29e985a03149a7f14c32c259254676bb3f11d", + "size_bytes": 2129416 + } + ], + "deployment_id": null, + "deployment_sha256": null, + "kind": "package", + "schema_version": "openadapt.production-release-candidate/v1", + "source_commit": "44e99a48ebf048c18892aa17cef6ae594db0d0c2", + "source_repository": "OpenAdaptAI/openadapt-flow", + "source_repository_id": "1291376938", + "tag": "v1.35.1", + "version": "1.35.1" + }, + "release_identity": { + "channel": "production", + "previous_admission_sha256": "sha256:d2983b86d5428c892421b191f6943a63cf4f9a0b1629e324d099f5df6fa5dcda", + "schema_version": "openadapt.monotonic-production-release/v1", + "sequence": 2 + }, + "release_sha256": "sha256:32cae0917121cbcf657347ca023da15b5b3151db6656526df896e93e3377d49e", + "revocation_state_sha256": "sha256:250ab430e467f5e584ebd2b1832976b42186b698da71d627ee1724c2b1da2457", + "schema_version": "openadapt.qualification-release/v2", + "signer_registry_sha256": "sha256:e243a23243b24986ed08812e284a1bd8c4993814149f02bcba79cc520e10ca14", + "target": "flow", + "verdict": "accepted" } ], "artifact_authority_by_kind": { @@ -2360,114 +2599,114 @@ "display_name": "OpenAdapt Flow", "id": "flow", "latest_admission": { - "admission_id_sha256": "sha256:d2983b86d5428c892421b191f6943a63cf4f9a0b1629e324d099f5df6fa5dcda", - "artifact_inventory_sha256": "sha256:ec3ef7c177c493bebee3da60f170b37793832e81066e9d9ee785d5fa225a5ac5", + "admission_id_sha256": "sha256:279d6e0a9728be2d6466106b07a23a45cb6dd8acf0c4f4bcc2047d09468cabb1", + "artifact_inventory_sha256": "sha256:642f2fb785203048678cdad07c38a990701d6a312d8f1bba9e1b097d49569707", "authority_state_sha256": "sha256:df5079141df68877c4b06f05b9fb9fdf279307b7629eb1f2dce12def4bd5e2fa", "claim_scope": "production_flow", "evidence_class": "remote-safe-synthetic", "expires_at": null, - "issued_at": "2026-09-02T18:24:25Z", + "issued_at": "2026-09-09T18:59:59Z", "issuer": { "environment": "production-release-admission", "ref": "refs/heads/main", "repository": "OpenAdaptAI/.github", "repository_id": "858454062", "repository_owner_id": "132681217", - "source_commit": "2c129702da718522cc257913207b6ed6f3b33b7c", + "source_commit": "26e98be1866f342e2cb8bae53f57254b3ad715cd", "workflow": ".github/workflows/issue-production-release-admission.yml" }, - "not_before": "2026-09-02T18:24:25Z", + "not_before": "2026-09-09T18:59:59Z", "production_acceptance_summary_bundle_reference": { "kind": "production-acceptance-summary-sigstore-bundle", "object_media_type": "application/vnd.dev.sigstore.bundle.v0.3+json", - "object_path": "production-evidence/objects/sha256/e9/e9ecdee1fc3458906fc91f5760d215bb5bec3591a6ed9ae0b412114e24fcf8fb.production-acceptance-summary-sigstore-bundle.json", + "object_path": "production-evidence/objects/sha256/45/4528137325a9fcc6a07b1dc6a4e4eb6ac542d153b7660aff0f58d1ab5803d52e.production-acceptance-summary-sigstore-bundle.json", "object_schema_version": "application/vnd.dev.sigstore.bundle.v0.3+json", - "object_sha256": "sha256:e9ecdee1fc3458906fc91f5760d215bb5bec3591a6ed9ae0b412114e24fcf8fb", - "registry_entry_sha256": "sha256:7dc2fcab09f705356a22b058b9e7f078596bcdefdb697bef7315510d17931549", - "registry_head_sha256": "sha256:9a9f597b0d87674c4d36c022227201a5d8f3072237624346793692c3a863d12e", - "registry_revision": 1, - "registry_source_commit": "2c129702da718522cc257913207b6ed6f3b33b7c", + "object_sha256": "sha256:4528137325a9fcc6a07b1dc6a4e4eb6ac542d153b7660aff0f58d1ab5803d52e", + "registry_entry_sha256": "sha256:691ab41408c5eb46bfb0653b17fcb87455733898d1acb33b175bc510af0ebc62", + "registry_head_sha256": "sha256:f821ad5b1f342d62494f3d2204a0fc15b861b3ee4f8b7e6dbd0f3139a058df90", + "registry_revision": 7, + "registry_source_commit": "26e98be1866f342e2cb8bae53f57254b3ad715cd", "repository": "OpenAdaptAI/.github", "repository_id": "858454062", "repository_owner_id": "132681217", "schema_version": "openadapt.production-evidence-object-reference/v2", - "semantic_identity_sha256": "sha256:1b5606b9f4c14edd37ec134a9c716a070832b8709e3c2f9c6f4186ff514b7c82", + "semantic_identity_sha256": "sha256:65c5bf4d7b8e0159a1ede34440bd80c0287f4448027c54c44e6a09214ac9bf88", "size_bytes": 2789, - "subject_sha256": "sha256:3849fd4e2cae0d3a74964362e4850870bb68c23007d5eb818800159d3eb730d4" + "subject_sha256": "sha256:30eab7c62a55e988a543c1e21463676585171ba9e80afe71f518d89b04e53977" }, "production_acceptance_summary_reference": { "kind": "production-acceptance-summary", "object_media_type": "application/vnd.openadapt.production-lifecycle-evidence-summary+json;version=3", - "object_path": "production-evidence/objects/sha256/38/3849fd4e2cae0d3a74964362e4850870bb68c23007d5eb818800159d3eb730d4.production-acceptance-summary.json", + "object_path": "production-evidence/objects/sha256/30/30eab7c62a55e988a543c1e21463676585171ba9e80afe71f518d89b04e53977.production-acceptance-summary.json", "object_schema_version": "openadapt.production-lifecycle-evidence-summary/v3", - "object_sha256": "sha256:3849fd4e2cae0d3a74964362e4850870bb68c23007d5eb818800159d3eb730d4", - "registry_entry_sha256": "sha256:932511112a7eb5f296b752953ac184516a47f7630643ecf70955af722e78ef68", - "registry_head_sha256": "sha256:9a9f597b0d87674c4d36c022227201a5d8f3072237624346793692c3a863d12e", - "registry_revision": 1, - "registry_source_commit": "2c129702da718522cc257913207b6ed6f3b33b7c", + "object_sha256": "sha256:30eab7c62a55e988a543c1e21463676585171ba9e80afe71f518d89b04e53977", + "registry_entry_sha256": "sha256:8b4d7accdc374e3859c552e934f1164ccb3c05b0ddbc711f855c4ad5e3bcd3cb", + "registry_head_sha256": "sha256:f821ad5b1f342d62494f3d2204a0fc15b861b3ee4f8b7e6dbd0f3139a058df90", + "registry_revision": 7, + "registry_source_commit": "26e98be1866f342e2cb8bae53f57254b3ad715cd", "repository": "OpenAdaptAI/.github", "repository_id": "858454062", "repository_owner_id": "132681217", "schema_version": "openadapt.production-evidence-object-reference/v2", - "semantic_identity_sha256": "sha256:cf65d1d351869c63e88d7188d1e78727994cc3f6e76c88f5643a61d168a84445", - "size_bytes": 14497, + "semantic_identity_sha256": "sha256:06b6ad85b2a88ab5f829a0d1b895d50a65e07c072f3fab1c8329b3320068133a", + "size_bytes": 14525, "subject_sha256": null }, "publication_policy_sha256": "sha256:ed5ab69fe8127604ca674263cbc8c774b7cfd10754e90bfaa9b71ba573bce1d1", "publication_staging": { "assets": [ { - "asset_id": "534029483", + "asset_id": "553219190", "kind": "python-wheel", "media_type": "application/zip", - "name": "openadapt_flow-1.34.0-py3-none-any.whl", + "name": "openadapt_flow-1.35.1-py3-none-any.whl", "publish_destinations": [ "github-release", "pypi" ], - "sha256": "sha256:56d32818989cb3a92830080ead39e10b08718c55e00988117f22cbfeaac98854", - "size_bytes": 1928194, + "sha256": "sha256:cfbc89acc6a2e3bcb5502a4d1fb29e985a03149a7f14c32c259254676bb3f11d", + "size_bytes": 2129416, "uploader_id": "321543906", "uploader_login": "openadapt-release[bot]" }, { - "asset_id": "534029484", + "asset_id": "553219189", "kind": "python-sdist", "media_type": "application/gzip", - "name": "openadapt_flow-1.34.0.tar.gz", + "name": "openadapt_flow-1.35.1.tar.gz", "publish_destinations": [ "github-release", "pypi" ], - "sha256": "sha256:a941a70015fe91897e655bfc9bf83e6cf8fbcf8bd4e4c7206ffb133c505203f8", - "size_bytes": 20440838, + "sha256": "sha256:c5c6c437472b23999223b0d2473dbf21de2ca05274c270ae3854adeda6b07dca", + "size_bytes": 21135866, "uploader_id": "321543906", "uploader_login": "openadapt-release[bot]" } ], "draft": false, - "draft_release_id": "378603192", + "draft_release_id": "385710075", "immutable_releases": { "enabled": true, "enforced_by_owner": false }, "immutable_releases_sha256": "sha256:07649aafb167237fecc138f5e93b48ddce5a69f4060da7130e3c78e59fd48581", - "observed_at": "2026-09-02T18:24:25Z", + "observed_at": "2026-09-09T18:40:27Z", "prerelease": false, "publication_mode": "already-published-pypi", "pypi_files": [ { - "filename": "openadapt_flow-1.34.0-py3-none-any.whl", + "filename": "openadapt_flow-1.35.1-py3-none-any.whl", "packagetype": "bdist_wheel", - "sha256": "sha256:56d32818989cb3a92830080ead39e10b08718c55e00988117f22cbfeaac98854", - "size_bytes": 1928194, + "sha256": "sha256:cfbc89acc6a2e3bcb5502a4d1fb29e985a03149a7f14c32c259254676bb3f11d", + "size_bytes": 2129416, "yanked": false }, { - "filename": "openadapt_flow-1.34.0.tar.gz", + "filename": "openadapt_flow-1.35.1.tar.gz", "packagetype": "sdist", - "sha256": "sha256:a941a70015fe91897e655bfc9bf83e6cf8fbcf8bd4e4c7206ffb133c505203f8", - "size_bytes": 20440838, + "sha256": "sha256:c5c6c437472b23999223b0d2473dbf21de2ca05274c270ae3854adeda6b07dca", + "size_bytes": 21135866, "yanked": false } ], @@ -2478,12 +2717,12 @@ "repository": "OpenAdaptAI/openadapt-flow", "repository_id": "1291376938", "schema_version": "openadapt.production-release-staging-evidence/v1", - "tag": "v1.34.0", + "tag": "v1.35.1", "tag_ref_state": { "exists": true, - "ref": "refs/tags/v1.34.0" + "ref": "refs/tags/v1.35.1" }, - "tag_ref_state_sha256": "sha256:74c5665395ed98ba3fd1b3a284bab1e8089f7c79cfafb3f88548e8b1456c5782", + "tag_ref_state_sha256": "sha256:2efe31dddf8f57f3ba0986b364e1ee4caf962f0172faa902662b747edaa84fa0", "tag_rulesets": [ { "bypass_actors": [ @@ -2511,7 +2750,7 @@ "type": "creation" } ], - "ruleset_id": "10", + "ruleset_id": "22667117", "schema_version": "openadapt.production-release-tag-ruleset/v1", "target": "tag" }, @@ -2538,63 +2777,60 @@ "type": "non_fast_forward" }, { - "parameters": { - "update_allows_fetch_and_merge": false - }, "type": "update" } ], - "ruleset_id": "11", + "ruleset_id": "22667137", "schema_version": "openadapt.production-release-tag-ruleset/v1", "target": "tag" } ], - "tag_rulesets_sha256": "sha256:b0fdf160d80ae7dc8ffd979b2ba101cee6b3e7cf139e8c5d32b5534cbc0f7f44", - "target_commitish": "30fc60e55778a0e0f92b9776117cafcfe2512249" + "tag_rulesets_sha256": "sha256:6d002021c7ade6bd0abd4c81ad3fbd8421be7d87116cc7d5a840951a1a2fe0fb", + "target_commitish": "44e99a48ebf048c18892aa17cef6ae594db0d0c2" }, - "publication_staging_sha256": "sha256:861175c196cfe9455ce5f0536b6b1eabf55a990ced03349b865643f52316dedd", + "publication_staging_sha256": "sha256:ade70ba59e8ca9827d3f090559f02b31cdbc7f1198dbf526b9b483510ac0b6b1", "release": { "artifacts": [ { "kind": "python-sdist", "media_type": "application/gzip", - "name": "openadapt_flow-1.34.0.tar.gz", + "name": "openadapt_flow-1.35.1.tar.gz", "publish_destinations": [ "github-release", "pypi" ], - "sha256": "sha256:a941a70015fe91897e655bfc9bf83e6cf8fbcf8bd4e4c7206ffb133c505203f8", - "size_bytes": 20440838 + "sha256": "sha256:c5c6c437472b23999223b0d2473dbf21de2ca05274c270ae3854adeda6b07dca", + "size_bytes": 21135866 }, { "kind": "python-wheel", "media_type": "application/zip", - "name": "openadapt_flow-1.34.0-py3-none-any.whl", + "name": "openadapt_flow-1.35.1-py3-none-any.whl", "publish_destinations": [ "github-release", "pypi" ], - "sha256": "sha256:56d32818989cb3a92830080ead39e10b08718c55e00988117f22cbfeaac98854", - "size_bytes": 1928194 + "sha256": "sha256:cfbc89acc6a2e3bcb5502a4d1fb29e985a03149a7f14c32c259254676bb3f11d", + "size_bytes": 2129416 } ], "deployment_id": null, "deployment_sha256": null, "kind": "package", "schema_version": "openadapt.production-release-candidate/v1", - "source_commit": "30fc60e55778a0e0f92b9776117cafcfe2512249", + "source_commit": "44e99a48ebf048c18892aa17cef6ae594db0d0c2", "source_repository": "OpenAdaptAI/openadapt-flow", "source_repository_id": "1291376938", - "tag": "v1.34.0", - "version": "1.34.0" + "tag": "v1.35.1", + "version": "1.35.1" }, "release_identity": { "channel": "production", - "previous_admission_sha256": null, + "previous_admission_sha256": "sha256:d2983b86d5428c892421b191f6943a63cf4f9a0b1629e324d099f5df6fa5dcda", "schema_version": "openadapt.monotonic-production-release/v1", - "sequence": 1 + "sequence": 2 }, - "release_sha256": "sha256:f84c03c857bda8372d39bc30afcc2986247778c34b3f93a39ec9de5dd151a289", + "release_sha256": "sha256:32cae0917121cbcf657347ca023da15b5b3151db6656526df896e93e3377d49e", "revocation_state_sha256": "sha256:250ab430e467f5e584ebd2b1832976b42186b698da71d627ee1724c2b1da2457", "schema_version": "openadapt.qualification-release/v2", "signer_registry_sha256": "sha256:e243a23243b24986ed08812e284a1bd8c4993814149f02bcba79cc520e10ca14", diff --git a/docs/production-workflow-admissions.json b/docs/production-workflow-admissions.json index a199fc6..df338bd 100644 --- a/docs/production-workflow-admissions.json +++ b/docs/production-workflow-admissions.json @@ -153,6 +153,28 @@ "size_bytes": 8055, "subject_sha256": null, "verdict": "accepted" + }, + { + "bundle_version": "1.35.1-reference.1", + "evidence_class": "remote-safe-synthetic", + "expires_at": null, + "kind": "qualification-admission", + "object_media_type": "application/vnd.openadapt.qualification-admission+json;version=4", + "object_path": "production-evidence/objects/sha256/94/947224523757df41127be021fc66adee5746f07ff1a0e913319c5461e430e34d.qualification-admission.json", + "object_schema_version": "openadapt.qualification-admission/v4", + "object_sha256": "sha256:947224523757df41127be021fc66adee5746f07ff1a0e913319c5461e430e34d", + "registry_entry_sha256": "sha256:8bf275e7e38161fdbc66084cad6f2365c4283d363b9097b01c5f2587aadc8a73", + "registry_head_sha256": "sha256:2de6be9baa56e46d4c705a19a790c1637130968f90dd2ab6cbd26678d53f2891", + "registry_revision": 5, + "registry_source_commit": "f0fb9cc812c0d1653b1ec2998749b078f244308e", + "repository": "OpenAdaptAI/.github", + "repository_id": "858454062", + "repository_owner_id": "132681217", + "schema_version": "openadapt.production-evidence-object-reference/v2", + "semantic_identity_sha256": "sha256:7181ef7c2f3a74d1a8e6b8289a544efd40d4e85b8b590855d3dff2715b9ed2d1", + "size_bytes": 8058, + "subject_sha256": null, + "verdict": "accepted" } ], "policy_sha256": "sha256:e1444a08ce6b16736168cce027ce9d48abb2e0e246fc0cd79c0772fa8e423e11", @@ -161,12 +183,12 @@ "files": { "admissions": { "path": "production-workflow-admissions.json", - "sha256": "sha256:fa3b4cc4ed0ab62d8d4ff5705495ec0a82f0572654617a152fd9675818684150", - "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/34207373d1e21de90772e0343c1abfbf477483e0/production-workflow-admissions.json" + "sha256": "sha256:ca24700ebf15acb3e7cd7c19b07d157c27ba90f2dd9705aa871c4015adf11857", + "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/5b48240494a250633eec8fcf99fab26553efd0c6/production-workflow-admissions.json" } }, "repository": "OpenAdaptAI/.github", "schema_version": "openadapt.production-workflow-admissions-source/v1", - "source_commit": "34207373d1e21de90772e0343c1abfbf477483e0" + "source_commit": "5b48240494a250633eec8fcf99fab26553efd0c6" } } diff --git a/docs/reference/production-lifecycle.md b/docs/reference/production-lifecycle.md index 363b9d9..2db159b 100644 --- a/docs/reference/production-lifecycle.md +++ b/docs/reference/production-lifecycle.md @@ -43,10 +43,15 @@ Production admission and still refuse an unqualified customer workflow. The public workflow ledger is [production-workflow-admissions.json](../production-workflow-admissions.json). -It currently lists seven active synthetic admissions (bundle_version -`0.0.0-synthetic`, evidence class remote-safe-synthetic). That isn't a -customer workflow. The source pin is OpenAdaptAI/.github -`34207373d1e21de90772e0343c1abfbf477483e0`. +It retains remote-safe-synthetic workflow-admission records and each bundle's +declared version. The projection includes tutorial bundles with +`bundle_version` `0.0.0-synthetic` and the Flow 1.35.1 reference bundle +`1.35.1-reference.1`. Its source descriptor binds the exact +OpenAdaptAI/.github commit and ledger hash. + +A retained row doesn't establish current qualification. Verify the exact +workflow, admitted runtime, and current authority state before execution. +Synthetic evidence does not qualify a customer workflow. ## Release admission @@ -66,11 +71,12 @@ An admission binds: - every failure-taxonomy and reliability count; and - an immutable evidence-retention record. -The admission validator checks current PyPI metadata, immutable GitHub release -metadata, or managed-evidence object metadata. It also verifies the GitHub -artifact attestation for the acceptance summary. Healthy-path model calls, -silent incorrect success, wrong-record effects, duplicate effects, collateral -effects, and uncertain delivery must all remain zero. +The canonical admission verifier checks the signed evidence chain and the +current authority, revocation, and signer state. It hashes the supplied release +files against the admitted inventory. Publication checks depend on the recorded +release mode. Healthy-path model calls, silent incorrect success, wrong-record +effects, duplicate effects, collateral effects, and blind retry or replay after +uncertain delivery must all remain zero. ## Current-state derivation @@ -84,19 +90,53 @@ contains the exact source commit, input hashes, policy, and admission history. It doesn't store a static `production: true` flag. A consumer must use the pinned validator and derive the state at read time. -The documentation build runs that pinned validator. It verifies the signed -summary and its GitHub attestation before publishing the projection. At read -time, the browser requires the current admissions file to match the projected -digest. It then checks the signed summary, attestation bundle, retained -manifest, and current artifact-authority metadata. A failed request removes the -affected target from the active set. - -This site derives each target label from that exact current admission. It -checks the current authority metadata for every admitted artifact and retained -evidence object. The five public package versions must match their admitted -PyPI releases. A record mismatch, expiry, revocation, artifact drift, or -authority outage removes the affected target from the current Production set. -The product-wide Production label appears only while all seven targets pass. +For V2 admissions, the build retains a result from the canonical release +verifier in [production-lifecycle-verifications.json](../production-lifecycle-verifications.json). +Each result binds the exact admission object, signature bundle, release files, +and the trust state that the verifier checked. The generated result relies on +the reviewed documentation build; its own hash isn't an independent signature. +The build checks those retained bindings without changing the verification time. + +At read time, the browser requires the live admissions ledger to match the +projection. It then verifies the latest admission's object and bundle hashes +against its generated result. Each signature statement in the evidence chain +must still be within its time window, and each used signer must remain active. +The browser reads the current canonical registry and +requires the same authority, revocation, and signer objects, with valid time +windows. An unrelated registry addition can leave those bindings intact. A new +trust object requires a new verification result. + +Package checks read current PyPI and GitHub metadata. The default PyPI version +must match the admitted version, and each admitted file must keep its hash and +size and remain available without a yank. GitHub checks bind the release and +asset IDs, uploader identity, and the tag's resolved source commit. Public tag +ruleset fields must still match their recorded values. + +The `already-published-pypi` mode supports releases that predate GitHub release +immutability. It doesn't require a release-app uploader or `immutable: true`. +The `draft-before-tag` mode keeps those requirements. GitHub's repository +immutability setting and ruleset bypass actors require authenticated access; +the public page retains their issuance-time observations and makes no claim +that it can read them in the browser. + +A failed request or changed binding removes the target's active label. A +missing verification result never restores an older release. Cloud and Docs +retain their deployment admissions; a retained deployment URL alone cannot +prove current deployment state. Their active labels require a current public +deployment observation. The product-wide Production label requires all seven +targets to pass. + +To refresh a V2 result after updating the exact source descriptors, use the +release files that the canonical verifier must check: + +```bash +python scripts/generate_production_lifecycle_verification.py \ + --target flow --artifact-root /path/to/verified/release-files +python scripts/generate_production_lifecycle_verification.py --check +``` + +Generation fails without a successful canonical verification. Commit the +result with its source pin and review it before publishing the site. An installation, release, or successful demo cannot create Production state without this complete evidence contract. diff --git a/production-lifecycle-source.json b/production-lifecycle-source.json index 2dff74f..e85d824 100644 --- a/production-lifecycle-source.json +++ b/production-lifecycle-source.json @@ -1,62 +1,62 @@ { "schema_version": "openadapt.production-lifecycle-source/v1", "repository": "OpenAdaptAI/.github", - "source_commit": "9b61391713203998e584f4834e4812bb41b40f6e", + "source_commit": "5b48240494a250633eec8fcf99fab26553efd0c6", "files": { "admissions": { "path": "production-lifecycle-admissions.json", - "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/9b61391713203998e584f4834e4812bb41b40f6e/production-lifecycle-admissions.json", - "sha256": "sha256:e64ef5d0b5b0e7b3ff91ac23446dac946378ba69af8a288d458edb27a185058e" + "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/5b48240494a250633eec8fcf99fab26553efd0c6/production-lifecycle-admissions.json", + "sha256": "sha256:8859deeb4ff2dc7704dc56eb0e356f10afa127d7e9e768a8d803cc97f6e61cf5" }, "admissions_schema": { "path": "schemas/production-lifecycle-admissions.schema.json", - "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/9b61391713203998e584f4834e4812bb41b40f6e/schemas/production-lifecycle-admissions.schema.json", + "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/5b48240494a250633eec8fcf99fab26553efd0c6/schemas/production-lifecycle-admissions.schema.json", "sha256": "sha256:33ad1421fafeef7780183bfd20100bada03111bc9b3eb06319097b6f0e61365f" }, "evidence_manifest_schema": { "path": "schemas/production-lifecycle-evidence-manifest.schema.json", - "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/9b61391713203998e584f4834e4812bb41b40f6e/schemas/production-lifecycle-evidence-manifest.schema.json", + "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/5b48240494a250633eec8fcf99fab26553efd0c6/schemas/production-lifecycle-evidence-manifest.schema.json", "sha256": "sha256:d720f9676dde3ea6deb9d3a2b91bcd8eae8fcecc2cafaef17a0d055391cee860" }, "evidence_registry": { "path": "evidence-registry.json", - "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/9b61391713203998e584f4834e4812bb41b40f6e/evidence-registry.json", - "sha256": "sha256:3d2e069c1b7ec3fce828d923bc79f94aeb6a25c01f3b201dbed65b099713c585" + "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/5b48240494a250633eec8fcf99fab26553efd0c6/evidence-registry.json", + "sha256": "sha256:549c8559c7225ce55e8d35f16ecd9845f83e8d52483c98cf846f0417f4c7e4e7" }, "evidence_registry_schema": { "path": "schemas/evidence-registry.schema.json", - "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/9b61391713203998e584f4834e4812bb41b40f6e/schemas/evidence-registry.schema.json", + "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/5b48240494a250633eec8fcf99fab26553efd0c6/schemas/evidence-registry.schema.json", "sha256": "sha256:3159f1e13360acedf51de79a3d8f910db65530dcf545659a861ea4ccb6e12869" }, "evidence_registry_validator": { "path": "scripts/validate_evidence_registry.py", - "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/9b61391713203998e584f4834e4812bb41b40f6e/scripts/validate_evidence_registry.py", + "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/5b48240494a250633eec8fcf99fab26553efd0c6/scripts/validate_evidence_registry.py", "sha256": "sha256:aea4d61cb5becb46f2853c7f765aba7a27b8f0737ae9a5f8d240def780e6c628" }, "evidence_summary_schema": { "path": "schemas/production-lifecycle-evidence-summary.schema.json", - "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/9b61391713203998e584f4834e4812bb41b40f6e/schemas/production-lifecycle-evidence-summary.schema.json", + "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/5b48240494a250633eec8fcf99fab26553efd0c6/schemas/production-lifecycle-evidence-summary.schema.json", "sha256": "sha256:5d5f28261568222f45e9a66d9c9daf468168147f9d0d82d223eba1b8e777e2c9" }, "lifecycle": { "path": "repository-lifecycle.yml", - "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/9b61391713203998e584f4834e4812bb41b40f6e/repository-lifecycle.yml", + "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/5b48240494a250633eec8fcf99fab26553efd0c6/repository-lifecycle.yml", "sha256": "sha256:823933a80ac4459f7359719f69dfeb7f164e940918ad4811d789eb2040bfc50b" }, "policy": { "path": "production-lifecycle-policy.json", - "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/9b61391713203998e584f4834e4812bb41b40f6e/production-lifecycle-policy.json", + "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/5b48240494a250633eec8fcf99fab26553efd0c6/production-lifecycle-policy.json", "sha256": "sha256:1887a1222f5c34a6bd3e2a03f8aa2b55368c66bb9e6cad109a0c5c1ed0b5c524" }, "policy_schema": { "path": "schemas/production-lifecycle-policy.schema.json", - "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/9b61391713203998e584f4834e4812bb41b40f6e/schemas/production-lifecycle-policy.schema.json", + "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/5b48240494a250633eec8fcf99fab26553efd0c6/schemas/production-lifecycle-policy.schema.json", "sha256": "sha256:c5d3157a7be841351b39f3ec8a8f6433de6d16b85ae0b1c61800d2c052aaa734" }, "validator": { "path": "scripts/validate_production_lifecycle.py", - "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/9b61391713203998e584f4834e4812bb41b40f6e/scripts/validate_production_lifecycle.py", - "sha256": "sha256:77e583d8baed30938268b8c3c4f34e4634acf7ba5b82dc9d58b1f0f4527ea3cb" + "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/5b48240494a250633eec8fcf99fab26553efd0c6/scripts/validate_production_lifecycle.py", + "sha256": "sha256:04c3945106fb1004535be531931eab58b5e7c209668f3602628c909ece669333" } } } diff --git a/production-workflow-admissions-source.json b/production-workflow-admissions-source.json index 50f0ba5..ec2f22e 100644 --- a/production-workflow-admissions-source.json +++ b/production-workflow-admissions-source.json @@ -1,12 +1,12 @@ { "schema_version": "openadapt.production-workflow-admissions-source/v1", "repository": "OpenAdaptAI/.github", - "source_commit": "34207373d1e21de90772e0343c1abfbf477483e0", + "source_commit": "5b48240494a250633eec8fcf99fab26553efd0c6", "files": { "admissions": { "path": "production-workflow-admissions.json", - "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/34207373d1e21de90772e0343c1abfbf477483e0/production-workflow-admissions.json", - "sha256": "sha256:fa3b4cc4ed0ab62d8d4ff5705495ec0a82f0572654617a152fd9675818684150" + "url": "https://raw.githubusercontent.com/OpenAdaptAI/.github/5b48240494a250633eec8fcf99fab26553efd0c6/production-workflow-admissions.json", + "sha256": "sha256:ca24700ebf15acb3e7cd7c19b07d157c27ba90f2dd9705aa871c4015adf11857" } } } diff --git a/scripts/generate_production_lifecycle_verification.py b/scripts/generate_production_lifecycle_verification.py new file mode 100644 index 0000000..8a685eb --- /dev/null +++ b/scripts/generate_production_lifecycle_verification.py @@ -0,0 +1,498 @@ +#!/usr/bin/env python3 +"""Retain the public canonical verifier's result for one exact latest admission. + +Generation runs the pinned release verifier against the complete local artifact +inventory. The deterministic check validates retained references and the receipt; +it does not repeat release downloads or claim that a historical result is active. +The browser must also check the current canonical state and public artifacts. +""" + +from __future__ import annotations + +import argparse +import contextlib +import importlib.util +import json +import os +import subprocess +import sys +import tempfile +from datetime import datetime +from pathlib import Path +from typing import Any, Iterator + +_projection_spec = importlib.util.spec_from_file_location( + "_ops_production_lifecycle_renderer", + Path(__file__).with_name("render_production_lifecycle.py"), +) +assert _projection_spec is not None and _projection_spec.loader is not None +projection = importlib.util.module_from_spec(_projection_spec) +_projection_spec.loader.exec_module(projection) + +ROOT = Path(__file__).resolve().parents[1] +OUTPUT_PATH = ROOT / "docs" / "production-lifecycle-verifications.json" +DOCUMENT_SCHEMA = "openadapt.public-production-lifecycle-verifications/v1" +RECORD_SCHEMA = "openadapt.public-production-lifecycle-verification/v1" +RECORD_FIELDS = { + "schema_version", "target", "admission_reference", + "admission_bundle_reference", "verification_receipt", "current_state", +} +CURRENT_STATE_FIELDS = { + "authority_reference", "authority_bundle_reference", "revocation_reference", + "revocation_bundle_reference", "signer_registry_pointer", +} +REFERENCE_PREFIX_FIELDS = { + "schema_version", "repository", "repository_id", "repository_owner_id", + "registry_source_commit", "registry_revision", "registry_head_sha256", +} + + +class VerificationError(ValueError): + """The retained result is not bound to the exact public evidence.""" + + +def closed(value: Any, fields: set[str], label: str) -> dict[str, Any]: + if not isinstance(value, dict) or set(value) != fields: + raise VerificationError(f"{label} fields differ") + return value + + +def exact_commit(value: Any) -> str: + if not isinstance(value, str) or projection.HEX40.fullmatch(value) is None: + raise VerificationError("canonical source commit is not exact") + return value + + +@contextlib.contextmanager +def canonical_verifier(tree: Path) -> Iterator[Any]: + """Load only the verifier and public dependencies from the pinned tree.""" + script_dir = tree / "scripts" + names = {path.stem for path in script_dir.glob("*.py")} + previous = {name: sys.modules.pop(name) for name in names if name in sys.modules} + sys.path.insert(0, str(script_dir)) + try: + spec = importlib.util.spec_from_file_location( + "_openadapt_pinned_release_verifier", + script_dir / "verify_production_release_admission.py", + ) + if spec is None or spec.loader is None: + raise VerificationError("canonical release verifier is missing") + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + yield module + finally: + sys.path.remove(str(script_dir)) + for name in names: + sys.modules.pop(name, None) + sys.modules.update(previous) + + +class CanonicalStore: + """Read exact Git objects without accepting caller-supplied transport URLs.""" + + def __init__(self, directory: Path, verifier: Any, trees: dict[str, Path]): + self.directory = directory + self.verifier = verifier + self.trees = trees + self.registries: dict[str, tuple[dict, list]] = {} + + def tree(self, commit: str) -> Path: + exact_commit(commit) + if commit not in self.trees: + path = self.directory / commit + projection.materialize_commit(commit, path) + self.trees[commit] = path + return self.trees[commit] + + def registry(self, commit: str) -> tuple[dict, list]: + if commit not in self.registries: + value = json.loads((self.tree(commit) / "evidence-registry.json").read_bytes()) + entries = self.verifier.evidence.validate_registry(value) + self.registries[commit] = value, entries + return self.registries[commit] + + def reference(self, commit: str, entry: dict) -> dict: + registry, _ = self.registry(commit) + evidence = self.verifier.evidence + return { + "schema_version": evidence.REFERENCE_SCHEMA, + "repository": evidence.REPOSITORY, + "repository_id": evidence.REPOSITORY_ID, + "repository_owner_id": evidence.REPOSITORY_OWNER_ID, + "registry_source_commit": commit, + "registry_revision": registry["revision"], + "registry_head_sha256": registry["registry_head_sha256"], + **entry, + } + + def read(self, reference: dict) -> dict: + reference = self.verifier.evidence.validate_reference(reference) + commit = reference["registry_source_commit"] + registry, entries = self.registry(commit) + if ( + reference["registry_revision"] != registry["revision"] + or reference["registry_head_sha256"] != registry["registry_head_sha256"] + ): + raise VerificationError("reference registry identity differs") + self.verifier.evidence.require_registered( + entries, reference=reference, label="verification record object" + ) + return self.read_object(reference) + + def read_object(self, reference: dict, *, source_commit: str | None = None) -> dict: + reference = self.verifier.evidence.validate_reference(reference) + commit = source_commit or reference["registry_source_commit"] + raw = (self.tree(commit) / reference["object_path"]).read_bytes() + return self.verifier.verify_bytes(raw, reference, "verification record object") + + def bundle(self, reference: dict) -> dict: + self.read(reference) + commit = reference["registry_source_commit"] + _, entries = self.registry(commit) + index = next(i for i, entry in enumerate(entries) + if entry["registry_entry_sha256"] == reference["registry_entry_sha256"]) + if index + 1 >= len(entries): + raise VerificationError("object has no adjacent signature bundle") + bundle = self.reference(commit, entries[index + 1]) + self.verifier.trust.validate_reference_pair(reference, bundle, kind=reference["kind"]) + self.read(bundle) + return bundle + + def pair(self, reference: dict, bundle: dict) -> dict: + if self.bundle(reference) != bundle: + raise VerificationError("signature bundle is not the exact adjacent entry") + return self.read(reference) + + def fetch(self, url: str, **_kwargs: Any) -> bytes: + prefix = "https://raw.githubusercontent.com/OpenAdaptAI/.github/" + if not isinstance(url, str) or not url.startswith(prefix): + raise VerificationError("offline verification refuses a non-canonical URL") + commit, separator, path = url[len(prefix):].partition("/") + exact_commit(commit) + if ( + not separator or not path or "?" in path or "#" in path or "%" in path + or any(part in {"", ".", ".."} for part in path.split("/")) + or "\\" in path + ): + raise VerificationError("offline verification refuses a non-canonical path") + return (self.tree(commit) / path).read_bytes() + + def verify_pair(self, reference: dict, bundle: dict, *, now: datetime) -> None: + """Run the pinned DSSE route at retained time from immutable Git bytes. + + Authority v2 binds the raw signer registry digest. The canonical route + handles that distinction and proves its unique historical reverse + revocation link before it verifies the authority's outer signature. + """ + self.pair(reference, bundle) + value = self.read(bundle) + envelope = value.get("dsseEnvelope") + if not isinstance(envelope, dict) or envelope.get("payloadType") != ( + self.verifier.public_trust.STATEMENT_MEDIA_TYPE + ): + raise VerificationError("retained verification requires a public DSSE profile") + + class RetainedTime(datetime): + @classmethod + def now(cls, tz=None): + return now.astimezone(tz) if tz is not None else now.replace(tzinfo=None) + + previous_fetch, previous_datetime = self.verifier.fetch, self.verifier.datetime + try: + self.verifier.fetch = self.fetch + self.verifier.datetime = RetainedTime + pair = self.verifier.fetch_pair(reference, bundle) + self.verifier.verify_registered_signature( + reference, bundle, pair, + policy=json.loads(self.verifier.POLICY_PATH.read_bytes()), + ) + finally: + self.verifier.fetch, self.verifier.datetime = previous_fetch, previous_datetime + + def current_state(self, commit: str) -> dict: + registry, entries = self.registry(commit) + result = {} + for label, kind in ( + ("authority", "qualification-authority-state-receipt"), + ("revocation", "qualification-revocation-state-receipt"), + ): + matches = [entry for entry in entries if entry["kind"] == kind] + if not matches: + raise VerificationError(f"verification state has no {kind}") + reference = self.reference(commit, matches[-1]) + result[f"{label}_reference"] = reference + result[f"{label}_bundle_reference"] = self.bundle(reference) + self.signer(commit) + result["signer_registry_pointer"] = registry["signer_registry"] + return result + + def signer(self, commit: str) -> dict: + registry, _ = self.registry(commit) + pointer = registry["signer_registry"] + if pointer is None: + raise VerificationError("verification state has no signer registry") + raw = (self.tree(commit) / pointer["object_path"]).read_bytes() + signer = self.verifier.evidence.validate_signer_registry(json.loads(raw)) + if ( + projection._digest_bytes(raw) != pointer["object_sha256"] + or raw != self.verifier.evidence.canonical(signer) + b"\n" + or signer["revision"] != pointer["registry_revision"] + or self.verifier.evidence.signer_registry_identity_digest(signer) + != pointer["registry_identity_sha256"] + ): + raise VerificationError("verification signer registry bytes or identity differ") + return signer + + +def latest_admissions(ledger: dict, store: CanonicalStore, + *, source_commit: str | None = None) -> dict[str, tuple[dict, dict]]: + if ledger.get("schema_version") != projection.ADMISSIONS_SCHEMA: + raise VerificationError("admissions ledger schema is not supported") + result: dict[str, tuple[dict, dict]] = {} + sequences: dict[str, set[int]] = {} + unsupported_targets = set() + for reference in ledger["admissions"]: + # Legacy inline rows have no V2 verification record. + if reference.get("kind") != "qualification-release": + if isinstance(reference.get("target"), str): + unsupported_targets.add(reference["target"]) + continue + # Selection must retain a latest refusal without making older, unrelated + # reference metadata a prerequisite for another target's verification. + # A selected record must still pass read()/pair() in validate_record. + admission = store.read_object(reference, source_commit=source_commit) + target = admission["target"] + sequence = admission["release_identity"]["sequence"] + if type(sequence) is not int or sequence < 1: + raise VerificationError("admission sequence is invalid") + seen = sequences.setdefault(target, set()) + if sequence in seen: + raise VerificationError("admission sequence is duplicated") + seen.add(sequence) + if target not in result or sequence > result[target][1]["release_identity"]["sequence"]: + result[target] = reference, admission + return {target: value for target, value in result.items() if target not in unsupported_targets} + + +def validate_record(record: dict, latest: dict, store: CanonicalStore) -> None: + closed(record, RECORD_FIELDS, "verification record") + if record["schema_version"] != RECORD_SCHEMA: + raise VerificationError("verification record schema is not supported") + target = record["target"] + if target not in latest: + raise VerificationError("verification target has no latest admission") + reference, admission = latest[target] + if record["admission_reference"] != reference: + raise VerificationError("verification record does not bind the latest admission") + store.pair(reference, record["admission_bundle_reference"]) + if admission.get("evidence_class") != "remote-safe-synthetic": + raise VerificationError("verification evidence is not remote-safe-synthetic") + receipt = record["verification_receipt"] + if not isinstance(receipt, dict): + raise VerificationError("verification receipt must be an object") + instant = store.verifier.trust.require_timestamp(receipt["verified_at"], "verified_at") + summary = store.pair( + admission["production_acceptance_summary_reference"], + admission["production_acceptance_summary_bundle_reference"], + ) + evidence_chain = { + kind: store.pair(summary[f"{kind}_reference"], summary[f"{kind}_bundle_reference"]) + for kind in ("production_acceptance_manifest", "qualification_evidence_decision_receipt") + } + qualification = store.pair( + summary["qualification_admission_reference"], + summary["qualification_admission_bundle_reference"], + ) + expected_receipt = store.verifier.verification_receipt( + admission=admission, admission_reference=reference, + admission_bundle_reference=record["admission_bundle_reference"], + summary=summary, qualification_admission=qualification, + verified_at=instant, + trust_state_source_commit=exact_commit(receipt["trust_state_source_commit"]), + ) + if receipt != expected_receipt: + raise VerificationError("closed canonical verification receipt differs") + closed(record["current_state"], CURRENT_STATE_FIELDS, "verification current state") + if record["current_state"] != store.current_state(receipt["trust_state_source_commit"]): + raise VerificationError("verification state is not the exact current registered state") + signer = store.signer(receipt["trust_state_source_commit"]) + store.verifier.trust.validate_release_evidence_chain( + admission, summary=summary, + manifest=evidence_chain["production_acceptance_manifest"], + receipt=evidence_chain["qualification_evidence_decision_receipt"], + qualification_admission=qualification, receipt_signer_registry=signer, + now=instant, + ) + store.verifier.trust.validate_admission_current_state( + admission, admission_reference=reference, + authority_state=store.read(record["current_state"]["authority_reference"]), + revocation_state=store.read(record["current_state"]["revocation_reference"]), + signer_registry=signer, now=instant, + ) + pairs = [ + (reference, record["admission_bundle_reference"]), + (admission["production_acceptance_summary_reference"], + admission["production_acceptance_summary_bundle_reference"]), + *((summary[f"{kind}_reference"], summary[f"{kind}_bundle_reference"]) + for kind in ("production_acceptance_manifest", + "qualification_evidence_decision_receipt", "qualification_admission")), + *((record["current_state"][f"{kind}_reference"], + record["current_state"][f"{kind}_bundle_reference"]) + for kind in ("authority", "revocation")), + ] + for regular, bundle in pairs: + store.verify_pair(regular, bundle, now=instant) + + +def validate_document(document: dict, source: dict, latest: dict, store: CanonicalStore) -> None: + closed(document, {"schema_version", "source_commit", "records"}, "verification document") + if document["schema_version"] != DOCUMENT_SCHEMA: + raise VerificationError("verification document schema is not supported") + if document["source_commit"] != source["source_commit"]: + raise VerificationError("verification source differs from the projection source") + records = document["records"] + if not isinstance(records, list): + raise VerificationError("verification records must be an array") + seen = set() + for record in records: + validate_record(record, latest, store) + if record["target"] in seen: + raise VerificationError("verification target is duplicated") + seen.add(record["target"]) + if [record["target"] for record in records] != sorted(seen): + raise VerificationError("verification records are not sorted by target") + + +def run_verifier(tree: Path, reference: dict, bundle: dict, admission: dict, + artifact_root: Path, *, python: str = sys.executable) -> dict: + release = admission["release"] + inventory = { + "schema_version": "openadapt.production-release-artifact-inventory/v1", + "target": admission["target"], "claim_scope": admission["claim_scope"], + "artifacts": release["artifacts"], + } + with tempfile.TemporaryDirectory(prefix="openadapt-verification-caller-") as directory: + path = Path(directory) + for name, value in (("admission", reference), ("bundle", bundle), ("inventory", inventory)): + (path / f"{name}.json").write_text(json.dumps(value), encoding="utf-8") + command = [ + python, str(tree / "scripts" / "verify_production_release_admission.py"), + "--admission-reference", str(path / "admission.json"), + "--admission-bundle-reference", str(path / "bundle.json"), + "--artifact-inventory", str(path / "inventory.json"), + "--artifact-root", str(artifact_root.resolve()), + "--expected-target", admission["target"], + "--expected-repository", release["source_repository"], + "--expected-repository-id", release["source_repository_id"], + "--expected-source-commit", release["source_commit"], + ] + for name in ("version", "tag", "deployment_id", "deployment_sha256"): + if release[name] is not None: + command.extend([f"--expected-{name.replace('_', '-')}", release[name]]) + result = subprocess.run(command, capture_output=True, text=True, check=False, timeout=600) + if result.returncode: + detail = (result.stderr or result.stdout or "verification failed").strip().splitlines()[-1] + raise VerificationError(f"canonical release verifier refused: {detail}") + receipt = json.loads(result.stdout) + if not isinstance(receipt, dict) or receipt.get("verdict") != "verified": + raise VerificationError("canonical release verifier returned no verified receipt") + return receipt + + +def generate_record(target: str, latest: dict, store: CanonicalStore, + verifier_tree: Path, artifact_root: Path) -> dict: + if target not in latest: + raise VerificationError("requested target has no latest V2 admission") + reference, admission = latest[target] + bundle = store.bundle(reference) + receipt = run_verifier(verifier_tree, reference, bundle, admission, artifact_root) + record = { + "schema_version": RECORD_SCHEMA, "target": target, + "admission_reference": reference, "admission_bundle_reference": bundle, + "verification_receipt": receipt, + "current_state": store.current_state(exact_commit(receipt["trust_state_source_commit"])), + } + validate_record(record, latest, store) + return record + + +def run_with_dependencies(tree: Path, directory: Path, argv: list[str]) -> int | None: + """Keep the pinned dependency environment alive until the child exits.""" + python = projection._validator_python(tree, directory / "runtime") + if python == sys.executable: + return None + result = subprocess.run( + [python, str(Path(__file__).resolve()), *argv], + check=False, + timeout=900, + ) + return result.returncode + + +def main(argv: list[str] | None = None) -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--source", type=Path, default=projection.SOURCE_PATH) + parser.add_argument("--output", type=Path, default=OUTPUT_PATH) + parser.add_argument("--target", choices=sorted(projection.PUBLIC_TARGET_CONTRACT)) + parser.add_argument("--artifact-root", type=Path) + parser.add_argument("--check", action="store_true") + args = parser.parse_args(argv) + if args.check and (args.target or args.artifact_root): + parser.error("--check does not accept generation inputs") + if not args.check and (not args.target or not args.artifact_root): + parser.error("generation requires --target and --artifact-root") + try: + source = projection.load_source(args.source) + with tempfile.TemporaryDirectory(prefix="openadapt-verification-") as temporary: + directory = Path(temporary) + tree = directory / source["source_commit"] + projection.materialize_commit(source["source_commit"], tree) + inputs = projection.read_pinned_files(source, tree) + child_status = run_with_dependencies( + tree, directory, sys.argv[1:] if argv is None else argv + ) + if child_status is not None: + return child_status + with canonical_verifier(tree) as verifier: + store = CanonicalStore(directory, verifier, {source["source_commit"]: tree}) + latest = latest_admissions(json.loads(inputs["admissions"]), store, + source_commit=source["source_commit"]) + if args.output.exists(): + document = json.loads(args.output.read_bytes()) + else: + if args.check: + raise VerificationError("verification document is missing") + document = {"schema_version": DOCUMENT_SCHEMA, + "source_commit": source["source_commit"], "records": []} + if not args.check: + closed(document, {"schema_version", "source_commit", "records"}, "verification document") + if document["schema_version"] != DOCUMENT_SCHEMA or not isinstance(document["records"], list): + raise VerificationError("existing verification document is invalid") + exact_commit(document["source_commit"]) + old_targets = [closed(r, RECORD_FIELDS, "verification record")["target"] + for r in document["records"]] + if len(set(old_targets)) != len(old_targets): + raise VerificationError("verification target is duplicated") + document["records"] = [r for r in document["records"] if r.get("target") != args.target] + document["source_commit"] = source["source_commit"] + validate_document(document, source, latest, store) + record = generate_record(args.target, latest, store, tree, args.artifact_root) + document["records"] = sorted([*document["records"], record], key=lambda r: r["target"]) + validate_document(document, source, latest, store) + if not args.check: + args.output.parent.mkdir(parents=True, exist_ok=True) + with tempfile.NamedTemporaryFile(mode="w", encoding="utf-8", dir=args.output.parent, + prefix=args.output.name + ".", delete=False) as handle: + json.dump(document, handle, indent=2, sort_keys=True) + handle.write("\n") + pending = Path(handle.name) + os.replace(pending, args.output) + print("Verification records match the pinned evidence." if args.check else "Verification record written.") + except (OSError, ValueError, KeyError, TypeError, ImportError, subprocess.SubprocessError) as exc: + print(f"REFUSED: {exc}", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/render_production_workflow_admissions.py b/scripts/render_production_workflow_admissions.py index 30e7f20..923ea5e 100644 --- a/scripts/render_production_workflow_admissions.py +++ b/scripts/render_production_workflow_admissions.py @@ -32,7 +32,7 @@ EXPECTED_FILE_KEYS = {"admissions"} EXPECTED_PATHS = {"admissions": "production-workflow-admissions.json"} REQUIRED_EVIDENCE_CLASS = "remote-safe-synthetic" -REQUIRED_BUNDLE_VERSION = "0.0.0-synthetic" +BUNDLE_VERSION = re.compile(r"^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z]+(?:[.-][0-9A-Za-z]+)*)?$") PUBLIC_OBJECT_FIELDS = ("bundle_version", "evidence_class", "expires_at", "verdict") @@ -241,9 +241,10 @@ def render( f"workflow admission {index} evidence class is not " f"{REQUIRED_EVIDENCE_CLASS}" ) - if admission.get("bundle_version") != REQUIRED_BUNDLE_VERSION: + bundle_version = admission.get("bundle_version") + if not isinstance(bundle_version, str) or BUNDLE_VERSION.fullmatch(bundle_version) is None: raise RenderError( - f"workflow admission {index} is not the synthetic tutorial bundle" + f"workflow admission {index} bundle version is invalid" ) serialized = json.dumps(admission, ensure_ascii=False) if "mockmed" in serialized.lower() or "production_acceptance" in admission: diff --git a/tests/js/production_lifecycle.test.cjs b/tests/js/production_lifecycle.test.cjs index fe32f02..355928f 100644 --- a/tests/js/production_lifecycle.test.cjs +++ b/tests/js/production_lifecycle.test.cjs @@ -661,16 +661,16 @@ test("the committed ledger is not yet active before issued_at", () => { } }); -test("the committed ledger admits all seven until-revoked targets", () => { +test("the committed ledger retains seven until-revoked candidates for current verification", () => { const committed = require("../../docs/production-lifecycle.json"); const targets = lifecycle.validateProjection(committed); - const afterIssue = Date.parse("2026-09-02T20:00:00Z"); + const afterIssue = Date.parse("2026-09-10T00:00:00Z"); const afterThirtyDays = Date.parse("2026-12-01T00:00:00Z"); const expectedVersion = { agent: "2.0.1", capture: "1.2.2", desktop: "0.16.0", - flow: "1.34.0", + flow: "1.35.1", openadapt: "1.16.0", }; @@ -683,7 +683,7 @@ test("the committed ledger admits all seven until-revoked targets", () => { assert.ok(derived, `${id} should derive an until-revoked admission`); assert.ok( lifecycle.deriveTarget(target, committed, afterThirtyDays), - `${id} must stay active past the retained 30-day v1 window`, + `${id} remains a candidate past the retained 30-day v1 window`, ); if (expectedVersion[id]) { assert.equal(derived.releaseVersion, expectedVersion[id]); @@ -698,7 +698,7 @@ test("a timestamped v2 expiry is not until-revoked", () => { const target = structuredClone( committed.targets.find((candidate) => candidate.id === "flow"), ); - const now = Date.parse("2026-09-03T12:00:00Z"); + const now = Date.parse("2026-09-10T00:00:00Z"); assert.ok(lifecycle.deriveTarget(target, committed, now)); target.latest_admission.expires_at = "2026-09-09T18:24:25Z"; @@ -792,15 +792,13 @@ function fetchUntilRevoked(fixture, options = {}) { const AFTER_ISSUE = Date.parse("2026-09-02T20:00:00Z"); -test("until-revoked object-refs keep retained policy hashes active", async () => { +test("until-revoked rows without verification cannot bypass current authority checks", async () => { const fixture = makeUntilRevokedFixture(); const state = await lifecycle.load(fetchUntilRevoked(fixture), AFTER_ISSUE); assert.ok(state?.activeTargets instanceof Map); - assert.deepEqual([...state.activeTargets.keys()].sort(), lifecycle.TARGET_IDS); - assert.equal(state.activeTargets.get("flow").untilRevoked, true); - assert.equal(state.activeTargets.get("flow").releaseLabel, "release 1.2.3"); - assert.equal(state.defaultInstallVerified, true); + assert.equal(state.activeTargets.size, 0); + assert.equal(state.defaultInstallVerified, false); }); test("until-revoked policy mismatch fails closed without a parseable policy", async () => { @@ -843,7 +841,7 @@ test("until-revoked mismatch fails closed on expiry, revocation, or identity dri }); const driftedState = await lifecycle.load(fetchUntilRevoked(drifted), AFTER_ISSUE); assert.equal(driftedState.activeTargets.has("flow"), false); - assert.equal(driftedState.activeTargets.size, 6); + assert.equal(driftedState.activeTargets.size, 0); assert.equal(driftedState.defaultInstallVerified, false); }); @@ -867,3 +865,266 @@ test("object-ref rows are not product targets and unknown rows fail closed", asy true, ); }); + +function canonicalJson(value) { + if (Array.isArray(value)) return `[${value.map(canonicalJson).join(",")}]`; + if (value && typeof value === "object") return `{${Object.keys(value).sort().map( + (key) => `${JSON.stringify(key)}:${canonicalJson(value[key])}`).join(",")}}`; + return JSON.stringify(value); +} + +function makeVerifiedV2Fixture({ stateExpiry = null, signerExpiry = null, draftMode = false, stateNotBefore = "2026-09-02T18:24:25Z", keyRevokedAt = null, mismatchedState = false, bundleExpiryKind = null, bundleStartKind = null, evidenceKeyRevokedAt = null } = {}) { + const fixture = makeUntilRevokedFixture(); + const responses = new Map(); + const commit = fixture.projection.source.source_commit; + const base = `https://raw.githubusercontent.com/OpenAdaptAI/.github/${commit}`; + const raw = (url, value) => responses.set(url, Buffer.from(`${canonicalJson(value)}\n`)); + const object = (kind, value, subject = null) => { + const bytes = Buffer.from(`${canonicalJson(value)}\n`); + const sha = digest(bytes); + const ref = { + schema_version: "openadapt.production-evidence-object-reference/v2", + repository: "OpenAdaptAI/.github", repository_id: "1", repository_owner_id: "2", + registry_source_commit: commit, registry_revision: 1, registry_head_sha256: `sha256:${"a".repeat(64)}`, + kind, object_schema_version: value.schema_version ?? "fixture/v1", + object_path: `production-evidence/objects/sha256/${sha.slice(7, 9)}/${sha.slice(7)}.${kind}.json`, + object_sha256: sha, size_bytes: bytes.length, object_media_type: "application/json", + semantic_identity_sha256: value.authority_state_sha256 && kind === "qualification-authority-state-receipt" ? value.authority_state_sha256 : + (kind === "qualification-revocation-state-receipt" ? value.revocation_state_sha256 : sha), subject_sha256: subject, registry_entry_sha256: sha, + }; + responses.set(`${base}/${ref.object_path}`, bytes); + return ref; + }; + const pair = (kind, value) => { + const ref = object(kind, value); + const key = evidenceKeyRevokedAt && !["qualification-authority-state-receipt", "qualification-revocation-state-receipt"].includes(kind) + ? "evidence-key" : "fixture-key"; + const statement = { schema_version: "openadapt.production-public-trust-signing-statement/v1", key_id: key, + object_kind: kind, object_sha256: ref.object_sha256, object_size_bytes: ref.size_bytes, + semantic_identity_sha256: ref.semantic_identity_sha256, + not_before: kind === bundleStartKind ? "2026-09-02T19:30:00Z" : "2026-09-02T18:24:25Z", + expires_at: kind === bundleExpiryKind ? "2026-09-02T19:30:00Z" : null }; + return [ref, object(`${kind}-sigstore-bundle`, { dsseEnvelope: { payload: Buffer.from(canonicalJson(statement)).toString("base64"), + signatures: [{ keyid: key, sig: "synthetic" }] } }, ref.object_sha256)]; + }; + const signer = { generated_at: "2026-09-02T18:24:25Z", expires_at: signerExpiry, revision: 1, + signers: [{ key_id: "fixture-key", status: "active", revoked_at: keyRevokedAt }] }; + if (evidenceKeyRevokedAt) signer.signers.push({ key_id: "evidence-key", status: "active", revoked_at: evidenceKeyRevokedAt }); + const signerBytes = Buffer.from(`${canonicalJson(signer)}\n`); + const signerHash = digest(signerBytes); + const signerIdentity = digest(`OpenAdapt qualification signer registry v2\0${canonicalJson(signer)}`); + const pointer = { object_sha256: signerHash, registry_identity_sha256: signerIdentity, registry_revision: 1, + object_path: `production-evidence/signer-registries/sha256/${signerHash.slice(7, 9)}/${signerHash.slice(7)}.qualification-signer-registry.json` }; + responses.set(`${base}/${pointer.object_path}`, signerBytes); + const authority = { status: "active", not_before: stateNotBefore, expires_at: stateExpiry, issuer_key_id: "fixture-key" }; + const revocation = { status: "current", not_before: stateNotBefore, expires_at: stateExpiry, issuer_key_id: "fixture-key", revocations: [] }; + const authorityIdentity = `sha256:${"2".repeat(64)}`; + const revocationIdentity = `sha256:${"3".repeat(64)}`; + Object.assign(authority, { authority_state_sha256: authorityIdentity, signer_registry_sha256: signerHash, + signer_registry_identity_sha256: signerIdentity, signer_registry_revision: 1 }); + Object.assign(revocation, { revocation_state_sha256: revocationIdentity, authority_state_sha256: authorityIdentity, + signer_registry_sha256: signerIdentity }); + if (mismatchedState) revocation.authority_state_sha256 = `sha256:${"9".repeat(64)}`; + const [authorityRef, authorityBundle] = pair("qualification-authority-state-receipt", authority); + const [revocationRef, revocationBundle] = pair("qualification-revocation-state-receipt", revocation); + authorityRef.semantic_identity_sha256 = authorityIdentity; + revocationRef.semantic_identity_sha256 = revocationIdentity; + const registry = { repository: "OpenAdaptAI/.github", entries: [], signer_registry: pointer }; + const entry = (ref) => Object.fromEntries(["kind", "object_schema_version", "object_path", "object_sha256", "size_bytes", "object_media_type", + "semantic_identity_sha256", "subject_sha256", "registry_entry_sha256"].map((key) => [key, ref[key]])); + registry.entries.push(...[authorityRef, authorityBundle, revocationRef, revocationBundle].map(entry)); + const records = []; + const references = []; + for (const id of lifecycle.TARGET_IDS.filter((id) => V2_SPEC[id].releaseKind === "package")) { + const target = fixture.projection.targets.find((item) => item.id === id); + const a = target.latest_admission; + const repository = target.source_repository; + const api = `https://api.github.com/repos/${repository}`; + const sha = `sha256:${"1".repeat(64)}`; + const artifact = { name: `${id}-1.2.3-py3-none-any.whl`, kind: "python-wheel", sha256: sha, size_bytes: 12, + media_type: "application/zip", publish_destinations: ["github-release", "pypi"] }; + a.schema_version = "openadapt.qualification-release/v2"; + a.not_before = "2026-09-02T18:24:25Z"; + for (const key of ["admission_id_sha256", "release_sha256", "artifact_inventory_sha256", "publication_staging_sha256", + "authority_state_sha256", "revocation_state_sha256", "signer_registry_sha256"]) a[key] = sha; + a.authority_state_sha256 = authorityIdentity; + a.revocation_state_sha256 = revocationIdentity; + a.signer_registry_sha256 = signerIdentity; + a.release = { kind: "package", version: "1.2.3", tag: "v1.2.3", source_repository: repository, + source_repository_id: "3", source_commit: COMMIT, artifacts: [artifact], deployment_id: null, deployment_sha256: null }; + const [manifestRef, manifestBundle] = pair("production-acceptance-manifest", { target: id }); + const [decisionRef, decisionBundle] = pair("qualification-evidence-decision-receipt", { target: id, issuer_key_id: "fixture-key" }); + const [qualificationRef, qualificationBundle] = pair("qualification-admission", { target: id, admission_id_sha256: sha, + workflow_version_id_sha256: sha, bundle_sha256: sha, admitted_runtime_sha256: sha }); + const [summaryRef, summaryBundle] = pair("production-acceptance-summary", { + target: id, production_acceptance_manifest_reference: manifestRef, production_acceptance_manifest_bundle_reference: manifestBundle, + qualification_evidence_decision_receipt_reference: decisionRef, qualification_evidence_decision_receipt_bundle_reference: decisionBundle, + qualification_admission_reference: qualificationRef, qualification_admission_bundle_reference: qualificationBundle, + }); + a.production_acceptance_summary_reference = summaryRef; + a.production_acceptance_summary_bundle_reference = summaryBundle; + a.publication_staging = { publication_mode: draftMode ? "draft-before-tag" : "already-published-pypi", draft_release_id: "42", + release_author_login: draftMode ? "openadapt-release[bot]" : "prior-author", release_app_bot_user_id: "100", + assets: [{ ...artifact, asset_id: "43", uploader_id: "100", uploader_login: draftMode ? "openadapt-release[bot]" : "prior-author" }], + tag_rulesets: [{ ruleset_id: "10", name: "creation", target: "tag", enforcement: "active", conditions: {}, rules: [{ type: "creation" }] }, + { ruleset_id: "11", name: "immutable", target: "tag", enforcement: "active", conditions: {}, rules: [{ type: "update" }] }] }; + target.admission_history = [a]; + const [ref, bundleRef] = pair("qualification-release", a); + references.push(ref); + registry.entries.push(entry(ref), entry(bundleRef)); + const receipt = { + schema_version: `openadapt.qualification-release-verification-receipt/v${id === "flow" ? 1 : 2}`, + verification_id_sha256: sha, verdict: "verified", evidence_class: a.evidence_class, target: id, claim_scope: a.claim_scope, + admission_object_sha256: ref.object_sha256, admission_bundle_object_sha256: bundleRef.object_sha256, + ...Object.fromEntries(["admission_id_sha256", "release_sha256", "artifact_inventory_sha256", "release_identity", "publication_staging_sha256", + "authority_state_sha256", "revocation_state_sha256", "signer_registry_sha256"].map((key) => [key, a[key]])), + ...Object.fromEntries(["source_repository", "source_repository_id", "source_commit", "version", "tag"].map((key) => [key, a.release[key]])), + draft_release_id: "42", acceptance_summary_object_sha256: summaryRef.object_sha256, acceptance_manifest_object_sha256: manifestRef.object_sha256, + decision_receipt_object_sha256: decisionRef.object_sha256, qualification_admission_object_sha256: qualificationRef.object_sha256, qualification_admission_id_sha256: sha, + workflow_version_id_sha256: sha, workflow_bundle_sha256: sha, admitted_runtime_sha256: sha, + verified_at: "2026-09-02T19:00:00Z", expires_at: null, registry_source_commit: commit, registry_revision: 1, + registry_head_sha256: ref.registry_head_sha256, trust_state_source_commit: commit, + ...(id === "flow" ? {} : { release_kind: "package", deployment_id: null, deployment_sha256: null }), + }; + const projection = { ...receipt }; delete projection.verification_id_sha256; + receipt.verification_id_sha256 = digest(`OpenAdapt qualification release verification receipt v${id === "flow" ? 1 : 2}\0${canonicalJson(projection)}`); + records.push({ schema_version: "openadapt.public-production-lifecycle-verification/v1", target: id, admission_reference: ref, + admission_bundle_reference: bundleRef, verification_receipt: receipt, + current_state: { authority_reference: authorityRef, authority_bundle_reference: authorityBundle, revocation_reference: revocationRef, + revocation_bundle_reference: revocationBundle, signer_registry_pointer: pointer } }); + raw(`${api}/releases/42`, { id: 42, tag_name: "v1.2.3", draft: false, prerelease: false, immutable: draftMode, + author: { login: a.publication_staging.release_author_login, id: 100 }, assets: [{ id: 43, name: artifact.name, size: 12, + digest: sha, state: "uploaded", uploader: { id: 100, login: a.publication_staging.assets[0].uploader_login } }] }); + raw(`${api}/git/ref/tags/v1.2.3`, { ref: "refs/tags/v1.2.3", object: { type: "tag", sha: "e".repeat(40) } }); + raw(`${api}/git/tags/${"e".repeat(40)}`, { sha: "e".repeat(40), object: { type: "commit", sha: COMMIT } }); + raw(`https://pypi.org/pypi/${target.package_index_project}/json`, { info: { version: "1.2.3" }, releases: { "1.2.3": [{ + filename: artifact.name, size: 12, packagetype: "bdist_wheel", yanked: false, digests: { sha256: sha.slice(7) }, + }] } }); + for (const rule of a.publication_staging.tag_rulesets) raw(`${api}/rulesets/${rule.ruleset_id}`, { ...rule, id: Number(rule.ruleset_id) }); + } + fixture.live.admissions = references; + fixture.bytes = jsonBytes(fixture.live); + fixture.projection.source.files.admissions.sha256 = digest(fixture.bytes); + const document = { schema_version: "openadapt.public-production-lifecycle-verifications/v1", source_commit: commit, records }; + const refresh = () => { + raw("/production-lifecycle-verifications.json", document); + raw(`${base}/evidence-registry.json`, registry); + }; + refresh(); + raw("https://api.github.com/repos/OpenAdaptAI/.github/git/ref/heads/main", { object: { sha: commit } }); + const baseFetch = fetchUntilRevoked(fixture); + const fetch = async (url, options) => { + const bytes = responses.get(url.split("?")[0]); + return bytes ? { ...byteResponse(bytes), json: async () => JSON.parse(bytes) } : baseFetch(url, options); + }; + const mutate = (url, change) => { const value = JSON.parse(responses.get(url)); change(value); raw(url, value); }; + return { ...fixture, fetch, responses, registry, document, raw, mutate, refresh, base }; +} + +test("V2 packages require verified records and live authority/artifact checks, including mutable already-published releases", async () => { + const fixture = makeVerifiedV2Fixture(); + const state = await lifecycle.load(fixture.fetch, AFTER_ISSUE); + assert.equal(state.activeTargets.size, 5); + assert.equal(state.activeTargets.get("flow").releaseVersion, "1.2.3"); + assert.equal(state.defaultInstallVerified, true); + assert.equal(state.activeTargets.has("cloud"), false); + fixture.document.records = fixture.document.records.filter((record) => record.target !== "flow"); fixture.refresh(); + const missing = await lifecycle.load(fixture.fetch, AFTER_ISSUE); + assert.equal(missing.activeTargets.has("flow"), false); + assert.equal(missing.defaultInstallVerified, false); +}); + +test("unchanged admission ledger cannot hide changed current trust references", async () => { + for (const kind of ["qualification-authority-state-receipt", "qualification-revocation-state-receipt", "signer_registry"]) { + const fixture = makeVerifiedV2Fixture(); + if (kind === "signer_registry") fixture.registry.signer_registry = { ...fixture.registry.signer_registry, registry_revision: 2 }; + else fixture.registry.entries.push({ ...fixture.registry.entries.find((entry) => entry.kind === kind), object_sha256: `sha256:${"9".repeat(64)}` }); + fixture.refresh(); + assert.equal((await lifecycle.load(fixture.fetch, AFTER_ISSUE)).activeTargets.size, 0, kind); + } + const unrelated = makeVerifiedV2Fixture(); + unrelated.registry.entries.push({ kind: "unrelated-object", object_sha256: "irrelevant" }); unrelated.refresh(); + assert.equal((await lifecycle.load(unrelated.fetch, AFTER_ISSUE)).activeTargets.size, 5); +}); + +test("exact verified state bytes still expire at consumption time", async () => { + for (const option of ["stateExpiry", "signerExpiry"]) { + const fixture = makeVerifiedV2Fixture({ [option]: "2026-09-02T19:30:00Z" }); + assert.equal((await lifecycle.load(fixture.fetch, AFTER_ISSUE)).activeTargets.size, 0, option); + } +}); + +test("V2 public controls fail closed without substituting historical release observations", async () => { + const repo = "https://api.github.com/repos/OpenAdaptAI/openadapt-flow"; + for (const [url, change] of [ + ["https://pypi.org/pypi/openadapt-flow/json", (v) => { v.info.version = "1.2.4"; }], + ["https://pypi.org/pypi/openadapt-flow/json", (v) => { v.releases["1.2.3"][0].yanked = true; }], + ["https://pypi.org/pypi/openadapt-flow/json", (v) => { v.releases["1.2.3"][0].size += 1; }], + [`${repo}/releases/42`, (v) => { v.assets[0].digest = `sha256:${"9".repeat(64)}`; }], + [`${repo}/releases/42`, (v) => { v.assets[0].id += 1; }], + [`${repo}/releases/42`, (v) => { v.assets = []; }], + [`${repo}/git/tags/${"e".repeat(40)}`, (v) => { v.object.sha = "f".repeat(40); }], + [`${repo}/rulesets/11`, (v) => { v.enforcement = "disabled"; }], + ]) { + const fixture = makeVerifiedV2Fixture(); fixture.mutate(url, change); + const state = await lifecycle.load(fixture.fetch, AFTER_ISSUE); + assert.equal(state.activeTargets.has("flow"), false, url); + assert.equal(state.defaultInstallVerified, false, url); + } + const unavailable = makeVerifiedV2Fixture(); unavailable.responses.delete(`${repo}/rulesets/11`); + assert.equal((await lifecycle.load(unavailable.fetch, AFTER_ISSUE)).activeTargets.has("flow"), false); +}); + +test("draft-before-tag keeps the stronger post-publication immutability requirement", async () => { + const fixture = makeVerifiedV2Fixture({ draftMode: true }); + assert.equal((await lifecycle.load(fixture.fetch, AFTER_ISSUE)).activeTargets.has("flow"), true); + fixture.mutate("https://api.github.com/repos/OpenAdaptAI/openadapt-flow/releases/42", (v) => { v.immutable = false; }); + assert.equal((await lifecycle.load(fixture.fetch, AFTER_ISSUE)).activeTargets.has("flow"), false); +}); + +test("a stale or forged generated result cannot admit a different latest object", async () => { + for (const change of [ + (f) => { f.document.source_commit = "e".repeat(40); }, + (f) => { f.document.records.find((r) => r.target === "flow").verification_receipt.version = "1.2.4"; }, + (f) => { f.document.records.find((r) => r.target === "flow").admission_bundle_reference.object_sha256 = `sha256:${"9".repeat(64)}`; }, + ]) { + const fixture = makeVerifiedV2Fixture(); change(fixture); fixture.refresh(); + assert.equal((await lifecycle.load(fixture.fetch, AFTER_ISSUE)).activeTargets.has("flow"), false); + } +}); + + +test("current state with future activation or an effective key revocation refuses", async () => { + for (const options of [{ stateNotBefore: "2026-09-03T00:00:00Z" }, { keyRevokedAt: "2026-09-02T19:30:00Z" }]) { + const fixture = makeVerifiedV2Fixture(options); + assert.equal((await lifecycle.load(fixture.fetch, AFTER_ISSUE)).activeTargets.size, 0); + } +}); + + +test("jointly rehashed current objects must still match the admitted trust identities", async () => { + const fixture = makeVerifiedV2Fixture({ mismatchedState: true }); + const state = await lifecycle.load(fixture.fetch, AFTER_ISSUE); + assert.equal(state.activeTargets.size, 0); +}); + + +test("every exact dependency statement keeps its independent validity window", async () => { + for (const kind of ["qualification-release", "production-acceptance-summary", "production-acceptance-manifest", + "qualification-evidence-decision-receipt", "qualification-admission"]) { + const fixture = makeVerifiedV2Fixture({ bundleExpiryKind: kind }); + const before = await lifecycle.load(fixture.fetch, Date.parse("2026-09-02T19:15:00Z")); + assert.equal(before.activeTargets.has("flow"), true, kind); + const after = await lifecycle.load(fixture.fetch, AFTER_ISSUE); + assert.equal(after.activeTargets.has("flow"), false, kind); + const future = makeVerifiedV2Fixture({ bundleStartKind: kind }); + assert.equal((await lifecycle.load(future.fetch, Date.parse("2026-09-02T19:15:00Z"))).activeTargets.has("flow"), false, kind); + } +}); + + +test("the current signer check includes keys used only by dependent evidence", async () => { + const fixture = makeVerifiedV2Fixture({ evidenceKeyRevokedAt: "2026-09-02T19:30:00Z" }); + assert.equal((await lifecycle.load(fixture.fetch, Date.parse("2026-09-02T19:15:00Z"))).activeTargets.has("flow"), true); + assert.equal((await lifecycle.load(fixture.fetch, AFTER_ISSUE)).activeTargets.has("flow"), false); +}); diff --git a/tests/test_production_lifecycle_policy.py b/tests/test_production_lifecycle_policy.py index b1c9bf0..cba02b4 100644 --- a/tests/test_production_lifecycle_policy.py +++ b/tests/test_production_lifecycle_policy.py @@ -114,7 +114,7 @@ def test_render_preserves_history_and_selects_highest_sequence(self) -> None: target["latest_admission"]["admission_id"], "production:flow:2" ) - def test_committed_projection_admits_all_seven_and_is_schema_bound(self) -> None: + def test_committed_projection_retains_seven_targets_and_is_schema_bound(self) -> None: output = json.loads( (ROOT / "docs" / "production-lifecycle.json").read_text(encoding="utf-8") ) @@ -139,7 +139,7 @@ def test_committed_projection_admits_all_seven_and_is_schema_bound(self) -> None "agent": "2.0.1", "capture": "1.2.2", "desktop": "0.16.0", - "flow": "1.34.0", + "flow": "1.35.1", "openadapt": "1.16.0", } for target_id, version in expected_versions.items(): @@ -150,7 +150,7 @@ def test_committed_projection_admits_all_seven_and_is_schema_bound(self) -> None ) self.assertEqual(target["latest_admission"]["verdict"], "accepted") self.assertIsNone(target["latest_admission"]["expires_at"]) - self.assertEqual(len(target["admission_history"]), 1) + self.assertEqual(len(target["admission_history"]), 2 if target_id == "flow" else 1) for target_id in ("cloud", "docs"): target = by_id[target_id] self.assertEqual(target["latest_admission"]["release"]["kind"], "deployment") diff --git a/tests/test_production_lifecycle_verification.py b/tests/test_production_lifecycle_verification.py new file mode 100644 index 0000000..6d6fd00 --- /dev/null +++ b/tests/test_production_lifecycle_verification.py @@ -0,0 +1,379 @@ +"""Test record selection and the canonical-verifier process boundary. + +These tests exercise the Ops wrapper, not the canonical signature implementation. +Full generation invokes that separate public verifier with actual release bytes. +""" + +from __future__ import annotations + +import copy +import base64 +import importlib.util +import json +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path +from types import SimpleNamespace +from datetime import datetime, timezone +from unittest.mock import Mock, patch + +ROOT = Path(__file__).resolve().parents[1] +SPEC = importlib.util.spec_from_file_location( + "generate_production_lifecycle_verification", + ROOT / "scripts" / "generate_production_lifecycle_verification.py", +) +assert SPEC is not None and SPEC.loader is not None +module = importlib.util.module_from_spec(SPEC) +SPEC.loader.exec_module(module) + + +def admission(sequence: int = 1) -> dict: + return { + "target": "flow", "claim_scope": "production_flow", + "evidence_class": "remote-safe-synthetic", + "release_identity": {"sequence": sequence}, + "release": { + "artifacts": [{"name": "candidate.whl"}], + "source_repository": "OpenAdaptAI/openadapt-flow", + "source_repository_id": "1291376938", "source_commit": "b" * 40, + "version": "1.35.1", "tag": "v1.35.1", + "deployment_id": None, "deployment_sha256": None, + }, + "production_acceptance_summary_reference": {"id": "summary"}, + "production_acceptance_summary_bundle_reference": {"id": "summary-bundle"}, + } + + +class RecordTests(unittest.TestCase): + def setUp(self) -> None: + self.source = {"source_commit": "a" * 40} + self.reference = {"kind": "qualification-release", "id": "latest"} + self.admission = admission(2) + self.summary = { + f"{kind}_{suffix}": {"id": f"{kind}-{suffix}"} + for kind in ("production_acceptance_manifest", + "qualification_evidence_decision_receipt", "qualification_admission") + for suffix in ("reference", "bundle_reference") + } + self.receipt = {"verdict": "verified", "verified_at": "2026-09-09T18:50:00Z", + "trust_state_source_commit": "c" * 40, "version": "1.35.1"} + self.state = {key: {"id": key} for key in module.CURRENT_STATE_FIELDS} + self.record = { + "schema_version": module.RECORD_SCHEMA, "target": "flow", + "admission_reference": self.reference, + "admission_bundle_reference": {"id": "latest-bundle"}, + "verification_receipt": copy.deepcopy(self.receipt), + "current_state": copy.deepcopy(self.state), + } + self.latest = {"flow": (self.reference, self.admission)} + self.store = Mock() + self.store.pair.side_effect = lambda reference, bundle: ( + self.summary if reference == {"id": "summary"} else {} + ) + self.store.verifier.trust.require_timestamp.side_effect = lambda value, label: value + self.store.verifier.verification_receipt.return_value = self.receipt + self.store.current_state.return_value = self.state + + def document(self, records=None) -> dict: + return {"schema_version": module.DOCUMENT_SCHEMA, **self.source, + "records": [self.record] if records is None else records} + + def test_check_keeps_retained_time_and_uses_canonical_chain_validation(self) -> None: + document = self.document() + before = copy.deepcopy(document) + with patch.object(module, "run_verifier", side_effect=AssertionError("must not regenerate")): + module.validate_document(document, self.source, self.latest, self.store) + self.assertEqual(document, before) + kwargs = self.store.verifier.verification_receipt.call_args.kwargs + self.assertEqual(kwargs["verified_at"], self.receipt["verified_at"]) + self.store.verifier.trust.validate_release_evidence_chain.assert_called_once() + self.store.verifier.trust.validate_admission_current_state.assert_called_once() + self.assertEqual(self.store.verify_pair.call_count, 7) + self.assertEqual({call.args[0]["id"] for call in self.store.verify_pair.call_args_list}, { + "latest", "summary", "production_acceptance_manifest-reference", + "qualification_evidence_decision_receipt-reference", "qualification_admission-reference", + "authority_reference", "revocation_reference", + }) + self.assertTrue(all(call.kwargs == {"now": self.receipt["verified_at"]} + for call in self.store.verify_pair.call_args_list)) + + def test_latest_selection_never_falls_back_to_an_accepted_older_row(self) -> None: + older = {"kind": "qualification-release", "id": "older"} + newer = {"kind": "qualification-release", "id": "newer"} + values = {"older": admission(1), "newer": {**admission(2), "verdict": "revoked"}} + self.store.read_object.side_effect = lambda ref, **kwargs: values[ref["id"]] + result = module.latest_admissions( + {"schema_version": module.projection.ADMISSIONS_SCHEMA, "admissions": [newer, older]}, self.store + ) + self.assertEqual(result["flow"][0], newer) + + def test_duplicate_sequences_refuse_instead_of_selecting_by_order(self) -> None: + self.store.read_object.return_value = admission() + with self.assertRaisesRegex(module.VerificationError, "sequence is duplicated"): + module.latest_admissions( + {"schema_version": module.projection.ADMISSIONS_SCHEMA, + "admissions": [self.reference, self.reference]}, self.store + ) + + def test_mixed_unsupported_admission_does_not_restore_a_v2_record(self) -> None: + self.store.read_object.return_value = admission() + latest = module.latest_admissions( + {"schema_version": module.projection.ADMISSIONS_SCHEMA, + "admissions": [self.reference, {"target": "flow", "sequence": 2}]}, self.store + ) + self.assertNotIn("flow", latest) + + def test_stale_admission_wrong_state_receipt_and_extra_fields_refuse(self) -> None: + mutations = ( + (lambda r: r.update(admission_reference={"id": "older"}), "latest admission"), + (lambda r: r["current_state"].update(authority_reference={"id": "older"}), "current registered"), + (lambda r: r["verification_receipt"].update(version="1.34.0"), "receipt differs"), + (lambda r: r.update(active=True), "fields differ"), + (lambda r: r["current_state"].update(active=True), "fields differ"), + ) + for change, message in mutations: + with self.subTest(message=message): + record = copy.deepcopy(self.record) + change(record) + with self.assertRaisesRegex(module.VerificationError, message): + module.validate_record(record, self.latest, self.store) + + def test_wrong_source_and_duplicate_target_refuse(self) -> None: + wrong_source = self.document() + wrong_source["source_commit"] = "d" * 40 + with self.assertRaisesRegex(module.VerificationError, "projection source"): + module.validate_document(wrong_source, self.source, self.latest, self.store) + with self.assertRaisesRegex(module.VerificationError, "target is duplicated"): + module.validate_document(self.document([self.record, self.record]), + self.source, self.latest, self.store) + + def test_empty_records_make_no_verification_claim(self) -> None: + module.validate_document(self.document([]), self.source, {}, self.store) + self.store.verifier.verification_receipt.assert_not_called() + + def test_evidence_class_is_independent_of_release_version(self) -> None: + self.admission["evidence_class"] = "customer-production" + with self.assertRaisesRegex(module.VerificationError, "remote-safe-synthetic"): + module.validate_record(self.record, self.latest, self.store) + + def test_generation_never_returns_a_record_after_canonical_refusal(self) -> None: + with patch.object(module, "run_verifier", side_effect=module.VerificationError("refused")): + with self.assertRaisesRegex(module.VerificationError, "refused"): + module.generate_record("flow", self.latest, self.store, Path("/verifier"), Path("/artifacts")) + self.store.current_state.assert_not_called() + + +class CanonicalStoreTests(unittest.TestCase): + def test_offline_fetch_refuses_other_origins_mutable_commits_and_paths(self) -> None: + store = module.CanonicalStore(Path("/unused"), None, {}) + for url in ( + "https://example.com/object.json", + "https://raw.githubusercontent.com/OpenAdaptAI/.github/main/object.json", + "https://raw.githubusercontent.com/OpenAdaptAI/.github/" + "a" * 40 + "/../secret", + "https://raw.githubusercontent.com/OpenAdaptAI/.github/" + "a" * 40 + "/%2e%2e/secret", + ): + with self.subTest(url=url), self.assertRaises(module.VerificationError): + store.fetch(url) + + def test_outer_failure_restores_verifier_fetch_and_clock(self) -> None: + with tempfile.TemporaryDirectory() as directory: + policy = Path(directory) / "policy.json" + policy.write_text("{}") + original_fetch = Mock() + verifier = SimpleNamespace( + fetch=original_fetch, datetime=datetime, POLICY_PATH=policy, + public_trust=SimpleNamespace(STATEMENT_MEDIA_TYPE="public-dsse"), + fetch_pair=Mock(return_value=()), + verify_registered_signature=Mock(side_effect=ValueError("invalid signature")), + ) + store = module.CanonicalStore(Path(directory), verifier, {}) + store.pair = Mock() + store.read = Mock(return_value={"dsseEnvelope": {"payloadType": "public-dsse"}}) + with self.assertRaisesRegex(ValueError, "invalid signature"): + store.verify_pair({}, {}, now=datetime(2026, 9, 9, tzinfo=timezone.utc)) + self.assertIs(verifier.fetch, original_fetch) + self.assertIs(verifier.datetime, datetime) + + def test_outer_verification_refuses_a_keyless_profile_without_dispatch(self) -> None: + verifier = SimpleNamespace( + public_trust=SimpleNamespace(STATEMENT_MEDIA_TYPE="public-dsse"), fetch_pair=Mock() + ) + store = module.CanonicalStore(Path("/unused"), verifier, {}) + store.pair = Mock() + store.read = Mock(return_value={"dsseEnvelope": {"payloadType": "keyless"}}) + with self.assertRaisesRegex(module.VerificationError, "public DSSE profile"): + store.verify_pair({}, {}, now=datetime(2026, 9, 9, tzinfo=timezone.utc)) + verifier.fetch_pair.assert_not_called() + + def test_current_state_uses_last_relevant_entries_and_adjacent_bundles(self) -> None: + verifier = SimpleNamespace(evidence=SimpleNamespace( + REFERENCE_SCHEMA="reference/v2", REPOSITORY="OpenAdaptAI/.github", + REPOSITORY_ID="858454062", REPOSITORY_OWNER_ID="132681217", + )) + store = module.CanonicalStore(Path("/unused"), verifier, {}) + entries = [ + {"kind": "qualification-authority-state-receipt", "id": "authority-older"}, + {"kind": "qualification-revocation-state-receipt", "id": "revocation-older"}, + {"kind": "qualification-authority-state-receipt", "id": "authority-current"}, + {"kind": "qualification-revocation-state-receipt", "id": "revocation-current"}, + {"kind": "qualification-release", "id": "unrelated"}, + ] + registry = {"revision": 9, "registry_head_sha256": "sha256:" + "f" * 64, + "signer_registry": {"id": "current-signer"}} + store.registry = Mock(return_value=(registry, entries)) + store.signer = Mock(return_value={}) + store.bundle = Mock(side_effect=lambda reference: {"subject": reference["id"]}) + state = store.current_state("a" * 40) + self.assertEqual(state["authority_reference"]["id"], "authority-current") + self.assertEqual(state["revocation_reference"]["id"], "revocation-current") + self.assertEqual(state["authority_bundle_reference"], {"subject": "authority-current"}) + self.assertEqual(state["signer_registry_pointer"], {"id": "current-signer"}) + self.assertEqual(state["authority_reference"]["registry_source_commit"], "a" * 40) + + +def check_retained_signed_pairs(tree: Path) -> None: + """Use the pinned upstream fixture's unchanged real public signatures.""" + with module.canonical_verifier(tree) as verifier: + spec = importlib.util.spec_from_file_location( + "_canonical_signed_fixture", tree / "tests" / "test_registered_software_verifier.py" + ) + fixture_module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(fixture_module) + fixture = fixture_module.RegisteredSoftwareVerifierTests() + fixture.setUp() + try: + with tempfile.TemporaryDirectory(prefix="ops-signed-fixture-") as directory: + root = Path(directory) + def store_files(): + for name, raw in fixture.files.items(): + path = root / name + path.parent.mkdir(parents=True, exist_ok=True) + path.write_bytes(raw) + (root / "evidence-registry.json").write_bytes(fixture_module.canonical(fixture.registry)) + return module.CanonicalStore(root, verifier, {fixture_module.STORAGE_COMMIT: root}) + + instant = datetime(2026, 9, 9, 18, 50, tzinfo=timezone.utc) + store = store_files() + for digest in (fixture_module.RELEASE_SHA, fixture_module.AUTHORITY_SHA, + fixture_module.REVOCATION_SHA): + reference = fixture.reference(digest) + store.verify_pair(reference, store.bundle(reference), now=instant) + reference = fixture.reference(fixture_module.RELEASE_SHA) + release = store.read(reference) + summary = json.loads((tree / release["production_acceptance_summary_reference"]["object_path"]).read_bytes()) + qualification = json.loads((tree / summary["qualification_admission_reference"]["object_path"]).read_bytes()) + + def receipt(current_store): + current_ref = fixture.reference(fixture_module.RELEASE_SHA) + return verifier.verification_receipt( + admission=release, admission_reference=current_ref, + admission_bundle_reference=current_store.bundle(current_ref), + summary=summary, qualification_admission=qualification, + verified_at=instant, trust_state_source_commit=fixture_module.STORAGE_COMMIT, + ) + + before = receipt(store) + fixture.replace_bundle(fixture_module.RELEASE_SHA, lambda bundle: ( + bundle["dsseEnvelope"]["signatures"][0].update( + sig=base64.b64encode(bytes(64)).decode() + ) + )) + store = store_files() + after = receipt(store) + assert before["verification_id_sha256"] != after["verification_id_sha256"] + reference = fixture.reference(fixture_module.RELEASE_SHA) + bundle = store.bundle(reference) + # Byte, semantic identity, registry, adjacency, and receipt hashes + # have all been recomputed. Only the signature check rejects it. + store.pair(reference, bundle) + try: + store.verify_pair(reference, bundle, now=instant) + except verifier.trust.TrustError as exc: + assert "signature" in str(exc).lower(), str(exc) + else: + raise AssertionError("corrupted registered outer signature was accepted") + finally: + fixture.doCleanups() + + +class SignedPairIntegrationTests(unittest.TestCase): + def test_retained_signatures_and_rehashed_corrupt_bundle(self) -> None: + # Pin public fixture and verifier code together. Reuse the same dependency + # selection as the command; no private signing key or service is needed. + commit = "f0fb9cc812c0d1653b1ec2998749b078f244308e" + with tempfile.TemporaryDirectory(prefix="ops-signed-verifier-") as directory: + root = Path(directory) + tree = root / commit + module.projection.materialize_commit(commit, tree) + python = module.projection._validator_python(tree, root / "runtime") + result = subprocess.run( + [python, "-c", "import runpy,sys; from pathlib import Path; " + "runpy.run_path(sys.argv[1])['check_retained_signed_pairs'](Path(sys.argv[2]))", + str(Path(__file__).resolve()), str(tree)], + capture_output=True, text=True, check=False, timeout=120, + ) + self.assertEqual(result.returncode, 0, result.stdout + result.stderr) + + +class VerifierProcessTests(unittest.TestCase): + def test_dependency_reexecution_preserves_arguments_and_exit_status(self) -> None: + args = ["--check", "--source", "source with spaces.json", "--output", "output.json"] + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + tree = root / "exact-canonical-tree" + for status in (0, 7): + with self.subTest(status=status), patch.object( + module.projection, "_validator_python", return_value="/pinned/venv/bin/python" + ) as selector, patch.object( + module.subprocess, "run", return_value=subprocess.CompletedProcess([], status) + ) as runner: + self.assertEqual(module.run_with_dependencies(tree, root, args), status) + selector.assert_called_once_with(tree, root / "runtime") + runner.assert_called_once_with( + ["/pinned/venv/bin/python", str(Path(module.__file__).resolve()), *args], + check=False, timeout=900, + ) + + def test_current_interpreter_does_not_start_another_process(self) -> None: + with patch.object(module.projection, "_validator_python", return_value=sys.executable), patch.object( + module.subprocess, "run" + ) as runner: + self.assertIsNone(module.run_with_dependencies(Path("/tree"), Path("/runtime"), ["--check"])) + runner.assert_not_called() + + def test_subprocess_receives_exact_pinned_verifier_identity_and_inventory(self) -> None: + # A real subprocess checks the wrapper's CLI contract. Signature checking + # remains the canonical verifier's contract and is not simulated here. + with tempfile.TemporaryDirectory() as directory: + tree = Path(directory) + (tree / "scripts").mkdir() + (tree / "scripts" / "verify_production_release_admission.py").write_text( + "import json,sys\n" + "from pathlib import Path\n" + "args=dict(zip(sys.argv[1::2],sys.argv[2::2]))\n" + "inventory=json.loads(Path(args['--artifact-inventory']).read_text())\n" + "assert inventory['artifacts']==[{'name':'candidate.whl'}]\n" + "assert args['--expected-version']=='1.35.1'\n" + "assert args['--expected-source-commit']=='b'*40\n" + "assert '--expected-deployment-id' not in args\n" + "assert json.loads(Path(args['--admission-reference']).read_text())=={'id':'admission'}\n" + "print(json.dumps({'verdict':'verified'}))\n", encoding="utf-8" + ) + result = module.run_verifier(tree, {"id": "admission"}, {"id": "bundle"}, + admission(), tree / "artifacts", python=sys.executable) + self.assertEqual(result, {"verdict": "verified"}) + + def test_nonzero_exit_and_non_receipt_output_refuse(self) -> None: + for result, message in ( + (subprocess.CompletedProcess([], 1, "", "REFUSED: stale state"), "stale state"), + (subprocess.CompletedProcess([], 0, '{"verdict":"refused"}', ""), "no verified receipt"), + ): + with self.subTest(message=message), patch.object(module.subprocess, "run", return_value=result): + with self.assertRaisesRegex(module.VerificationError, message): + module.run_verifier(Path("/verifier"), {}, {}, admission(), Path("/artifacts")) + + +if __name__ == "__main__": + unittest.main() diff --git a/tests/test_production_workflow_admissions.py b/tests/test_production_workflow_admissions.py index 4e504c8..ce95260 100644 --- a/tests/test_production_workflow_admissions.py +++ b/tests/test_production_workflow_admissions.py @@ -17,9 +17,9 @@ MODULE = importlib.util.module_from_spec(SPEC) SPEC.loader.exec_module(MODULE) -PINNED_COMMIT = "34207373d1e21de90772e0343c1abfbf477483e0" +PINNED_COMMIT = "5b48240494a250633eec8fcf99fab26553efd0c6" PINNED_LEDGER_SHA256 = ( - "sha256:fa3b4cc4ed0ab62d8d4ff5705495ec0a82f0572654617a152fd9675818684150" + "sha256:ca24700ebf15acb3e7cd7c19b07d157c27ba90f2dd9705aa871c4015adf11857" ) @@ -42,7 +42,7 @@ def _source() -> dict: class ProductionWorkflowAdmissionsProjectionTests(unittest.TestCase): - def test_committed_projection_lists_seven_synthetic_admissions(self) -> None: + def test_committed_projection_retains_tutorial_and_flow_reference_records(self) -> None: source = json.loads( (ROOT / "production-workflow-admissions-source.json").read_text( encoding="utf-8" @@ -59,11 +59,16 @@ def test_committed_projection_lists_seven_synthetic_admissions(self) -> None: ) self.assertEqual(projection["source"], source) self.assertEqual(projection["schema_version"], MODULE.OUTPUT_SCHEMA) - self.assertEqual(len(projection["admissions"]), 7) + self.assertEqual(len(projection["admissions"]), 8) + versions = [row["bundle_version"] for row in projection["admissions"]] + self.assertEqual(versions.count("0.0.0-synthetic"), 7) + self.assertEqual(versions.count("1.35.1-reference.1"), 1) + flow = next(row for row in projection["admissions"] if row["bundle_version"] == "1.35.1-reference.1") + self.assertEqual(flow["object_sha256"], "sha256:947224523757df41127be021fc66adee5746f07ff1a0e913319c5461e430e34d") for row in projection["admissions"]: self.assertEqual(row["kind"], "qualification-admission") self.assertEqual(row["evidence_class"], "remote-safe-synthetic") - self.assertEqual(row["bundle_version"], "0.0.0-synthetic") + self.assertIn(row["bundle_version"], {"0.0.0-synthetic", "1.35.1-reference.1"}) self.assertEqual(row["verdict"], "accepted") self.assertIsNone(row["expires_at"]) encoded = json.dumps(projection) @@ -145,6 +150,40 @@ def test_render_refuses_non_synthetic_evidence_class(self) -> None: tree=tree, ) + def test_versioned_reference_bundle_preserves_the_public_evidence_boundary(self) -> None: + for version, evidence_class, extra, accepted in ( + ("0.0.0-synthetic", "remote-safe-synthetic", {}, True), + ("1.35.1-reference.1", "remote-safe-synthetic", {}, True), + ("1.35.1-reference.1", "customer-production", {}, False), + ("1.35.1-reference.1", "remote-safe-synthetic", {"production_acceptance": {}}, False), + ("1.35.1-reference.1", "remote-safe-synthetic", {"name": "MockMed"}, False), + (None, "remote-safe-synthetic", {}, False), + ("customer workflow", "remote-safe-synthetic", {}, False), + ): + with self.subTest(version=version, evidence_class=evidence_class, extra=extra): + body = json.dumps({"bundle_version": version, "evidence_class": evidence_class, + "expires_at": None, "verdict": "accepted", **extra}).encode() + digest = MODULE._digest_bytes(body) + object_path = "production-evidence/objects/sha256/aa/test.qualification-admission.json" + row = {"kind": "qualification-admission", "object_path": object_path, "object_sha256": digest} + ledger = {"$schema": "schemas/production-workflow-admissions.schema.json", + "schema_version": MODULE.LEDGER_SCHEMA, + "policy_sha256": "sha256:" + "a" * 64, "admissions": [row]} + with tempfile.TemporaryDirectory() as directory: + tree = Path(directory) + target = tree / object_path + target.parent.mkdir(parents=True) + target.write_bytes(body) + if not accepted: + with self.assertRaises(MODULE.RenderError): + MODULE.render(_source(), {"admissions": json.dumps(ledger).encode()}, tree=tree) + else: + result = MODULE.render(_source(), {"admissions": json.dumps(ledger).encode()}, tree=tree) + self.assertEqual(result["admissions"], [{**row, "bundle_version": version, + "evidence_class": evidence_class, + "expires_at": None, "verdict": "accepted"}]) + self.assertNotIn("active", result["admissions"][0]) + class ProductionWorkflowAdmissionsCopyTests(unittest.TestCase): def test_llms_names_public_synthetic_ledger_not_a_customer_job(self) -> None: @@ -155,12 +194,12 @@ def test_llms_names_public_synthetic_ledger_not_a_customer_job(self) -> None: text, ) self.assertIn("seven active target admissions", text) - self.assertIn("seven active synthetic admissions", text) + self.assertIn("A retained row doesn't establish current qualification", text) self.assertIn("0.0.0-synthetic", text) self.assertIn("remote-safe-synthetic", text) self.assertIn("production-workflow-admissions.json", text) - self.assertIn(PINNED_COMMIT, text) - self.assertIn("isn't a customer workflow", text) + self.assertIn("Synthetic evidence does not qualify a customer workflow", text) + self.assertNotIn("seven active synthetic admissions", text) self.assertNotIn("no target is actively admitted", text) self.assertNotIn("null expiry", text) self.assertNotIn("customer job is admitted", text.lower()) @@ -173,9 +212,9 @@ def test_lifecycle_page_names_public_synthetic_ledger(self) -> None: collapsed = " ".join(text.split()) self.assertIn("0.0.0-synthetic", text) self.assertIn("production-workflow-admissions.json", text) - self.assertIn(PINNED_COMMIT, text) - self.assertIn("isn't a customer workflow", collapsed) - self.assertIn("seven active synthetic admissions", collapsed) + self.assertIn("A retained row doesn't establish current qualification", collapsed) + self.assertIn("Synthetic evidence does not qualify a customer workflow", collapsed) + self.assertNotIn("seven active synthetic admissions", collapsed) self.assertIn("seven Production targets", text) self.assertNotIn("none is actively admitted", collapsed) self.assertNotIn("No target is actively admitted", text)