diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index f458572eb7..4c9f085b39 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -189,7 +189,7 @@ jobs: node-version: ${{ steps.node-version.outputs.node-version }} - name: Run shellcheck - run: shellcheck scripts/docs-proposal/*.sh + run: shellcheck scripts/docs-proposal/*.sh scripts/release/phase1-build.sh - name: Test hunk review builder run: node --test scripts/docs-proposal/hunks.test.mjs diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml new file mode 100644 index 0000000000..251840eaf4 --- /dev/null +++ b/.github/workflows/release-build.yml @@ -0,0 +1,166 @@ +name: Release build and signing +on: + workflow_call: +permissions: + contents: read +jobs: + checks: + runs-on: ubuntu-24.04 + permissions: + contents: read + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + - run: node --test scripts/release/phase1.test.mjs + build: + needs: checks + name: Build ${{ matrix.asset }} + runs-on: ${{ matrix.os }} + timeout-minutes: 90 + permissions: + contents: read + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-24.04 + package: trusted-server-adapter-fastly + target: wasm32-wasip1 + asset: trusted-server-fastly.wasm + - os: ubuntu-24.04 + package: trusted-server-cli + target: x86_64-unknown-linux-gnu + asset: ts-x86_64-unknown-linux-gnu + - os: macos-15 + package: trusted-server-cli + target: aarch64-apple-darwin + asset: ts-aarch64-apple-darwin + - os: macos-15-intel + package: trusted-server-cli + target: x86_64-apple-darwin + asset: ts-x86_64-apple-darwin + - os: ubuntu-24.04 + package: trusted-server-adapter-axum + target: x86_64-unknown-linux-gnu + asset: trusted-server-axum-x86_64-unknown-linux-gnu + - os: ubuntu-24.04 + package: trusted-server-adapter-axum + target: x86_64-unknown-linux-musl + asset: trusted-server-axum-x86_64-unknown-linux-musl + - os: ubuntu-24.04-arm + package: trusted-server-adapter-axum + target: aarch64-unknown-linux-gnu + asset: trusted-server-axum-aarch64-unknown-linux-gnu + steps: + - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 + with: + persist-credentials: false + - name: Read pinned toolchains + id: tools + shell: bash + run: | + set -euo pipefail + echo "node=$(awk '$1 == "nodejs" { print $2 }' .tool-versions)" >> "$GITHUB_OUTPUT" + echo "rust=$(sed -n 's/^channel = "\([^"]*\)"/\1/p' rust-toolchain.toml)" >> "$GITHUB_OUTPUT" + - uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0 + with: + node-version: ${{ steps.tools.outputs.node }} + - name: Install the pinned Rust toolchain without build caches + env: + RUST_VERSION: ${{ steps.tools.outputs.rust }} + BUILD_TARGET: ${{ matrix.target }} + run: rustup toolchain install "$RUST_VERSION" --profile minimal --target "$BUILD_TARGET" + - name: Build and generate the build inventory in a clean environment + shell: bash + env: + BUILD_PACKAGE: ${{ matrix.package }} + BUILD_TARGET: ${{ matrix.target }} + RELEASE_ASSET: ${{ matrix.asset }} + SOURCE_COMMIT: ${{ github.sha }} + RELEASE_VERSION: ${{ github.ref_name }} + run: | + set -euo pipefail + env -i PATH="$PATH" HOME="$HOME" \ + CARGO_HOME="${CARGO_HOME:-$HOME/.cargo}" \ + RUSTUP_HOME="${RUSTUP_HOME:-$HOME/.rustup}" \ + TMPDIR="${TMPDIR:-/tmp}" RUNNER_TEMP="$RUNNER_TEMP" \ + SOURCE_COMMIT="$SOURCE_COMMIT" RELEASE_VERSION="$RELEASE_VERSION" \ + bash scripts/release/phase1-build.sh "$BUILD_PACKAGE" "$BUILD_TARGET" "$RELEASE_ASSET" + - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: release-${{ matrix.asset }} + path: release-assets/* + if-no-files-found: error + retention-days: 7 + + sign: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && vars.RELEASE_PUBLICATION_ENABLED == 'true' + needs: build + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + contents: read + id-token: write + attestations: write + strategy: + fail-fast: false + matrix: + asset: + [ + trusted-server-axum-x86_64-unknown-linux-gnu, + trusted-server-axum-x86_64-unknown-linux-musl, + trusted-server-axum-aarch64-unknown-linux-gnu, + trusted-server-fastly.wasm, + ts-aarch64-apple-darwin, + ts-x86_64-apple-darwin, + ts-x86_64-unknown-linux-gnu, + ] + steps: + # Only fixed inline checks execute here; never checkout or execute build inputs. + - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + name: release-${{ matrix.asset }} + path: release-assets + - name: Validate signing inputs and retain their digests + env: + ASSET: ${{ matrix.asset }} + run: | + set -euo pipefail + cd release-assets + test "$(find . -mindepth 1 -maxdepth 1 | wc -l)" -eq 2 + for file in "$ASSET" "$ASSET.spdx.json"; do test -f "$file" && test ! -L "$file" && test -s "$file"; done + sha256sum "$ASSET" "$ASSET.spdx.json" > ../inputs.sha256 + - name: Attest SLSA build provenance + id: provenance + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 + with: + subject-path: | + release-assets/${{ matrix.asset }} + release-assets/${{ matrix.asset }}.spdx.json + - name: Attest the binary's SPDX SBOM + id: sbom + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 + with: + subject-path: release-assets/${{ matrix.asset }} + sbom-path: release-assets/${{ matrix.asset }}.spdx.json + - name: Retain only this run's real bundles and signed-input digests + env: + ASSET: ${{ matrix.asset }} + PROVENANCE: ${{ steps.provenance.outputs.bundle-path }} + SBOM: ${{ steps.sbom.outputs.bundle-path }} + run: | + set -euo pipefail + (cd release-assets && sha256sum --check ../inputs.sha256) + test -s "$PROVENANCE" && test -s "$SBOM" + mkdir signed-assets + cp "$PROVENANCE" "signed-assets/$ASSET.provenance.sigstore.json" + cp "$SBOM" "signed-assets/$ASSET.sbom.sigstore.json" + cp inputs.sha256 signed-assets/SHA256SUMS + (cd signed-assets && sha256sum "$ASSET.provenance.sigstore.json" "$ASSET.sbom.sigstore.json" >> SHA256SUMS) + - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + with: + name: signed-${{ matrix.asset }} + path: signed-assets/* + if-no-files-found: error + retention-days: 7 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000000..ed8efdfb98 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,282 @@ +name: Binary attestations +on: + push: + tags: ['v*'] + pull_request: + paths: + - '.github/workflows/release*.yml' + - 'scripts/release/phase1*' + - 'scripts/release/release-cohort.json' + - 'Cargo.toml' + - 'Cargo.lock' + - 'rust-toolchain.toml' + - '.tool-versions' + - '.cargo/**' + - 'edgezero.toml' + - 'crates/trusted-server-core/src/**' + - 'crates/trusted-server-core/build_support/**' + - 'crates/trusted-server-core/build.rs' + - 'crates/trusted-server-core/Cargo.toml' + - 'crates/trusted-server-cli/**' + - 'crates/trusted-server-js/**' + - 'crates/trusted-server-adapter-fastly/**' + - 'crates/trusted-server-adapter-axum/**' + - 'crates/trusted-server-openrtb*/**' +permissions: + contents: read +concurrency: + group: binary-attestations-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} +jobs: + build: + uses: ./.github/workflows/release-build.yml + permissions: + contents: read + id-token: write + attestations: write + release: + if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') && vars.RELEASE_PUBLICATION_ENABLED == 'true' + needs: build + runs-on: ubuntu-24.04 + timeout-minutes: 15 + permissions: + contents: write + steps: + - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + name: release-trusted-server-fastly.wasm + path: build/trusted-server-fastly.wasm + - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + name: signed-trusted-server-fastly.wasm + path: signed/trusted-server-fastly.wasm + - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + name: release-ts-aarch64-apple-darwin + path: build/ts-aarch64-apple-darwin + - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + name: signed-ts-aarch64-apple-darwin + path: signed/ts-aarch64-apple-darwin + - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + name: release-ts-x86_64-apple-darwin + path: build/ts-x86_64-apple-darwin + - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + name: signed-ts-x86_64-apple-darwin + path: signed/ts-x86_64-apple-darwin + - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + name: release-ts-x86_64-unknown-linux-gnu + path: build/ts-x86_64-unknown-linux-gnu + - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + name: signed-ts-x86_64-unknown-linux-gnu + path: signed/ts-x86_64-unknown-linux-gnu + - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + name: release-trusted-server-axum-x86_64-unknown-linux-gnu + path: build/trusted-server-axum-x86_64-unknown-linux-gnu + - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + name: signed-trusted-server-axum-x86_64-unknown-linux-gnu + path: signed/trusted-server-axum-x86_64-unknown-linux-gnu + - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + name: release-trusted-server-axum-x86_64-unknown-linux-musl + path: build/trusted-server-axum-x86_64-unknown-linux-musl + - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + name: signed-trusted-server-axum-x86_64-unknown-linux-musl + path: signed/trusted-server-axum-x86_64-unknown-linux-musl + - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + name: release-trusted-server-axum-aarch64-unknown-linux-gnu + path: build/trusted-server-axum-aarch64-unknown-linux-gnu + - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + with: + name: signed-trusted-server-axum-aarch64-unknown-linux-gnu + path: signed/trusted-server-axum-aarch64-unknown-linux-gnu + - name: Verify handoff, create once, publish by release ID and verify + env: + GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} + RELEASE_TAG: ${{ github.ref_name }} + SOURCE_COMMIT: ${{ github.sha }} + RELEASE_PUBLICATION_ENABLED: ${{ vars.RELEASE_PUBLICATION_ENABLED }} + run: | + set -euo pipefail + gh --version + node <<'NODE' + ;(async () => { + // BEGIN PUBLICATION + const fs = require('node:fs'); + const { createHash } = require('node:crypto'); + const { execFileSync } = require('node:child_process'); + const { isDeepStrictEqual } = require('node:util'); + const { GH_REPO: repo, RELEASE_TAG: tag, GH_TOKEN: token, SOURCE_COMMIT: source } = process.env; + if (!/^[\w-]+\/[\w.-]+$/.test(repo) || !/^v[\w.+-]+$/.test(tag)) throw Error('Invalid repository or tag'); + if (process.env.RELEASE_PUBLICATION_ENABLED !== 'true') throw Error('Release publication is not enabled by maintainers'); + const prerelease = tag.includes('-'), make_latest = prerelease ? 'false' : 'legacy'; + const names = ['trusted-server-axum-x86_64-unknown-linux-gnu', 'trusted-server-axum-x86_64-unknown-linux-musl', 'trusted-server-axum-aarch64-unknown-linux-gnu', 'trusted-server-fastly.wasm', 'ts-aarch64-apple-darwin', 'ts-x86_64-apple-darwin', 'ts-x86_64-unknown-linux-gnu']; + const hash = bytes => createHash('sha256').update(bytes).digest('hex'); + const regular = path => { const stat = fs.lstatSync(path); if (!stat.isFile() || stat.size === 0) throw Error(`Invalid file: ${path}`); return fs.readFileSync(path); }; + const exact = (dir, expected) => { if (JSON.stringify(fs.readdirSync(dir).sort()) !== JSON.stringify([...expected].sort())) throw Error(`Unexpected artifact contents: ${dir}`); }; + fs.mkdirSync('release-assets'); + const files = []; + for (const name of names) { + const build = `build/${name}`, signed = `signed/${name}`; + const inputs = [name, `${name}.spdx.json`], bundles = [`${name}.provenance.sigstore.json`, `${name}.sbom.sigstore.json`]; + exact(build, inputs); exact(signed, [...bundles, 'SHA256SUMS']); + const receipt = [...inputs.map(file => [build, file]), ...bundles.map(file => [signed, file])].map(([dir, file]) => { + const bytes = regular(`${dir}/${file}`); + fs.writeFileSync(`release-assets/${file}`, bytes); files.push(file); + return `${hash(bytes)} ${file}\n`; + }).join(''); + if (regular(`${signed}/SHA256SUMS`).toString() !== receipt) throw Error('Signing handoff digest mismatch'); + } + fs.writeFileSync('release-assets/SHA256SUMS', files.map(file => `${hash(regular(`release-assets/${file}`))} ${file}\n`).join('')); + files.push('SHA256SUMS'); + // Close API connections before synchronous gh checks can leave pooled sockets idle. + const api = async (path, method = 'GET', body, timeout = 10000) => { + const response = await fetch(`https://api.github.com/repos/${repo}/${path}`, {method, headers:{Connection:'close', Authorization:`Bearer ${token}`, Accept:'application/vnd.github+json', 'Content-Type':'application/json'}, ...(method === 'GET' && (path.startsWith('releases/') || path.startsWith('attestations/')) ? {signal:AbortSignal.timeout(timeout)} : {}), ...(body ? {body:JSON.stringify(body)} : {})}); // allow-domain: api.github.com + if (!response.ok || path.startsWith('attestations/') && response.status !== 200) throw Object.assign(Error(`GitHub API failed: ${method} ${path} ${response.status}`), {status:response.status, retryAfter:response.headers.get('retry-after')}); + return response.json(); + }; + // List releases, including drafts. Never resume, overwrite or delete a matching tag. + for (let page = 1; ; page++) { + if (page > 1000) throw Error('Release listing exceeded bound'); + const releases = await api(`releases?per_page=100&page=${page}`); + if (!Array.isArray(releases)) throw Error('Invalid release listing'); + if (releases.some(release => release.tag_name === tag)) throw Error('Release or draft already exists'); + if (releases.length < 100) break; + } + const body = [ + `[Verification guide](https://github.com/${repo}/blob/${tag}/docs/guide/release-verification.md)`, '', '```bash', 'set -euo pipefail', + `REPO=${repo}`, `TAG=${tag}`, `COMMIT=${source}`, `ASSETS=(${names.join(' ')})`, + 'gh release download "$TAG" --repo "$REPO" --dir .', 'test "$(gh api "repos/$REPO/releases/tags/$TAG" --jq .immutable)" = true', 'gh release verify "$TAG" --repo "$REPO"', + 'for asset in "${ASSETS[@]}"; do', + ' for suffix in "" .spdx.json .provenance.sigstore.json .sbom.sigstore.json; do gh release verify-asset "$TAG" "$asset$suffix" --repo "$REPO"; done', + ' for kind in provenance sbom; do', + ' predicate=https://slsa.dev/provenance/v1; if [[ "$kind" == sbom ]]; then predicate=https://spdx.dev/Document/v2.3; fi', + ' checks=(--repo "$REPO" --bundle "$asset.$kind.sigstore.json" --source-ref "refs/tags/$TAG" --source-digest "$COMMIT" --signer-digest "$COMMIT" --deny-self-hosted-runners --predicate-type "$predicate")', + ' gh attestation verify "$asset" "${checks[@]}" --signer-workflow "$REPO/.github/workflows/release-build.yml" --format json > "$asset.$kind.verified.json"', + ` if [[ "$kind" == sbom ]]; then jq -e --slurpfile sbom "$asset.spdx.json" '[.[].verificationResult.statement.predicate] | length == 1 and .[0] == $sbom[0]' "$asset.$kind.verified.json"; fi`, + ' gh attestation verify "$asset" "${checks[@]}" --cert-identity "https://github.com/$REPO/.github/workflows/release-build.yml@refs/tags/$TAG"', + ' done', 'done', 'gh release verify-asset "$TAG" SHA256SUMS --repo "$REPO"', 'shasum -a 256 --check SHA256SUMS', '```', + ].join('\n'); + const created = await api('releases', 'POST', {tag_name:tag, target_commitish:source, name:tag, draft:true, prerelease, make_latest, body}); + if (!Number.isSafeInteger(created.id) || created.id <= 0 || created.tag_name !== tag || created.draft !== true || created.prerelease !== prerelease) throw Error('Invalid created release identity'); + const id = created.id; + console.log(`Created release ID: ${id}`); + for (const file of files) { + const bytes = regular(`release-assets/${file}`); + const response = await fetch(`https://uploads.github.com/repos/${repo}/releases/${id}/assets?name=${encodeURIComponent(file)}`, {method:'POST', headers:{Authorization:`Bearer ${token}`, 'Content-Type':'application/octet-stream'}, body:bytes}); // allow-domain: uploads.github.com + if (!response.ok) throw Error(`Asset upload failed: ${file}`); + const asset = await response.json(); + if (asset.name !== file || asset.state !== 'uploaded' || asset.size !== bytes.length || asset.digest !== `sha256:${hash(bytes)}`) throw Error(`Upload mismatch: ${file}`); + } + const published = await api(`releases/${id}`, 'PATCH', {draft:false, prerelease, make_latest}); + // BEGIN READ RETRIES + const retryDeadline = Date.now() + 180000; + const retryRead = async (operation, deadline = retryDeadline) => { + for (let attempt = 1; ; attempt++) try { + if (Date.now() >= deadline) throw Error('Post-publish verification window exhausted'); + const result = await operation(); + if (Date.now() >= deadline) throw Error('Post-publish verification window exhausted'); + return result; + } catch (error) { + const stderr = error.stderr?.toString().trim() ?? ''; + const transient = !error.stdout?.length && (error.name === 'TimeoutError' || error.status === 429 || error.status >= 500 && error.status <= 599 || /^(ECONNRESET|ETIMEDOUT|EAI_AGAIN|UND_ERR_SOCKET|UND_ERR_CONNECT_TIMEOUT|UND_ERR_HEADERS_TIMEOUT|UND_ERR_BODY_TIMEOUT)$/.test(error.cause?.code ?? error.code) || /^HTTP (429|5\d\d): [^\r\n]+$/.test(stderr)); + if (!transient || attempt === 3) throw error; + const after = Number(error.retryAfter) * 1000 || Date.parse(error.retryAfter) - Date.now(); + const delay = Math.min(10000, Math.max([2000, 5000][attempt - 1], Number.isFinite(after) ? after : 0)); + if (Date.now() + delay >= deadline) throw Error('Post-publish retry window exhausted'); + console.warn(`Post-publish read retry: attempt ${attempt + 1}/3, transient ${error.status ?? 'transport/CLI'} failure, wait ${delay}ms`); + await new Promise(resolve => setTimeout(resolve, delay)); + } + }; + // END READ RETRIES + const verifyIdentity = async () => { + const release = await retryRead(() => api(`releases/${id}`, 'GET', undefined, Math.max(1, Math.min(10000, retryDeadline - Date.now())))); + if (published.id !== id || release.id !== id || release.tag_name !== tag || release.draft !== false || release.immutable !== true || release.prerelease !== prerelease) throw Error('Published release identity or immutability mismatch'); + if (!Array.isArray(release.assets) || release.assets.length !== files.length || new Set(release.assets.map(asset => asset.name)).size !== files.length) throw Error('Published asset set mismatch'); + for (const asset of release.assets) { + if (!files.includes(asset.name)) throw Error('Unexpected published asset'); + const bytes = regular(`release-assets/${asset.name}`); + if (asset.size !== bytes.length || asset.digest !== `sha256:${hash(bytes)}`) throw Error('Published asset digest mismatch'); + } + if ((await retryRead(() => api(`releases/tags/${encodeURIComponent(tag)}`, 'GET', undefined, Math.max(1, Math.min(10000, retryDeadline - Date.now()))))).id !== id) throw Error('Published tag identity mismatch'); + }; + await verifyIdentity(); + // The release attestation can be created after the immutable release becomes visible. + const availabilityDeadline = Math.min(retryDeadline, Date.now() + 60000); + const verifyBundle = async (record, otherTag = false) => { + if (Date.now() >= retryDeadline) throw Error('Post-publish verification window exhausted'); + const deadline = otherTag ? availabilityDeadline : retryDeadline; + const read = () => execFileSync('gh', ['release','verify','--repo',repo,'--format','json','--',record.statement.predicate.tag], {timeout:Math.max(1, Math.min(10000, deadline - Date.now())), stdio:'pipe'}); + const proof = JSON.parse(otherTag ? await retryRead(read, deadline) : read()); + if (Date.now() >= deadline) throw Error('Post-publish verification window exhausted'); + if (!isDeepStrictEqual(proof.attestation?.bundle, record.bundle)) throw Error('Verified release attestation bundle mismatch'); + }; + for (let attempt = 1; ; attempt++) { + if (Date.now() >= availabilityDeadline) throw Error('Release attestation availability window exhausted'); + // Only this endpoint maps HTTP 404 to an unavailable record. Other statuses fail. + let page, missing = false; + try { page = await api(`attestations/sha1:${source}?per_page=100&predicate_type=release`, 'GET', undefined, Math.min(4000, availabilityDeadline - Date.now())); } + catch (error) { if (error.status !== 404) throw error; page = {attestations:[]}; missing = true; } + if (!Array.isArray(page.attestations) || page.attestations.length >= 100) throw Error('Invalid or truncated release attestation response'); + const records = []; + for (const record of page.attestations) { + if (!record || typeof record.initiator !== 'string') throw Error('Malformed release attestation record'); + if (record.initiator !== 'github') continue; + const envelope = record.bundle?.dsseEnvelope; + if (envelope?.payloadType !== 'application/vnd.in-toto+json' || typeof envelope.payload !== 'string' || !envelope.payload || !Array.isArray(envelope.signatures) || !envelope.signatures.length) throw Error('Malformed release attestation bundle'); + const bytes = Buffer.from(envelope.payload, 'base64'); + if (bytes.toString('base64') !== envelope.payload) throw Error('Malformed release attestation payload'); + const statement = JSON.parse(bytes.toString()); + const predicate = statement.predicate; + if (statement._type !== 'https://in-toto.io/Statement/v1' || statement.predicateType !== 'https://in-toto.io/attestation/release/v0.2' || predicate?.repository !== repo || typeof predicate.tag !== 'string' || !predicate.tag || !/^[1-9][0-9]*$/.test(predicate.databaseId) || !Array.isArray(statement.subject)) throw Error('Malformed release attestation statement'); + const sources = statement.subject.filter(subject => subject.uri === `pkg:github/${repo}@${predicate.tag}`); + const subjects = statement.subject.filter(subject => !sources.includes(subject)); + if (sources.length !== 1 || sources[0].digest?.sha1 !== source || Object.keys(sources[0].digest).length !== 1 || !subjects.length || subjects.some(subject => typeof subject.name !== 'string' || !subject.name || !/^[a-f0-9]{64}$/.test(subject.digest?.sha256) || Object.keys(subject.digest).length !== 1) || new Set(subjects.map(subject => subject.name)).size !== subjects.length) throw Error('Invalid release attestation subjects'); + records.push({...record, statement, subjects}); + } + const current = records.filter(record => record.statement.predicate.tag === tag); + if (current.length > 1) throw Error('Multiple release attestations for current tag'); + if (current.length && (current[0].statement.predicate.databaseId !== String(id) || current[0].subjects.length !== files.length || current[0].subjects.some(subject => !files.includes(subject.name) || subject.digest.sha256 !== hash(regular(`release-assets/${subject.name}`))))) throw Error('Current release attestation identity or assets mismatch'); + for (const record of records.filter(record => record.statement.predicate.tag !== tag)) { + await verifyBundle(record, true); + console.log('Ignored verified release attestation for other tag:', JSON.stringify(record.statement.predicate.tag)); + } + if (current.length) { + await verifyBundle(current[0]); + console.log(attempt === 1 ? 'Release attestation available on first check' : `Release attestation available on check ${attempt}/5`); + console.log('Verified release:', tag, 'signature and exact attestation bundle match'); + break; + } + if (attempt === 5) throw Error('Release already published and immutable; release attestation unavailable after five checks'); + const delay = [2000, 5000, 10000, 20000][attempt - 1]; + if (Date.now() + delay >= availabilityDeadline) throw Error('Release attestation availability window exhausted'); + console.warn(`Release attestation availability: attempt ${attempt + 1}/5, ${missing ? 'HTTP 404' : 'HTTP 200, no current-tag record'}, wait ${delay}ms`); + await new Promise(resolve => setTimeout(resolve, delay)); + } + for (const file of files) console.log('Verified asset:', file, (await retryRead(() => execFileSync('gh', ['release','verify-asset',tag,`release-assets/${file}`,'--repo',repo], {timeout:Math.max(1, Math.min(10000, retryDeadline - Date.now()))}))).toString()); + // Re-download into a fresh directory on every transport retry; verify bytes outside retries. + let download; + try { + await retryRead(() => { + if (download) fs.rmSync(download, {recursive:true, force:true}); + download = fs.mkdtempSync('published-download-'); + return execFileSync('gh', ['release','download',tag,'--repo',repo,'--dir',download], {timeout:Math.max(1, Math.min(60000, retryDeadline - Date.now())), stdio:'pipe'}); + }); + exact(download, files); + if (!regular(`${download}/SHA256SUMS`).equals(regular('release-assets/SHA256SUMS'))) throw Error('Downloaded SHA256SUMS mismatch'); + for (const file of files) if (!regular(`${download}/${file}`).equals(regular(`release-assets/${file}`))) throw Error(`Downloaded asset mismatch: ${file}`); + console.log('Verified post-publish re-download: all', files.length, 'assets byte-identical to signed checksums and handoff'); + } finally { if (download) fs.rmSync(download, {recursive:true, force:true}); } + await verifyIdentity(); + // END PUBLICATION + })().catch(error => { console.error(error); process.exitCode = 1; }); + NODE diff --git a/AGENTS.md b/AGENTS.md index a88d0df59e..662915095b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -373,6 +373,7 @@ Every PR must pass: 9. Native core build-digest test and lint (`cargo test -p trusted-server-core --test template_build_digest` and `cargo clippy -p trusted-server-core --test template_build_digest -- -D warnings`) 10. Wasm clock lint: `scripts/lint-wasm-clock.sh` (crates built into the Cloudflare Worker must use `web_time`, not the `std::time::{Instant,SystemTime}` `now`/`elapsed` methods, which panic there) 11. Documentation proposal scripts: `shellcheck scripts/docs-proposal/*.sh && node --test scripts/docs-proposal/hunks.test.mjs && scripts/docs-proposal/test.sh` +12. Release producer checks: `node --test scripts/release/phase1.test.mjs && shellcheck scripts/release/phase1-build.sh` --- diff --git a/CHANGELOG.md b/CHANGELOG.md index f691c6a005..44407886b9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add Phase 1 attested Fastly wasm, three Linux/macOS CLI and three production Axum binaries, with SPDX 2.3 inventories, provenance/SBOM bundles, immutable verification, GNU glibc 2.34 and static musl startup checks. Reproducibility and the custom verifier remain later work; production publication requires maintainer settings. + ### Changed +- Attested release binaries report `x-ts-version` as the full 40-character source commit, rather than the release tag. CLI output labels package/source/target/variant, and Axum supports runtime `EDGEZERO__ADAPTER__HOST` and primary `EDGEZERO__ADAPTER__PORT` (legacy `PORT` must agree when both are set) binding with a loopback default. - **Breaking:** `x-ts-version` now reports the deployed git version — the release tag, else the branch, else the first 6 characters of the commit — compiled in from the build-time `TRUSTED_SERVER__GIT_VERSION` (set by the deploy pipeline) or local git, and is sent by every adapter, including on the Fastly `GET /health` probe. The Fastly service version it previously carried is now `x-ts-fastly-version`; update dashboards, monitors, and scripts that read `x-ts-version` as the Fastly version number. Builds with neither the override nor git omit the header. - **Breaking:** `ts prebid bundle` is now `ts prebid client`, alongside the new `ts prebid server` namespace. Update scripts and runbooks to use `ts prebid client` with the same arguments. The old `bundle` spelling is no longer accepted and has no compatibility alias. - The S2S `/_ts/api/v1/batch-sync` endpoint now validates the full batch and calls the CAS-protected update path once per distinct normalized EC ID. The last valid UID wins within a group, and infrastructure failures reject the failing and each unprocessed group, so accepted and `kv_unavailable` input indexes may interleave. diff --git a/crates/trusted-server-adapter-axum/src/main.rs b/crates/trusted-server-adapter-axum/src/main.rs index 66d592e0b7..0c0e84a176 100644 --- a/crates/trusted-server-adapter-axum/src/main.rs +++ b/crates/trusted-server-adapter-axum/src/main.rs @@ -1,4 +1,5 @@ use edgezero_adapter_axum::dev_server::{AxumDevServer, AxumDevServerConfig}; +use edgezero_core::addr::resolve_bind_addr; use edgezero_core::app::Hooks as _; use log::LevelFilter; use trusted_server_adapter_axum::app::TrustedServerApp; @@ -26,15 +27,18 @@ fn main() { eprintln!("warning: logger init failed: {e}"); } - let config = match port_from_env() { - // When PORT is set, bind to a specific address so integration tests - // can allocate a fresh OS port each run and avoid TIME_WAIT flakiness. - Some(port) => AxumDevServerConfig { - addr: std::net::SocketAddr::from(([127, 0, 0, 1], port)), - enable_ctrl_c: true, - }, - // Normal development path: read bind address from axum.toml. - None => AxumDevServerConfig::default(), + // The default is loopback; runtime overrides are passed directly to EdgeZero. + let host = std::env::var_os("EDGEZERO__ADAPTER__HOST"); + let port = std::env::var_os("PORT"); + let adapter_port = std::env::var_os("EDGEZERO__ADAPTER__PORT"); + let addr = parse_bind_address(host.as_deref(), port.as_deref(), adapter_port.as_deref()) + .unwrap_or_else(|error| { + log::error!("Invalid runtime bind address: {error}"); + std::process::exit(1); + }); + let config = AxumDevServerConfig { + addr, + enable_ctrl_c: true, }; log::info!("Listening on http://{}", config.addr); @@ -65,26 +69,59 @@ fn parse_log_level(configured: Option<&str>) -> Result { } } -/// Read a port number from the `PORT` environment variable. +/// Resolve runtime binding using `EdgeZero` without embedding deployment configuration. /// -/// Returns `None` when the variable is unset. Exits non-zero if the value -/// is set but cannot be parsed — silently falling back to a different port -/// would surprise tooling that expects the server at the requested address. -#[allow(clippy::print_stderr)] -fn port_from_env() -> Option { - let raw = std::env::var("PORT").ok()?; - match raw.parse() { - Ok(port) => Some(port), - Err(e) => { - eprintln!("error: PORT env var '{raw}' is not a valid u16: {e}"); - std::process::exit(1); +/// `EDGEZERO__ADAPTER__PORT` is primary; legacy `PORT` must agree when both +/// are set. Ports must be 1–65535; the default is loopback port 8787. +/// +/// # Errors +/// +/// Rejects non-Unicode values, conflicting ports and every `EdgeZero` resolver +/// warning, including malformed addresses and port zero, rather than falling back. +fn parse_bind_address( + host: Option<&std::ffi::OsStr>, + port: Option<&std::ffi::OsStr>, + adapter_port: Option<&std::ffi::OsStr>, +) -> Result { + let [host, port, adapter_port] = [host, port, adapter_port].map(|value| { + value + .map(|value| { + value + .to_str() + .ok_or_else(|| "Runtime bind values must be Unicode".to_owned()) + }) + .transpose() + }); + let host = host?; + let port = port?; + let adapter_port = adapter_port?; + let primary = resolve_bind_addr(host, adapter_port.or(port), None, None); + if !primary.warnings.is_empty() { + return Err(format!( + "Runtime bind address must use a valid IP and port 1–65535: {}", + primary.warnings.join("; ") + )); + } + if let Some(port) = port { + let legacy = resolve_bind_addr(None, Some(port), None, None); + if !legacy.warnings.is_empty() { + return Err(format!( + "PORT must be 1–65535: {}", + legacy.warnings.join("; ") + )); + } + if legacy.addr.port() != primary.addr.port() { + return Err("PORT and EDGEZERO__ADAPTER__PORT must agree when both are set".to_owned()); } } + Ok(primary.addr) } #[cfg(test)] mod tests { use super::*; + #[cfg(unix)] + use std::os::unix::ffi::OsStrExt as _; #[test] fn crate_compiles() {} @@ -145,4 +182,124 @@ mod tests { "should reject an unknown level name and return it trimmed" ); } + + #[test] + fn adapter_port_alias_is_validated_alongside_port() { + let port = std::ffi::OsStr::new("8080"); + for (legacy, adapter) in [ + (None, Some(port)), + (Some(port), None), + (Some(port), Some(port)), + ] { + assert_eq!( + parse_bind_address(None, legacy, adapter) + .expect("should accept matching runtime ports") + .port(), + 8080, + "should resolve either runtime port name" + ); + } + assert!( + parse_bind_address(None, Some(port), Some(std::ffi::OsStr::new("8081"))).is_err(), + "should reject conflicting port names" + ); + for invalid in ["0", "", " ", " 80", "invalid", "65536"] { + let invalid = std::ffi::OsStr::new(invalid); + assert!( + parse_bind_address(None, Some(port), Some(invalid)).is_err(), + "should reject invalid adapter port even with valid PORT" + ); + assert!( + parse_bind_address(None, Some(invalid), Some(port)).is_err(), + "should reject invalid PORT even with valid adapter port" + ); + } + #[cfg(unix)] + assert!( + parse_bind_address(None, Some(port), Some(std::ffi::OsStr::from_bytes(b"\xff"))) + .is_err(), + "should reject non-Unicode adapter port" + ); + } + + #[test] + fn bind_address_rejects_zero_and_accepts_port_boundaries() { + let zero = std::ffi::OsStr::new("0"); + assert!( + parse_bind_address(None, Some(zero), None).is_err(), + "should reject legacy port zero" + ); + assert!( + parse_bind_address(None, None, Some(zero)).is_err(), + "should reject primary port zero" + ); + for value in ["1", "65535"] { + let port = std::ffi::OsStr::new(value); + assert!( + parse_bind_address(None, Some(port), Some(port)).is_ok(), + "should accept port bounds" + ); + } + } + + #[test] + fn bind_address_defaults_and_runtime_overrides() { + assert_eq!( + parse_bind_address(None, None, None).expect("should default"), + "127.0.0.1:8787".parse().expect("should parse fixture"), + "should retain loopback default" + ); + assert_eq!( + parse_bind_address( + Some(std::ffi::OsStr::new("0.0.0.0")), + Some(std::ffi::OsStr::new("8080")), + None + ) + .expect("should override") + .to_string(), + "0.0.0.0:8080", + "should bind all interfaces at runtime" + ); + assert_eq!( + parse_bind_address(Some(std::ffi::OsStr::new("::1")), None, None) + .expect("should accept IPv6") + .to_string(), + "[::1]:8787", + "should accept IPv6" + ); + assert_eq!( + parse_bind_address(None, Some(std::ffi::OsStr::new("9000")), None) + .expect("should preserve PORT") + .port(), + 9000, + "should honor existing port setting" + ); + } + + #[test] + fn bind_address_rejects_invalid_explicit_values() { + for host in ["", "localhost", "0.0.0.0:8080", " 0.0.0.0"] { + assert!( + parse_bind_address(Some(std::ffi::OsStr::new(host)), None, None).is_err(), + "should reject invalid host" + ); + } + for port in ["", "invalid", "65536", " 80"] { + assert!( + parse_bind_address(None, Some(std::ffi::OsStr::new(port)), None).is_err(), + "should reject invalid port" + ); + } + #[cfg(unix)] + { + assert!( + parse_bind_address(Some(std::ffi::OsStr::from_bytes(b"\xff")), None, None).is_err(), + "should reject non-Unicode host" + ); + assert!( + parse_bind_address(None, Some(std::ffi::OsStr::from_bytes(b"\xff")), None).is_err(), + "should reject non-Unicode port" + ); + } + } } diff --git a/crates/trusted-server-adapter-fastly/build.rs b/crates/trusted-server-adapter-fastly/build.rs new file mode 100644 index 0000000000..0889fe217b --- /dev/null +++ b/crates/trusted-server-adapter-fastly/build.rs @@ -0,0 +1,26 @@ +//! Compile immutable facts owned by the Fastly product. + +#![allow( + clippy::print_stdout, + reason = "Cargo build script directives use stdout" +)] + +#[allow( + dead_code, + reason = "This owner uses only release validation from the shared resolver" +)] +#[path = "../trusted-server-core/build_support/git_version.rs"] +mod git_version; + +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=../trusted-server-core/build_support/git_version.rs"); + println!("cargo:rerun-if-env-changed=TRUSTED_SERVER__RELEASE_BUILD"); + println!("cargo:rerun-if-env-changed=TRUSTED_SERVER__GIT_VERSION"); + let control = std::env::var_os("TRUSTED_SERVER__RELEASE_BUILD"); + let source = std::env::var_os("TRUSTED_SERVER__GIT_VERSION"); + git_version::release_source(control.as_deref(), source.as_deref()) + .expect("should provide valid release build inputs"); + let target = std::env::var("TARGET").expect("should set the owning Cargo target"); + println!("cargo:rustc-env=TRUSTED_SERVER__BUILD_TARGET={target}"); +} diff --git a/crates/trusted-server-adapter-fastly/src/main.rs b/crates/trusted-server-adapter-fastly/src/main.rs index b8f814a01c..cfb4fe7fd9 100644 --- a/crates/trusted-server-adapter-fastly/src/main.rs +++ b/crates/trusted-server-adapter-fastly/src/main.rs @@ -11,6 +11,7 @@ use error_stack::Report; use fastly::http::Method as FastlyMethod; use fastly::{Request as FastlyRequest, Response as FastlyResponse}; +use trusted_server_core::build_info::{BuildInfo, BuildVariant}; use trusted_server_core::cache_policy::{EdgeCacheHeader, cache_control_headers_have_directive}; use trusted_server_core::constants::HEADER_X_TS_VERSION; use trusted_server_core::ec::device::DeviceSignals; @@ -56,6 +57,17 @@ use crate::sandbox::{RetainedApp, Sandbox, SandboxCounters, ServeMode, StartupDi #[cfg(feature = "reusable-sandbox")] use crate::sandbox::RetirementCounters; +/// Immutable facts compiled into the owning Fastly product. +pub const BUILD_INFO: BuildInfo = BuildInfo::new( + env!("CARGO_PKG_VERSION"), + env!("TRUSTED_SERVER__BUILD_TARGET"), + if cfg!(feature = "reusable-sandbox") { + BuildVariant::ReusableSandbox + } else { + BuildVariant::Default + }, +); + /// Opens the Fastly Config Store used by the `EdgeZero` dispatcher. /// /// # Errors @@ -97,10 +109,7 @@ fn main() { // Held outside the sandbox: `serve_custom` owns the `Sandbox` and exposes // no slot for application state that is not the retained payload. - let mut startup = StartupDiagnostics::default(); - for message in diagnostics { - startup.push(message); - } + let mut startup = startup_diagnostics(mode, diagnostics); match mode { ServeMode::Single => { @@ -116,6 +125,23 @@ fn main() { } } +fn startup_diagnostics(mode: ServeMode, diagnostics: Vec) -> StartupDiagnostics { + let mut startup = StartupDiagnostics::default(); + if matches!(mode, ServeMode::Reuse(_)) { + startup.push(format!( + "Build facts: package={} source={} target={} variant={}", + BUILD_INFO.package_version(), + BUILD_INFO.source().unwrap_or("unknown"), + BUILD_INFO.target(), + BUILD_INFO.variant() + )); + } + for message in diagnostics { + startup.push(message); + } + startup +} + /// Serves requests from one sandbox under explicit bounds. /// /// Only compiled with the `reusable-sandbox` feature; [`serve_mode`] can never @@ -912,6 +938,70 @@ mod tests { ); } + #[test] + fn fastly_build_info_uses_actual_owning_package_target_source_and_feature() { + assert_eq!( + BUILD_INFO.package_version(), + env!("CARGO_PKG_VERSION"), + "should own Fastly package version" + ); + assert_eq!( + BUILD_INFO.target(), + "wasm32-wasip1", + "should own Fastly Cargo target" + ); + assert_eq!( + BUILD_INFO.source(), + trusted_server_core::constants::TS_GIT_VERSION, + "should read source from core" + ); + let expected = if cfg!(feature = "reusable-sandbox") { + BuildVariant::ReusableSandbox + } else { + BuildVariant::Default + }; + assert_eq!( + BUILD_INFO.variant(), + expected, + "should derive actual Fastly feature variant" + ); + } + + #[test] + fn single_mode_does_not_queue_build_facts() { + let startup = startup_diagnostics(ServeMode::Single, Vec::new()); + assert_eq!( + startup.pending(), + 0, + "should omit build facts in single mode" + ); + let startup = startup_diagnostics(ServeMode::Single, vec!["example diagnostic".to_owned()]); + assert_eq!( + startup.pending(), + 1, + "should preserve existing startup diagnostics" + ); + } + + #[test] + fn reuse_mode_retains_one_build_fact_message() { + let limits = crate::sandbox::SandboxLimits { + max_requests: 2, + max_memory_mib: 128, + max_lifetime: std::time::Duration::from_secs(60), + timeout: std::time::Duration::from_secs(1), + }; + let startup = startup_diagnostics( + ServeMode::Reuse(limits), + vec!["example diagnostic".to_owned()], + ); + assert_eq!( + startup.pending(), + 2, + "should retain build facts and supplied diagnostics in reuse mode" + ); + } + #[test] fn health_response_reports_git_version_only() { let req = FastlyRequest::get("https://example.com/health"); diff --git a/crates/trusted-server-cli/build.rs b/crates/trusted-server-cli/build.rs new file mode 100644 index 0000000000..1c40ab678b --- /dev/null +++ b/crates/trusted-server-cli/build.rs @@ -0,0 +1,31 @@ +//! Compile immutable facts owned by the CLI product. + +#![allow( + clippy::print_stdout, + reason = "Cargo build script directives use stdout" +)] + +#[allow( + dead_code, + reason = "This owner uses only release validation from the shared resolver" +)] +#[path = "../trusted-server-core/build_support/git_version.rs"] +mod git_version; + +fn main() { + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=../trusted-server-core/build_support/git_version.rs"); + println!("cargo:rerun-if-env-changed=TRUSTED_SERVER__RELEASE_BUILD"); + println!("cargo:rerun-if-env-changed=TRUSTED_SERVER__GIT_VERSION"); + println!("cargo:rustc-check-cfg=cfg(trusted_server_release_build)"); + let control = std::env::var_os("TRUSTED_SERVER__RELEASE_BUILD"); + let source = std::env::var_os("TRUSTED_SERVER__GIT_VERSION"); + if git_version::release_source(control.as_deref(), source.as_deref()) + .expect("should provide valid release build inputs") + .is_some() + { + println!("cargo:rustc-cfg=trusted_server_release_build"); + } + let target = std::env::var("TARGET").expect("should set the owning Cargo target"); + println!("cargo:rustc-env=TRUSTED_SERVER__BUILD_TARGET={target}"); +} diff --git a/crates/trusted-server-cli/release-policy.json b/crates/trusted-server-cli/release-policy.json new file mode 100644 index 0000000000..4aa1115dfa --- /dev/null +++ b/crates/trusted-server-cli/release-policy.json @@ -0,0 +1,140 @@ +{ + "artifacts": [ + { + "actions": ["readonly-verify"], + "assurance_profile": "provenance-sbom-repeatable", + "bin": "trusted-server-adapter-fastly", + "capabilities": ["fastly-production"], + "compatibility": { + "architecture": "wasm32", + "minimum_version": "wasip1", + "runtime": "fastly-compute" + }, + "default_features": true, + "features": [], + "kind": "wasm-core", + "name": "trusted-server-fastly.wasm", + "package": "trusted-server-adapter-fastly", + "role": "server", + "target": "wasm32-wasip1", + "variant": "default" + }, + { + "actions": ["readonly-verify"], + "assurance_profile": "provenance-sbom", + "bin": "ts", + "capabilities": ["cli-execute"], + "compatibility": { + "architecture": "aarch64", + "minimum_version": "13.0", + "runtime": "macos" + }, + "default_features": true, + "features": [], + "kind": "native-executable", + "name": "ts-aarch64-apple-darwin", + "package": "trusted-server-cli", + "role": "operator-cli", + "target": "aarch64-apple-darwin", + "variant": "default" + }, + { + "actions": ["readonly-verify"], + "assurance_profile": "provenance-sbom", + "bin": "ts", + "capabilities": ["cli-execute"], + "compatibility": { + "architecture": "x86_64", + "minimum_version": "13.0", + "runtime": "macos" + }, + "default_features": true, + "features": [], + "kind": "native-executable", + "name": "ts-x86_64-apple-darwin", + "package": "trusted-server-cli", + "role": "operator-cli", + "target": "x86_64-apple-darwin", + "variant": "default" + }, + { + "actions": ["readonly-verify"], + "assurance_profile": "provenance-sbom", + "bin": "ts", + "capabilities": ["cli-execute"], + "compatibility": { + "architecture": "x86_64", + "minimum_version": "glibc-2.34", + "runtime": "linux-gnu" + }, + "default_features": true, + "features": [], + "kind": "native-executable", + "name": "ts-x86_64-unknown-linux-gnu", + "package": "trusted-server-cli", + "role": "operator-cli", + "target": "x86_64-unknown-linux-gnu", + "variant": "default" + } + ], + "bounds": { + "attempt_seconds": 900, + "bundle_bytes": 33554432, + "child_seconds": 120, + "cohort_executables": 4, + "companion_bytes": 33554432, + "executable_bytes": 536870912, + "index_bytes": 1048576, + "inventory_bytes": 1048576, + "json_depth": 64, + "matching_attestations_per_bundle": 1, + "metadata_bytes": 1048576, + "policy_bytes": 1048576, + "spdx_bytes": 16777216, + "stderr_bytes": 1048576, + "stdout_bytes": 134217728, + "trusted_roots_bytes": 1048576 + }, + "cohort": "phase1-fastly-cli", + "github_host": "github.com", + "historical_producer_policies": [], + "identity": { + "builder_id": "https://github.com/IABTechLab/trusted-server/.github/workflows/release-build.yml@refs/tags/", + "caller_commit": "same-approved-source-commit", + "caller_path": ".github/workflows/release.yml", + "caller_repository": "https://github.com/IABTechLab/trusted-server", + "certificate_fields": [ + "buildConfigDigest", + "buildConfigURI", + "buildTrigger", + "runInvocationURI", + "sourceRepositoryIdentifier", + "sourceRepositoryOwnerIdentifier" + ], + "certificate_san": "https://github.com/IABTechLab/trusted-server/.github/workflows/release-build.yml@refs/tags/", + "event": "push", + "external_parameters": "exact-caller-workflow-only-no-inputs", + "invocation": "same-repository-run-and-attempt-across-certificate-and-provenance", + "issuer": "https://token.actions.githubusercontent.com", + "runner": "github-hosted", + "signer_commit": "same-approved-source-commit", + "source_commit": "independently-approved-40-lowercase-hex", + "source_dependency": "git+https://github.com/IABTechLab/trusted-server@refs/tags/", + "source_ref": "refs/tags/" + }, + "policy_version": 1, + "predicates": { + "build_type": "https://actions.github.io/buildtypes/workflow/v1", + "provenance": "https://slsa.dev/provenance/v1", + "sbom": "https://spdx.dev/Document/v2.3" + }, + "public_actions": ["readonly-verify"], + "repository": "IABTechLab/trusted-server", + "repository_id": "950860906", + "repository_owner_id": "82903538", + "tool_versions": { + "actions_attest": "4.2.2", + "actions_attest_revision": "1e69f48acb82d1966a394da916b4c1698aa569d6", + "gh": "2.97.0" + } +} diff --git a/crates/trusted-server-cli/src/commands/dev/lint/domains.rs b/crates/trusted-server-cli/src/commands/dev/lint/domains.rs index 8978af61c0..5b6df96154 100644 --- a/crates/trusted-server-cli/src/commands/dev/lint/domains.rs +++ b/crates/trusted-server-cli/src/commands/dev/lint/domains.rs @@ -94,6 +94,9 @@ pub const REFERENCE_HOSTS: &[&str] = &[ "docs.github.com", "help.github.com", "token.actions.githubusercontent.com", + // GitHub Actions SLSA buildType URI used in the carried release policy: + // https://actions.github.io/buildtypes/workflow/v1 + "actions.github.io", // Git commit conventions "chris.beams.io", // Rust @@ -111,9 +114,15 @@ pub const REFERENCE_HOSTS: &[&str] = &[ "iabtechlab.com", "iabtechlab.github.io", "iabeurope.github.io", + // GitHub CLI: canonical verification command documentation. + "cli.github.com", // Specs (supply chain) "in-toto.io", "rslstandard.org", + // SLSA: canonical provenance predicate and build specification references. + "slsa.dev", + // SPDX: canonical document predicate in the retained reviewed contract. + "spdx.dev", // Specs (other) "webassembly.org", // Fastly docs @@ -311,10 +320,51 @@ mod allow_check_tests { fn reference_hosts_allowed_everywhere() { assert!(is_allowed("github.com", ¬hing_suppressed())); assert!(is_allowed("docs.rs", ¬hing_suppressed())); + assert!( + is_allowed("slsa.dev", ¬hing_suppressed()), + "should allow the canonical slsa.dev reference" + ); + assert!( + is_allowed("spdx.dev", ¬hing_suppressed()), + "should allow the canonical spdx.dev reference" + ); + assert!( + !is_allowed("cyclonedx.org", ¬hing_suppressed()), + "should require per-line suppression for the inventory format reference" + ); + assert!( + is_allowed("cli.github.com", ¬hing_suppressed()), + "should allow the canonical cli.github.com reference" + ); + assert!( + !is_allowed("unapproved.slsa.dev", ¬hing_suppressed()), + "should reject unapproved slsa.dev subdomains" + ); + assert!( + !is_allowed("unapproved.cyclonedx.org", ¬hing_suppressed()), + "should reject unapproved cyclonedx.org subdomains" + ); // But NOT subdomains of REFERENCE_HOSTS (exact-match). assert!(!is_allowed("other.github.com", ¬hing_suppressed())); } + #[test] + fn actions_build_type_host_is_exact() { + assert!( + is_allowed("actions.github.io", ¬hing_suppressed()), + "should allow the GitHub Actions build type reference" + ); + for host in [ + "unapproved.actions.github.io", + "actions.github.io.invalid-host.com", + ] { + assert!( + !is_allowed(host, ¬hing_suppressed()), + "should reject Actions host subdomains and lookalikes" + ); + } + } + #[test] fn suppression_set_allows() { let mut suppressed = HashSet::new(); diff --git a/crates/trusted-server-cli/src/lib.rs b/crates/trusted-server-cli/src/lib.rs index 24e001d853..080bdb0db5 100644 --- a/crates/trusted-server-cli/src/lib.rs +++ b/crates/trusted-server-cli/src/lib.rs @@ -14,7 +14,7 @@ mod tls; mod url_guard; #[cfg(not(target_arch = "wasm32"))] -pub use run::{RunOutcome, run_from_env}; +pub use run::{BUILD_INFO, RunOutcome, run_from_env}; // Every `ts` subcommand's implementation lives under `commands/`. The // `ts dev` group is available on every host target; `ts dev lint` and diff --git a/crates/trusted-server-cli/src/run.rs b/crates/trusted-server-cli/src/run.rs index 05b4779f8f..f573c491ee 100644 --- a/crates/trusted-server-cli/src/run.rs +++ b/crates/trusted-server-cli/src/run.rs @@ -1,10 +1,12 @@ use std::process; +use std::sync::LazyLock; use clap::{Parser, Subcommand}; use edgezero_cli::args::{ ActiveVersionArgs, AuthArgs, BuildArgs, ConfigDiffArgs, ConfigGcArgs, ConfigPushArgs, ConfigValidateArgs, DeployArgs, HealthcheckArgs, ProvisionArgs, RollbackArgs, ServeArgs, }; +use trusted_server_core::build_info::{BuildInfo, BuildVariant}; use trusted_server_core::config::TrustedServerAppConfig; use crate::commands::audit::{AuditArgs, run_audit}; @@ -13,8 +15,25 @@ use crate::commands::config::init::{ConfigInitArgs, run_config_init}; use crate::commands::pbs::{self, PbsArgs}; use crate::prebid_bundle::{NpmPrebidBundleGenerator, PrebidBundleArgs, run_bundle}; +/// Immutable facts compiled into the owning CLI product. +pub const BUILD_INFO: BuildInfo = BuildInfo::new( + env!("CARGO_PKG_VERSION"), + env!("TRUSTED_SERVER__BUILD_TARGET"), + BuildVariant::Default, +); + +static VERSION_TEXT: LazyLock = LazyLock::new(|| { + format!( + "package={} source={} target={} variant={}", + BUILD_INFO.package_version(), + BUILD_INFO.source().unwrap_or("unknown"), + BUILD_INFO.target(), + BUILD_INFO.variant() + ) +}); + #[derive(Debug, Parser)] -#[command(name = "ts", version, about = "Trusted Server CLI")] +#[command(name = "ts", version = VERSION_TEXT.as_str(), about = "Trusted Server CLI")] struct Args { #[command(subcommand)] command: Command, diff --git a/crates/trusted-server-cli/tests/release_build_facts.rs b/crates/trusted-server-cli/tests/release_build_facts.rs new file mode 100644 index 0000000000..f5995e3bdd --- /dev/null +++ b/crates/trusted-server-cli/tests/release_build_facts.rs @@ -0,0 +1,261 @@ +//! Host checks of actual owning build scripts and the compiled CLI binary. + +#![cfg(not(target_arch = "wasm32"))] + +use std::ffi::OsStr; +use std::fs; +#[cfg(unix)] +use std::os::unix::ffi::OsStrExt as _; +use std::path::{Path, PathBuf}; +use std::process::Command; + +use tempfile::TempDir; +use trusted_server_cli::BUILD_INFO; +use trusted_server_core::build_info::BuildVariant; + +const SOURCE: &str = "abcdef0123456789abcdef0123456789abcdef01"; + +fn workspace() -> &'static Path { + Path::new(env!("CARGO_MANIFEST_DIR")) + .parent() + .expect("should resolve crates") + .parent() + .expect("should resolve workspace") +} + +fn compile_build_script(owner: &str, directory: &TempDir) -> PathBuf { + let output = directory.path().join(format!("{owner}-build")); + let compiled = Command::new("rustc") + .args(["--edition=2024"]) + .arg(workspace().join("crates").join(owner).join("build.rs")) + .arg("-o") + .arg(&output) + .output() + .expect("should run rustc"); + assert!( + compiled.status.success(), + "should compile actual owner script: {}", + String::from_utf8_lossy(&compiled.stderr) + ); + output +} + +fn owner_command(script: &Path) -> Command { + let mut command = Command::new(script); + command + .env("TARGET", "example-target") + .env_remove("TRUSTED_SERVER__RELEASE_BUILD") + .env_remove("TRUSTED_SERVER__GIT_VERSION") + .env_remove("CARGO_FEATURE_REUSABLE_SANDBOX"); + command +} + +#[cfg(unix)] +#[test] +fn actual_owner_scripts_reject_invalid_release_controls_and_sources() { + let directory = tempfile::tempdir().expect("should create fixture"); + for owner in ["trusted-server-cli", "trusted-server-adapter-fastly"] { + let script = compile_build_script(owner, &directory); + for control in [ + OsStr::new(""), + OsStr::new("0"), + OsStr::new(" 1"), + OsStr::new("1\n"), + OsStr::from_bytes(b"\xff"), + ] { + let output = owner_command(&script) + .env("TRUSTED_SERVER__RELEASE_BUILD", control) + .env("TRUSTED_SERVER__GIT_VERSION", SOURCE) + .output() + .expect("should run actual owner script"); + assert!( + !output.status.success(), + "should reject control for {owner}: {control:?}" + ); + assert!( + String::from_utf8_lossy(&output.stderr) + .contains("TRUSTED_SERVER__RELEASE_BUILD must be exactly 1"), + "should identify malformed release control" + ); + } + for source in [ + None, + Some(OsStr::new("main")), + Some(OsStr::new("v1.2.3")), + Some(OsStr::new("abcdef")), + Some(OsStr::new("ABCDEF0123456789abcdef0123456789abcdef01")), + Some(OsStr::new(" abcdef0123456789abcdef0123456789abcdef01")), + Some(OsStr::new("abcdef0123456789abcdef0123456789abcdef01\n")), + Some(OsStr::from_bytes(b"\xff")), + ] { + let mut command = owner_command(&script); + command.env("TRUSTED_SERVER__RELEASE_BUILD", "1"); + if let Some(source) = source { + command.env("TRUSTED_SERVER__GIT_VERSION", source); + } + let output = command.output().expect("should run actual owner script"); + assert!( + !output.status.success(), + "should reject source for {owner}: {source:?}" + ); + let diagnostic = if source.and_then(OsStr::to_str).is_none() { + "release builds require TRUSTED_SERVER__GIT_VERSION" + } else { + "release source must be exactly 40 lowercase ASCII hexadecimal bytes" + }; + assert!( + String::from_utf8_lossy(&output.stderr).contains(diagnostic), + "should identify malformed release source: {diagnostic}" + ); + } + } +} + +#[test] +fn actual_owner_scripts_emit_only_own_target_and_cli_release_cfg() { + let directory = tempfile::tempdir().expect("should create fixture"); + for owner in ["trusted-server-cli", "trusted-server-adapter-fastly"] { + let script = compile_build_script(owner, &directory); + let mut outputs = Vec::new(); + for tag in ["v1.2.3", "v1.2.4"] { + let output = owner_command(&script) + .env("TRUSTED_SERVER__RELEASE_BUILD", "1") + .env("TRUSTED_SERVER__GIT_VERSION", SOURCE) + .env("GITHUB_REF_NAME", tag) + .env("GITHUB_RUN_ID", tag) + .output() + .expect("should run release owner script"); + assert!( + output.status.success(), + "should accept valid release source" + ); + let directives = + String::from_utf8(output.stdout).expect("should emit UTF-8 directives"); + assert!( + directives.contains("cargo:rustc-env=TRUSTED_SERVER__BUILD_TARGET=example-target"), + "should emit the target owned by Cargo TARGET" + ); + assert!( + !directives.contains("cargo:rustc-env=TRUSTED_SERVER__GIT_VERSION="), + "should consume the source fact owned by core" + ); + assert!( + !directives.contains("BUILD_VARIANT"), + "should derive variant from owning feature cfg" + ); + if owner == "trusted-server-cli" { + assert!( + directives.contains("cargo:rustc-cfg=trusted_server_release_build"), + "should identify release mode for immutable-fact tests" + ); + } + outputs.push(directives); + } + assert_eq!( + outputs[0], outputs[1], + "should exclude external tags and run IDs from compiled facts" + ); + let output = owner_command(&script) + .output() + .expect("should run development script"); + assert!( + output.status.success(), + "should preserve development builds" + ); + assert!( + !String::from_utf8_lossy(&output.stdout) + .contains("cargo:rustc-cfg=trusted_server_release_build"), + "should retain development build mode" + ); + } +} + +#[test] +fn actual_cli_version_reads_all_owning_facts_and_ignores_external_tags() { + assert_eq!( + BUILD_INFO.package_version(), + env!("CARGO_PKG_VERSION"), + "should own package version" + ); + assert_eq!( + BUILD_INFO.source(), + trusted_server_core::constants::TS_GIT_VERSION, + "should consume the source owned by core" + ); + assert_eq!( + BUILD_INFO.target(), + env!("TRUSTED_SERVER__BUILD_TARGET"), + "should own Cargo target" + ); + assert_eq!( + BUILD_INFO.variant(), + BuildVariant::Default, + "should use CLI default variant" + ); + if cfg!(trusted_server_release_build) { + let source = BUILD_INFO + .source() + .expect("should compile release source into the actual CLI"); + assert_eq!(source.len(), 40, "should preserve complete release source"); + assert!( + source + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)), + "should use lowercase source SHA in release facts" + ); + } + let directory = tempfile::tempdir().expect("should create outside-checkout fixture"); + for tag in ["v1.2.3", "v1.2.4"] { + let output = Command::new(env!("CARGO_BIN_EXE_ts")) + .current_dir(directory.path()) + .arg("--version") + .env("GITHUB_REF_NAME", tag) + .env("GITHUB_RUN_ID", tag) + .output() + .expect("should run actual CLI binary"); + assert!( + output.status.success(), + "should print version outside checkout" + ); + assert_eq!( + String::from_utf8_lossy(&output.stdout), + format!( + "ts package={} source={} target={} variant=default\n", + BUILD_INFO.package_version(), + BUILD_INFO.source().unwrap_or("unknown"), + BUILD_INFO.target() + ), + "should label all four actual immutable facts" + ); + } +} + +#[test] +fn help_and_config_init_work_outside_the_checkout() { + let directory = tempfile::tempdir().expect("should create outside-checkout fixture"); + let binary = env!("CARGO_BIN_EXE_ts"); + let output = Command::new(binary) + .current_dir(directory.path()) + .arg("--help") + .output() + .expect("should run portable CLI"); + assert!( + output.status.success(), + "should print help outside checkout" + ); + let output = Command::new(binary) + .current_dir(directory.path()) + .args(["config", "init"]) + .output() + .expect("should initialize scaffold outside checkout"); + assert!( + output.status.success(), + "should initialize embedded example config" + ); + assert_eq!( + fs::read_to_string(directory.path().join("trusted-server.toml")) + .expect("should read config scaffold"), + include_str!("../../../trusted-server.example.toml"), + "should use tracked fictional scaffold" + ); +} diff --git a/crates/trusted-server-core/build.rs b/crates/trusted-server-core/build.rs index db9c31d27c..552e5bb116 100644 --- a/crates/trusted-server-core/build.rs +++ b/crates/trusted-server-core/build.rs @@ -17,7 +17,7 @@ use std::path::{Path, PathBuf}; use std::process::Command; use edgezero_core::manifest::ManifestLoader; -use git_version::{Candidates, is_usable, resolve_git_version}; +use git_version::{Candidates, is_usable, resolve_build_source, resolve_git_version}; use sha2::{Digest as _, Sha256}; /// Crate-relative build inputs that are both watched and hashed besides `src/`. @@ -160,24 +160,29 @@ fn main() { fn emit_git_version() { println!("cargo:rerun-if-env-changed={OVERRIDE_ENV}"); - let override_value = std::env::var(OVERRIDE_ENV).ok(); - let resolved = match override_value.as_deref() { - Some(value) if is_usable(value) => resolve_git_version(&Candidates { - override_value: Some(value), - exact_tag: None, - branch: None, - commit: None, - }), - Some(value) => { - if !value.trim().is_empty() { - println!( - "cargo:warning={OVERRIDE_ENV}={value:?} is not visible ASCII; \ - falling back to local git for x-ts-version" - ); - } - resolve_from_local_git() + println!("cargo:rerun-if-env-changed=TRUSTED_SERVER__RELEASE_BUILD"); + let control = env::var_os("TRUSTED_SERVER__RELEASE_BUILD"); + let source = env::var_os(OVERRIDE_ENV); + if control.is_none() + && let Some(value) = source.as_deref().and_then(std::ffi::OsStr::to_str) + && !is_usable(value) + && !value.trim().is_empty() + { + println!( + "cargo:warning={OVERRIDE_ENV}={value:?} is not visible ASCII; \ + falling back to local git for x-ts-version" + ); + } + let resolved = match resolve_build_source( + control.as_deref(), + source.as_deref(), + resolve_from_local_git, + ) { + Ok(source) => source, + Err(message) => { + println!("cargo::error={message}"); + std::process::exit(1); } - None => resolve_from_local_git(), }; // Always emitted, even when empty, so the raw pipeline value never reaches diff --git a/crates/trusted-server-core/build_support/git_version.rs b/crates/trusted-server-core/build_support/git_version.rs index f4cf2d3688..36f426372a 100644 --- a/crates/trusted-server-core/build_support/git_version.rs +++ b/crates/trusted-server-core/build_support/git_version.rs @@ -3,6 +3,62 @@ //! Shared by `build.rs` and `tests/git_version_resolve.rs` via `#[path]`, so it //! must stay dependency-free. +use std::ffi::OsStr; + +/// Returns the exact source commit in release mode, or `None` in development. +/// +/// # Errors +/// +/// Rejects any present release control except `1`, and release sources other +/// than exactly 40 lowercase ASCII hexadecimal bytes. +pub fn release_source( + control: Option<&OsStr>, + source: Option<&OsStr>, +) -> Result, &'static str> { + match control { + None => return Ok(None), + Some(value) if value == OsStr::new("1") => {} + Some(_) => return Err("TRUSTED_SERVER__RELEASE_BUILD must be exactly 1 when present"), + } + let source = source + .and_then(OsStr::to_str) + .ok_or("release builds require TRUSTED_SERVER__GIT_VERSION as a full lowercase commit")?; + if source.len() != 40 + || !source + .bytes() + .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte)) + { + return Err("release source must be exactly 40 lowercase ASCII hexadecimal bytes"); + } + Ok(Some(source.to_owned())) +} + +/// Resolve the actual build source, calling local Git only in development. +/// +/// # Errors +/// +/// Returns release input errors before invoking the fallback. +pub fn resolve_build_source( + control: Option<&OsStr>, + source: Option<&OsStr>, + local_git: impl FnOnce() -> Option, +) -> Result, &'static str> { + if let Some(commit) = release_source(control, source)? { + return Ok(Some(commit)); + } + let override_value = source.and_then(OsStr::to_str); + if override_value.is_some_and(is_usable) { + Ok(resolve_git_version(&Candidates { + override_value, + exact_tag: None, + branch: None, + commit: None, + })) + } else { + Ok(local_git()) + } +} + /// Raw candidates for the deployed git version, in priority order. pub struct Candidates<'a> { /// `TRUSTED_SERVER__GIT_VERSION`, supplied by the deploy pipeline. diff --git a/crates/trusted-server-core/src/build_info.rs b/crates/trusted-server-core/src/build_info.rs new file mode 100644 index 0000000000..2809ded936 --- /dev/null +++ b/crates/trusted-server-core/src/build_info.rs @@ -0,0 +1,101 @@ +//! Immutable facts compiled into an owning product. +//! +//! These facts identify build inputs. They do not establish release provenance +//! or publishing authority; release tags, run IDs and artifact checksums remain +//! external to the compiled product. + +use crate::constants::TS_GIT_VERSION; + +/// The owning product's compile-time feature variant. +#[derive(Debug, Clone, Copy, Eq, PartialEq, derive_more::Display)] +pub enum BuildVariant { + /// The product's default feature set. + #[display("default")] + Default, + /// Fastly's opt-in reusable sandbox feature. + #[display("reusable-sandbox")] + ReusableSandbox, +} + +/// Immutable package, source, target and variant facts for an owning product. +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub struct BuildInfo { + package_version: &'static str, + source: Option<&'static str>, + target: &'static str, + variant: BuildVariant, +} + +impl BuildInfo { + /// Defines owning product facts using the source compiled into core. + /// + /// Call this in an owning constant with Cargo's package version, its build + /// script's target and its own feature variant. + /// + /// # Examples + /// + /// ``` + /// use trusted_server_core::build_info::{BuildInfo, BuildVariant}; + /// const FACTS: BuildInfo = BuildInfo::new("1.2.3", "example-target", BuildVariant::Default); + /// assert_eq!(FACTS.package_version(), "1.2.3"); + /// ``` + #[must_use] + pub const fn new( + package_version: &'static str, + target: &'static str, + variant: BuildVariant, + ) -> Self { + Self { + package_version, + source: TS_GIT_VERSION, + target, + variant, + } + } + + /// Returns the owning product's Cargo package version. + #[must_use] + pub const fn package_version(&self) -> &'static str { + self.package_version + } + + /// Returns the compiled source identity, when known. + /// + /// Release builds require a complete commit. Development builds retain + /// the existing tag, branch and abbreviated commit resolution. + #[must_use] + pub const fn source(&self) -> Option<&'static str> { + self.source + } + + /// Returns the owning product's Cargo target triple. + #[must_use] + pub const fn target(&self) -> &'static str { + self.target + } + + /// Returns the owning product's feature variant. + #[must_use] + pub const fn variant(&self) -> BuildVariant { + self.variant + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn variants_display_canonical_product_labels() { + assert_eq!( + BuildVariant::Default.to_string(), + "default", + "should label the default product variant" + ); + assert_eq!( + BuildVariant::ReusableSandbox.to_string(), + "reusable-sandbox", + "should label the reusable sandbox product variant" + ); + } +} diff --git a/crates/trusted-server-core/src/constants.rs b/crates/trusted-server-core/src/constants.rs index 1fd3c2382c..ae6d953004 100644 --- a/crates/trusted-server-core/src/constants.rs +++ b/crates/trusted-server-core/src/constants.rs @@ -32,14 +32,15 @@ pub const HEADER_X_COMPRESS_HINT: HeaderName = HeaderName::from_static("x-compre pub const HEADER_X_DEBUG_FASTLY_POP: HeaderName = HeaderName::from_static("x-debug-fastly-pop"); // Staging / version identification headers -/// Deployed git version: tag, else branch, else 6-char commit (see [`TS_GIT_VERSION`]). +/// Release source commit, or development ref/short commit (see [`TS_GIT_VERSION`]). pub const HEADER_X_TS_VERSION: HeaderName = HeaderName::from_static("x-ts-version"); /// Fastly service version (`FASTLY_SERVICE_VERSION`), formerly sent as `x-ts-version`. pub const HEADER_X_TS_FASTLY_VERSION: HeaderName = HeaderName::from_static("x-ts-fastly-version"); pub const HEADER_X_TS_ENV: HeaderName = HeaderName::from_static("x-ts-env"); /// Deployed git version compiled in by `build.rs`, from the deploy pipeline's -/// `TRUSTED_SERVER__GIT_VERSION` or local git. `None` when unknown. +/// `TRUSTED_SERVER__GIT_VERSION` or local git. Release mode requires the full +/// lowercase commit; development retains ref/short-commit display. `None` when unknown. pub const TS_GIT_VERSION: Option<&str> = match option_env!("TRUSTED_SERVER__GIT_VERSION") { Some(version) if !version.is_empty() => Some(version), _ => None, diff --git a/crates/trusted-server-core/src/lib.rs b/crates/trusted-server-core/src/lib.rs index 357660ea0d..4cab6fcdde 100644 --- a/crates/trusted-server-core/src/lib.rs +++ b/crates/trusted-server-core/src/lib.rs @@ -35,6 +35,7 @@ pub(crate) mod asset_image_optimizer; pub mod auction; pub mod auction_config_types; pub mod auth; +pub mod build_info; pub mod cache_policy; pub mod cache_purge; pub mod config; diff --git a/crates/trusted-server-core/tests/git_version_resolve.rs b/crates/trusted-server-core/tests/git_version_resolve.rs index 9fb77a2b54..c1dbbd58f7 100644 --- a/crates/trusted-server-core/tests/git_version_resolve.rs +++ b/crates/trusted-server-core/tests/git_version_resolve.rs @@ -4,6 +4,9 @@ mod git_version; use git_version::{Candidates, resolve_git_version}; +use std::ffi::OsStr; +#[cfg(unix)] +use std::os::unix::ffi::OsStrExt as _; const COMMIT: &str = "abcdef0123456789abcdef0123456789abcdef01"; @@ -139,3 +142,113 @@ fn is_usable_accepts_only_visible_ascii() { ); assert!(!git_version::is_usable("v1-été"), "should reject non-ASCII"); } + +#[test] +fn release_source_requires_an_exact_full_lowercase_commit() { + let commit = "abcdef0123456789abcdef0123456789abcdef01"; + assert_eq!( + git_version::release_source(Some(OsStr::new("1")), Some(OsStr::new(commit))), + Ok(Some(commit.to_owned())), + "should preserve the complete release source" + ); + for source in [ + None, + Some(""), + Some("main"), + Some("v1.2.3"), + Some("abcdef"), + Some("ABCDEF0123456789abcdef0123456789abcdef01"), + Some(" abcdef0123456789abcdef0123456789abcdef01"), + Some("abcdef0123456789abcdef0123456789abcdef01\n"), + Some("abcdef0123456789abcdef0123456789abcdef0g"), + ] { + assert!( + git_version::release_source(Some(OsStr::new("1")), source.map(OsStr::new)).is_err(), + "should reject malformed release source {source:?}" + ); + } +} + +#[test] +fn release_control_accepts_only_absence_or_exact_one() { + assert_eq!( + git_version::release_source(None, Some(OsStr::new("main"))), + Ok(None), + "should retain development resolution when release control is absent" + ); + for control in ["", "0", "true", " 1", "1\n"] { + assert!( + git_version::release_source( + Some(OsStr::new(control)), + Some(OsStr::new("abcdef0123456789abcdef0123456789abcdef01")) + ) + .is_err(), + "should reject invalid control even with valid source" + ); + } +} + +#[cfg(unix)] +#[test] +fn non_unicode_release_inputs_fail_closed() { + let non_unicode = OsStr::from_bytes(b"\xff"); + assert!( + git_version::release_source(Some(non_unicode), None).is_err(), + "should recognize non-Unicode control as present and invalid" + ); + assert!( + git_version::release_source(Some(OsStr::new("1")), Some(non_unicode)).is_err(), + "should reject non-Unicode source" + ); +} + +#[test] +fn actual_build_source_resolution_never_calls_git_in_release_mode() { + let source = + git_version::resolve_build_source(Some(OsStr::new("1")), Some(OsStr::new(COMMIT)), || { + panic!("should not call Git in release mode") + }) + .expect("should resolve valid release source"); + assert_eq!( + source.as_deref(), + Some(COMMIT), + "should retain the complete release commit" + ); + let result = + git_version::resolve_build_source(Some(OsStr::new("1")), Some(OsStr::new("main")), || { + panic!("should not fall back after invalid release source") + }); + assert!( + result.is_err(), + "should reject malformed release source without Git fallback" + ); +} + +#[test] +fn actual_build_source_resolution_preserves_development_override_and_fallback() { + for identity in ["main", " v1.2.3 ", COMMIT] { + let source = git_version::resolve_build_source(None, Some(OsStr::new(identity)), || { + panic!("should not call Git with a usable development override") + }) + .expect("should resolve development override"); + assert_eq!( + source.as_deref(), + Some(identity.trim()), + "should preserve development override" + ); + } + for identity in [None, Some(OsStr::new("")), Some(OsStr::new("bad identity"))] { + let mut called = false; + let source = git_version::resolve_build_source(None, identity, || { + called = true; + Some("local-branch".to_owned()) + }) + .expect("should resolve development fallback"); + assert!(called, "should call actual fallback for unusable override"); + assert_eq!( + source.as_deref(), + Some("local-branch"), + "should use fallback result" + ); + } +} diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index 4220851797..b5959253bc 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -101,6 +101,10 @@ export default withMermaid( { text: 'EdgeZero Lifecycle', link: '/guide/edgezero' }, { text: 'Configuration', link: '/guide/configuration' }, { text: 'CLI', link: '/guide/cli' }, + { + text: 'Release Verification', + link: '/guide/release-verification', + }, { text: 'Dev Proxy', link: '/guide/ts-dev-proxy' }, { text: 'Testing', link: '/guide/testing' }, { text: 'Auction Testing', link: '/guide/auction-testing' }, diff --git a/docs/guide/cli.md b/docs/guide/cli.md index 9466e1f3df..d52f78b76f 100644 --- a/docs/guide/cli.md +++ b/docs/guide/cli.md @@ -1,5 +1,7 @@ # Trusted Server CLI +Prebuilt release binaries must pass the [release verification procedure](/guide/release-verification) before execution or deployment. That guide lists the Linux/macOS baselines and the manual Fastly route that preserves verified wasm bytes. + The Trusted Server CLI binary is `ts`. It is a host-target operator tool for configuration, page audits, and EdgeZero-backed lifecycle commands. @@ -57,6 +59,13 @@ commands are shown explicitly. ## Install from source +`cargo install-cli` builds `ts` from source. The resulting local binary is not +covered by the release attestation. Operators who need an attested CLI should +download the matching `ts-` release asset, verify it with the documented +`gh attestation verify` and `gh release verify-asset` commands in the +[release verification guide](/guide/release-verification), and install that +verified file. + From the repository root, install the `ts` binary with the workspace Cargo alias: ```bash diff --git a/docs/guide/release-verification.md b/docs/guide/release-verification.md new file mode 100644 index 0000000000..c01e05780e --- /dev/null +++ b/docs/guide/release-verification.md @@ -0,0 +1,460 @@ +# Verify release binaries + +Phase 1 publishes seven binaries, seven SPDX 2.3 SBOMs, fourteen Sigstore bundles +and `SHA256SUMS`: **29 immutable release assets**. Each binary has SLSA build +provenance and an SBOM attestation. Verify the downloaded bytes before execution +or deployment. Reproducibility, complete composition and the custom Trusted +Server verifier remain later phases. + +The tag caller is `.github/workflows/release.yml`. Its local reusable +`.github/workflows/release-build.yml` owns builds and signing at the same source +revision. The reusable workflow is the certificate signer and provenance builder; +the caller remains the publication owner. Build jobs have no signing authority, +and signing jobs do not check out, compile or execute the downloaded binaries. + +## Platforms and coverage + +| Asset | Phase 1 baseline and qualification limit | +| ----------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | +| `trusted-server-fastly.wasm` | Fastly Compute, `wasm32-wasip1`, default features; reproducibility and hosted runtime qualification remain pending | +| `ts-x86_64-unknown-linux-gnu` | GNU x86-64; UBI 9 builder; maximum required glibc 2.34; Debian 12, Ubuntu 22.04 and UBI 9 startup smokes | +| `ts-aarch64-apple-darwin` | macOS 15 Apple Silicon runner, deployment target 13.0; runtime compatibility qualification remains pending | +| `ts-x86_64-apple-darwin` | macOS 15 Intel runner, deployment target 13.0; runtime compatibility qualification remains pending | +| `trusted-server-axum-x86_64-unknown-linux-gnu` | GNU x86-64; UBI 9 build; maximum glibc 2.34; Debian 12, Ubuntu 22.04 and UBI 9 startup smokes | +| `trusted-server-axum-x86_64-unknown-linux-musl` | Fully static x86-64; musl-tools 1.2.4-2; pinned Alpine and scratch-like startup smokes with runtime CA data | +| `trusted-server-axum-aarch64-unknown-linux-gnu` | ARM64 GNU; native ARM UBI 9 build; maximum glibc 2.34; Debian 12 startup smoke | + +The build checks 64-bit Mach-O magic, target CPU and minimum macOS 13.0 load commands. Actual execution on macOS 13 remains later qualification. +The executables are not notarized. GNU builds require no GLIBC symbol newer than 2.34; glibc 2.34 and newer are +the selected baseline. This includes +Debian 12/bookworm-slim, Ubuntu 22.04+ and RHEL/UBI 9+. Distroless +cc-debian12 has a compatible libc floor but is not a Phase 1 smoke image. +The musl asset needs no dynamic loader or libraries; Alpine and a scratch-like +container are required smoke images, while distroless-static is another deployment option +without a separate Phase 1 runtime test. All acquired smoke images are digest-pinned; build logs record their image IDs. These required startup checks do not establish complete platform/tool qualification. +Axum defaults to `127.0.0.1:8787`. Set runtime `EDGEZERO__ADAPTER__HOST=0.0.0.0` +and `EDGEZERO__ADAPTER__PORT=8080` for containers and publish that port. `PORT` +is a legacy alias. Ports must be 1–65535; zero is rejected. When both are set, they must agree; both values are +validated and any invalid explicit value fails startup. This runtime setting is never compiled into the executable. +The smoke reaches a bridge-networked container through a host-published port; +loopback-only binding cannot pass it. TLS termination, graceful shutdown and +request/resource limits remain follow-up production hardening. + +Axum's TLS client requires a runtime CA certificate bundle, including for health +request initialization. Every Axum smoke mounts the runner's system CA +bundle read-only at `/ca.pem` with `SSL_CERT_FILE=/ca.pem`; production operators +must supply their own runtime trust store. This is runtime data, not an embedded +configuration or dynamically linked library. +Native libraries and provider tools needed by individual `ts` commands retain their existing prerequisites. Cloudflare, Spin, Windows and Linux ARM64 CLI releases follow separately. + +Each `.spdx.json` is a **conservative build dependency inventory** of +the selected Cargo target and the installed npm build tree used for that build. +It records the final executable's SHA-256. SPDX 2.3 is the published format; +format compatibility does not imply the complete composition qualification +required by the retained design. The conversion preserves package identities +but drops dependency relationships; the document adds only the binary-root +`DESCRIBES` relationship. + +The inventory includes npm build tools and may include packages removed by +linking, tree-shaking or minification. The installed `prebid.js` package is a +build-tree entry; the external Prebid bundle is not embedded in tsjs. Native +C/assembly, system libraries, Rust's standard library and compiler runtimes do +not have complete coverage. Exact retained-code attribution and complete native +and tool closure remain Phase 2 work. An SBOM or provenance result is not a +vulnerability scan. + +## Download and verify + +Use an independently trusted GitHub CLI installation with `gh attestation verify`, +`gh release verify`, and `gh release verify-asset`. Use the GitHub CLI version +recorded in the release job log. The SBOM comparison below also uses `jq`. +Verification needs GitHub network access. Obtain the approved tag +and its 40-character source commit through an independent source-review process. +The retained strict SemVer/lightweight-tag validation is deferred; use a +separately approved tag and its peeled source commit. A checksum or commit supplied only with +an untrusted download is not source approval. + +Run this in a new private directory, using Bash. Replace the fictional tag and +commit. For rehearsal, change `REPO` to the approved public sandbox and use the +individually approved, never-reused rehearsal tag. + + + +```bash +set -euo pipefail +REPO=IABTechLab/trusted-server +TAG=v1.2.3 +COMMIT=0123456789abcdef0123456789abcdef01234567 +ASSETS=(trusted-server-axum-x86_64-unknown-linux-gnu trusted-server-axum-x86_64-unknown-linux-musl trusted-server-axum-aarch64-unknown-linux-gnu trusted-server-fastly.wasm ts-aarch64-apple-darwin ts-x86_64-apple-darwin ts-x86_64-unknown-linux-gnu) +gh release download "$TAG" --repo "$REPO" --dir . +test "$(gh api "repos/$REPO/releases/tags/$TAG" --jq .immutable)" = true +gh release verify "$TAG" --repo "$REPO" +for asset in "${ASSETS[@]}"; do + for suffix in "" .spdx.json .provenance.sigstore.json .sbom.sigstore.json; do gh release verify-asset "$TAG" "$asset$suffix" --repo "$REPO"; done + for kind in provenance sbom; do + predicate=https://slsa.dev/provenance/v1; if [[ "$kind" == sbom ]]; then predicate=https://spdx.dev/Document/v2.3; fi + checks=(--repo "$REPO" --bundle "$asset.$kind.sigstore.json" --source-ref "refs/tags/$TAG" --source-digest "$COMMIT" --signer-digest "$COMMIT" --deny-self-hosted-runners --predicate-type "$predicate") + gh attestation verify "$asset" "${checks[@]}" --signer-workflow "$REPO/.github/workflows/release-build.yml" --format json > "$asset.$kind.verified.json" + if [[ "$kind" == sbom ]]; then jq -e --slurpfile sbom "$asset.spdx.json" '[.[].verificationResult.statement.predicate] | length == 1 and .[0] == $sbom[0]' "$asset.$kind.verified.json"; fi + gh attestation verify "$asset" "${checks[@]}" --cert-identity "https://github.com/$REPO/.github/workflows/release-build.yml@refs/tags/$TAG" + done +done +gh release verify-asset "$TAG" SHA256SUMS --repo "$REPO" +shasum -a 256 --check SHA256SUMS +``` + + + +Both predicates use the binary as their subject. The verified SBOM predicate must equal the shipped SPDX document. `--signer-workflow` matches a prefix; the separate `--cert-identity` check pins the exact reusable workflow at the tag. GitHub CLI makes those two identity options mutually exclusive. + +The checksums cover the other 28 assets, never `SHA256SUMS` itself. Immutable release verification establishes membership; provenance establishes the reviewed signer and source. Both are required. + +WORKFLOW and IDENTITY below refer to the same reusable signer: + +```bash +WORKFLOW="$REPO/.github/workflows/release-build.yml" +IDENTITY="https://github.com/$WORKFLOW@refs/tags/$TAG" +``` + +Stop if any check fails. Keep the outputs, reviewed source identity, bundles +and checksums with your deployment record. These checks do not approve a +publisher service, runtime configuration or deployment operation. See the +[GitHub CLI verification reference](https://cli.github.com/manual/gh_attestation_verify) +and [immutable asset verification reference](https://cli.github.com/manual/gh_release_verify-asset). + +## Verify bundles offline + +While online, obtain the trust roots using an independently trusted GitHub CLI +installation. GitHub CLI 2.102.0 supports the flags below; retain its version, +the roots and their digest with the independently approved source identity. +Do not accept replacement roots supplied only alongside an untrusted binary. + +```bash +gh --version +gh attestation trusted-root > trusted_root.jsonl +shasum -a 256 trusted_root.jsonl +``` + +Transfer the binaries, their bundles, published SPDX documents, approved source +pins and trusted roots to the offline machine. Reuse `REPO`, `TAG`, `COMMIT`, +`ASSETS`, `WORKFLOW` and `IDENTITY` from above. Supplying both the bundle and +custom roots avoids network acquisition; there is no `--offline` flag. + +Before disconnecting, obtain an independent trusted SHA-256 pin for each binary +(for example from a separately authenticated release verification record). +Do not treat SHA256SUMS delivered with the same untrusted download as that pin. +Transfer that approved pin file as `APPROVED_BINARY_SHA256SUMS`. + +```bash +shasum -a 256 --check APPROVED_BINARY_SHA256SUMS +for asset in "${ASSETS[@]}"; do + for kind in provenance sbom; do + predicate=https://slsa.dev/provenance/v1 + if [[ "$kind" == sbom ]]; then predicate=https://spdx.dev/Document/v2.3; fi + checks=(--repo "$REPO" --bundle "$asset.$kind.sigstore.json" + --custom-trusted-root trusted_root.jsonl + --source-ref "refs/tags/$TAG" --source-digest "$COMMIT" + --signer-digest "$COMMIT" --deny-self-hosted-runners --predicate-type "$predicate") + gh attestation verify "$asset" "${checks[@]}" --signer-workflow "$WORKFLOW" + gh attestation verify "$asset" "${checks[@]}" --cert-identity "$IDENTITY" \ + --format json > "$asset.offline-$kind-verification.json" + if [[ "$kind" == sbom ]]; then + jq -e --slurpfile document "$asset.spdx.json" \ + 'length == 1 and .[0].verificationResult.statement.predicate == $document[0]' \ + "$asset.offline-$kind-verification.json" + fi + done +done +``` + +Offline success establishes signed build evidence for those retained bytes. +It does not establish current release membership, immutability or deployment +authorization. Retain the separate online release and asset verification record; +checksums alone cannot replace it. + +## Use the verified CLI + +`cargo install-cli` builds `ts` from source. The resulting local binary is not +covered by the release attestation. Operators who need an attested CLI should +download the `ts-` release asset matching their platform, verify it with +the `gh attestation verify` and `gh release verify-asset` commands above, then +copy that verified file into a directory on their `PATH` as `ts` and make it +executable. Verify before first execution. Individual commands may still require +a source checkout and provider tools; see [CLI](/guide/cli). + +## Run a verified Axum release + +Select one of the three `trusted-server-axum-` assets above, download it +and complete the same online/offline attestation and immutable membership checks +before execution. The aarch64 GNU variant is for ARM servers. Copy the verified +file into your deployment directory, make it executable, and retain its digest. + +Application configuration and secrets stay external. With a reviewed checkout's +`edgezero.toml` and adapter manifests available, use the verified `ts` CLI to +validate and publish your private `trusted-server.toml` into the local config +store envelope; this does not rebuild Axum: + +```bash +ts config push --adapter axum --local --manifest ./edgezero.toml \ + --app-config ./trusted-server.toml --no-diff --yes +export TRUSTED_SERVER_CONFIG_TRUSTED_SERVER_CONFIG_TRUSTED_SERVER_CONFIG="$( + jq -er '.trusted_server_config' .edgezero/local-config-trusted_server_config.json +)" +``` + +Supply these runtime secret environment variables through your private secret +management, matching the secret key references in your configuration: + +- `TRUSTED_SERVER_SECRET_TRUSTED_SERVER_SECRETS_HANDLER_PASSWORD` +- `TRUSTED_SERVER_SECRET_TRUSTED_SERVER_SECRETS_PUBLISHER_PROXY_SECRET` +- `TRUSTED_SERVER_SECRET_TRUSTED_SERVER_SECRETS_EC_PASSPHRASE` + +For example, after verifying the x86-64 GNU asset and supplying those variables: + +```bash +chmod u+x ./trusted-server-axum-x86_64-unknown-linux-gnu +EDGEZERO__ADAPTER__PORT=8080 ./trusted-server-axum-x86_64-unknown-linux-gnu +``` + +With `EDGEZERO__ADAPTER__HOST` unset, either port setting retains loopback +(`127.0.0.1`); use your reviewed reverse proxy and +TLS configuration for public traffic. Axum has no help/version or dry-run mode. +Its build smoke starts an isolated server with a fictional external config +envelope and requires `/health` to return `200` and `ok`, distinguishing a +configured router from the configuration-error router. This is a startup check, +not production proxy/load qualification. + +An Axum source build, like `cargo install-cli`, is not covered by these release +attestations. The musl asset is fully static; no allocator change is included. +Later work should consider mimalloc/jemalloc for musl under load. + +## Deploy the verified Fastly wasm + +Keep the verified download unchanged. The ordinary source-build route does not +establish equality with this release, and Phase 1 adds no `ts deploy --release` +command. Use a prebuilt package route for the verified bytes. + +For a manual deployment to an **existing** Fastly service, use the Fastly CLI +16.0.0 pinned in the release tag's `.tool-versions`. Prepare a fresh private +packaging directory containing only the verified wasm and a minimal manifest: + +```toml +manifest_version = 3 +name = "example-app" +``` + +Do not copy operator config, secrets, local fixtures or build hooks into that +directory. Approve the destination service and base version, and inspect their +Config, KV and Secret Store links beforehand. The logical config-store ID comes +from the release tag's tracked `edgezero.toml`; physical store names and actual +configuration are runtime resources. This manual route does not create links, +push configuration, implement staging, or supply recovery guarantees. + +From the private directory: + +```bash +fastly compute pack --wasm-binary ./trusted-server-fastly.wasm +fastly compute validate --package ./pkg/example-app.tar.gz +tar -tzf ./pkg/example-app.tar.gz +tar -xOzf ./pkg/example-app.tar.gz example-app/bin/main.wasm > packaged.wasm +cmp trusted-server-fastly.wasm packaged.wasm +tar -xOzf ./pkg/example-app.tar.gz example-app/fastly.toml > packaged.toml +cmp fastly.toml packaged.toml +``` + +Inspect the archive inventory: it must contain only the expected named package, +its directories, `fastly.toml`, and `bin/main.wasm`. Check the extracted manifest +against the approved minimal manifest. Select the named archive explicitly; +packing can also produce a generic `package.tar.gz`. Do not guess an archive +name or use a previously populated `pkg/` directory. + +After verifying the package member is byte-identical, use your reviewed service +ID and integer base version in place of these fictional values: + +```bash +fastly compute deploy --package ./pkg/example-app.tar.gz \ + --service-id '99999' --version '7' --no-default-domain +``` + +This command can activate production and change an active/locked base version +by cloning it; confirm its behavior for the pinned CLI before use. Retain the +verified wasm, manifest, package digest, previous active version and observed +result. No build step may replace or annotate the verified wasm. Automated +EdgeZero prepared-payload deployment and operation recovery are later phases. + +### Local runtime evidence + +Run verified Fastly bytes with pinned Viceroy and external example configuration for local testing. A page request can check the origin response, injected JavaScript and source SHA in `x-ts-version`. Default single mode emits no Build facts log. Local execution does not establish provider submission or production operation. + +## Build inputs, policy and release administration + +Builds use the tracked source, manifests, `Cargo.lock`, pinned Rust and Node, +the JS lockfile and fresh JS bundles. The tracked `edgezero.toml` supplies the +logical config-store ID and participates in the template build digest. Build +facts include the source commit. The CLI carries the fictional example-config +scaffold, not an operator config. + +Release Fastly responses report `x-ts-version` as the full 40-character source +commit supplied by the release build. Development display fallbacks are not the +release identity. `ts --version` labels `package= source= target= variant=default`. Release mode requires the full 40-character lowercase source commit; the package version is not the release tag. Attestation pins authenticate these inputs externally. +The executable's SHA-256 is checked externally against the retained release +bytes; exposing that digest as a runtime build fact belongs to #161. + +The current compile-time input audit is recorded in the spec's Phase 1 note. +It includes Rust 1.98.1, tracked logical IDs, the private JS build path and its +indirect build-module options, and the locked native dependencies. It does not +claim a complete native tool closure or hermetic build. + +The tools and consumer policy files are carried unchanged. The cohort retains +its original four entries and adds the three Axum entries documented in the +[release PR’s compatibility audit](https://github.com/IABTechLab/trusted-server/pull/1435). +All three have **no Phase 1 readers** and do not authorize these builds or replace +the workflow's explicit selections. + +| Policy | Current use | Additive later adoption | +| ----------------------------------------------- | ---------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------ | +| `scripts/release/release-cohort.json` | Original four plus three descriptive Axum entries; no reader | Checked fixed cohort selection, original assurance profiles and compatibility qualification | +| `scripts/release/release-tools.json` | Retained pending/null qualification records; no reader | Checked tool identities, authenticated acquisition, image/SDK measurements and qualified recipes | +| `crates/trusted-server-cli/release-policy.json` | Retained signer/caller and SPDX contract; no reader or custom verifier | Embedded consumer policy, bounds, historical-policy acceptance and complete index/evidence joins | + +Phase 1 does not ship `release-index.json`, `tsjs-inventory.json`, build-evidence, +build-recipe or rebuild-check placeholders. Their frozen schemas and names +remain reserved for the later implementation. These releases do not yet satisfy +the frozen custom verifier's complete evidence graph. + +CI audits compile-time deployment inputs, scrubs inherited environment values, +and scans every executable for planted fictional config values. These are +regression checks, not hermetic-build qualification. Operator configuration, +credentials, physical bindings and variable permission policy remain runtime +inputs. + +Before publication, a maintainer must enable immutable releases, protect `v*` +tags and approve tag-push authority and required checks. Immutability remains a +human precondition. Set repository variable `RELEASE_PUBLICATION_ENABLED=true` only after confirming those settings with the read-only commands below. Without it, both signing and publication are skipped; an early tag push creates no public attestations. There is no repository-administration token or prepublication settings API call, following the reviewed decision to check the published release instead. A +violation can expose a mutable release before the workflow reports failure. + +Tags containing `-` publish as prereleases and never become Latest. Stable tags publish as normal releases with `make_latest: legacy`; GitHub chooses Latest by version/date rather than forcing a backport release to Latest. Whether to authorize tags from rc branches is a maintainer decision; the Phase 2 ancestry/ruleset validator is not implemented here. + +Publication lists releases, including drafts, and refuses every existing +matching tag. It creates one draft, retains its release ID, and binds uploads +and the final publish mutation to that ID. It checks the exact signing-approved asset +set, validates each upload response's size and SHA-256 digest before publication, +and checks immutable membership after publication. Published assets are then re-downloaded and byte-compared against the signed `SHA256SUMS` and validated handoff. The reviewed publication owner remains additive later work. There are no +automatic mutation retries or draft-resume claims. + +Only post-publish reads may retry: release/asset metadata GETs and the GitHub +CLI asset verification calls. Each check has at most three attempts, +with 2s then 5s backoff. A supplied `Retry-After` can increase a delay, capped +at 10s (at most 20s waiting per check). A shared three-minute window bounds +retry scheduling across the job, and individual read requests/commands time out +after 10s, except bulk asset downloads, which have up to 60s capped by the remaining shared deadline. `Connection: close` remains enabled for API calls. + +Retries require a recognized transport error, timeout, HTTP 5xx or 429. +GitHub CLI errors are classified conservatively: an otherwise empty-output +HTTP 5xx/429 diagnostic or a recognized transport/timeout code may retry; +ambiguous failures stop. Successful responses with wrong identity, immutability, +membership or digests, and definitive verification failures stop immediately. +Release listing before creation and every create/upload/publish mutation remain +single-shot. + +Before cryptographic release verification, the availability check makes at most five release-attestation GETs, with 2s/5s/10s/20s backoff (37s total waiting). Only this endpoint treats HTTP 404 as no record yet; a valid HTTP 200 with no current-tag record may also wait. Each GET times out after at most 4s, and a separate 60s deadline bounds availability checking, including other-tag verification, within the shared three-minute scheduling window. Every other non-200 status fails immediately. Malformed records, duplicate current-tag records, identity/source/asset mismatches and definitive cryptographic failures stop immediately. Other-tag records are ignored only after their exact bundles verify; those read-only verification calls may retry recognized transient failures within the same bounds. Current-tag cryptographic verification is single-shot. Logs show every wait or availability on the first check. No mutation is retried. + +The publisher re-downloads all 29 assets into a fresh temporary directory, checks the exact file set and byte-compares them with the validated signed handoff, including identical SHA256SUMS. Only transport download failures may retry; any missing, extra or different file fails immediately. Temporary downloads are removed after each attempt. + +If the availability check exhausts its bound, the release is already published and immutable (verified before availability polling). Keep the tag and release unchanged and use the read-only recovery commands below. + +If these retries are exhausted, the release has already been published; a +failure does not undo publication or establish that immutability was verified. +Confirm its state using the recorded release ID. For an immutable release, keep +its tag and assets unchanged and rerun only the following read-only checks in +the directory containing the downloaded assets. Set `RELEASE_ID` to the ID from +the release job log; retain the reviewed `REPO`, `TAG`, `COMMIT` and `ASSETS` +values from the verification procedure above. + +```bash +gh api "repos/$REPO/releases/$RELEASE_ID" \ + --jq '{id, tag_name, draft, immutable, assets: [.assets[] | {name, size, digest}]}' +test "$(gh api "repos/$REPO/releases/tags/$TAG" --jq '.id')" = "$RELEASE_ID" +test "$(gh api "repos/$REPO/releases/$RELEASE_ID" --jq '.immutable')" = true +gh api "repos/$REPO/attestations/sha1:$COMMIT?per_page=100&predicate_type=release" \ + --jq '[.attestations[] | select(.initiator == "github") | .bundle.dsseEnvelope.payload | @base64d | fromjson | .predicate.tag]' +gh release verify "$TAG" --repo "$REPO" +for asset in "${ASSETS[@]}"; do + for suffix in '' .spdx.json .provenance.sigstore.json .sbom.sigstore.json; do + gh release verify-asset "$TAG" "$asset$suffix" --repo "$REPO" + done +done +gh release verify-asset "$TAG" SHA256SUMS --repo "$REPO" +shasum -a 256 --check SHA256SUMS +``` + +Then rerun both signer identity checks for provenance and SPDX for each binary, +including the SPDX predicate comparison, exactly as documented above. Preserve +the successful verification outputs with the release record; do not rerun the +publication job to recover a failed read-only check. + +After a failed or indeterminate mutation, inspect the recorded release ID and +all matching drafts read-only. Do not rerun uploads, delete a release, or reuse +the tag without an independently approved recovery decision. Published immutable +releases are never overwritten. A fresh release needs a fresh approved tag; +Phase 2's full reconciliation and incident owner remains deferred. + +Before the first production release, rehearse the reviewed commit in a separate +public repository with Actions and immutable releases enabled. Use a fresh, +lightweight `v0.0.0-rehearsal.N` tag for each attempt and retain verification +outputs in the release PR. Keep rehearsal tags and releases out of the upstream +repository. For production, confirm the required settings and checks, then +follow the maintainer release authorization process before tagging or deploying. + +## Maintainer settings checklist + +An administrator must configure these settings before a production tag. The +commands below only inspect settings; this workflow applies none of them. + +- **Settings → General → Releases:** enable immutable releases. See + [GitHub's immutability instructions](https://docs.github.com/en/code-security/how-tos/secure-your-supply-chain/establish-provenance-and-integrity/prevent-release-changes). +- **Settings → Rules → Rulesets → New tag ruleset:** set enforcement to **Active**, + target `v*` (API pattern `refs/tags/v*`), and enable **Restrict creations**, + **Restrict updates** and **Restrict deletions**. Add only the designated release + maintainers' team to the bypass list, with **Always allow** bypass. Record the + team's name and ID in the maintainer release record; avoid broad role bypasses. + See [repository ruleset configuration](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/creating-rulesets-for-a-repository). +- Optionally require the reviewed release build checks on the source branch + before approving a tag. Use actual check names from that commit. A tag-triggered + check cannot be required to pass before creating its own tag. Source eligibility + and the later policy reader are not enforced by Phase 1. + +```bash +REPO=IABTechLab/trusted-server +gh api "repos/$REPO/immutable-releases" +gh api "repos/$REPO/rulesets?includes_parents=true" \ + --jq '.[] | {id, name, target, enforcement}' +RULESET_ID=12345 +gh api "repos/$REPO/rulesets/$RULESET_ID?includes_parents=true" \ + --jq '{name, target, enforcement, conditions, rules, bypass_actors}' +gh api "repos/$REPO/commits/$COMMIT/check-runs" \ + --jq '.check_runs[] | {name, status, conclusion}' +``` + +The immutable-releases endpoint must return `enabled: true`; a disabled setting +returns HTTP 404. Inspect the ruleset's tag target, active enforcement, exact +patterns, creation/update/deletion rules and team bypass actors. The API exposes +bypass actors only to callers with sufficient repository access; an omitted field +is not proof of an empty bypass list. Also review inherited rulesets. These are +maintainer gates, not evidence that Phase 1's publication token can administer +repository settings. + +### Phase 1 review boundary notes + +The reusable caller declares the maximum signing permissions required by its +called workflow. GitHub does not allow a read-only caller to invoke a nested +workflow that declares greater permissions, even for conditionally skipped jobs. +Actual build/check jobs narrow permissions to contents-read; signing remains +tag-only and requires the publication enablement variable. Splitting a new build-only workflow is deferred to preserve this slice. + +The configuration scanner covers literal/raw/concatenated macros and literal +build-script deployment prefixes. It is a best-effort source regression audit, +not Rust expansion or dataflow analysis: `include!`, raw byte strings (`br"…"` / +`rb"…"`), computed names, procedural macros and third-party build scripts are not +completely analyzed. Planted canary scans and +clean inherited environments provide additional checks, not hermeticity. diff --git a/docs/superpowers/plans/2026-10-13-attested-release-implementation.md b/docs/superpowers/plans/2026-10-13-attested-release-implementation.md new file mode 100644 index 0000000000..3f0b2cb702 --- /dev/null +++ b/docs/superpowers/plans/2026-10-13-attested-release-implementation.md @@ -0,0 +1,1624 @@ +# Attested release binaries and verified consumption Implementation Plan + +## Phasing — 2026-10-22 + +This is the phased rework of [#1262](https://github.com/IABTechLab/trusted-server/pull/1262), frozen at `122b78664b1235c717e9208fd200f9a9b0c335df`. The reviewed contract below is retained as the destination design. This section controls phase applicability; historical statements about first-release gates and PR ownership below refer to the original combined proposal. No new spec approval cycle is introduced by reducing scope. + +**Phase 1 contributes to #1235 (D7 settings pending):** tag-triggered builds of Fastly wasm, three native CLI binaries and three production Axum binaries; SLSA provenance and a shipped, attested SBOM for each binary; draft/upload/publish of an immutable release with `SHA256SUMS`; GitHub CLI verification pinned to the reusable `release-build.yml` signer and predicate; and CI enforcement of configuration independence. Operators deploy the verified release bytes. Reproducibility is not a Phase 1 claim or gate. The source-version and fresh JS mechanisms already on main are reused. + +Phase 1 publishes SPDX 2.3 for a conservative selected-Cargo-target and installed-npm-build-tree inventory, with the final binary hash. CycloneDX is only an intermediate generator format. Complete composition qualification below remains Phase 2+; Phase 1 does not claim exact retained-code or native-source closure. + +**Phase 2+ retains all remaining requirements:** two-build reproducibility, confined recipes, platform qualification, authenticated tool/image acquisition, complete composition/SPDX coverage, candidate collection/index, the custom consumer, publication owner, Task 14 same-commit validation, and Cloudflare/Spin distributions plus full custom-verifier qualification of Axum. These slices remain closed to production selection and signing until separately qualified and authorized. + +**Phase 1 Axum extension:** the original four asset names remain unchanged; +three named Axum variants extend this slice to seven binaries and 29 files. +GNU builds enforce a maximum glibc requirement of 2.34; musl is fully static. +The cohort records this explicit extension, but tools/policy and the frozen v1 +index schema remain unchanged. Phase 1 emits no index. The later index/consumer +slices need a reviewed seven-artifact amendment, including new target/capability +forms; see the [compatibility audit](binary-attestations-phase1-compatibility.md) +and slice-map annotations. + +### Phase 1 review notes + +Phase 1 is **part of #1235**, pending D7 maintainer settings. These notes supersede +historical phase applicability, not the retained destination requirements. +Goals, Non-goals, R-COHORT and R-BOUNDS refer to the original four-artifact design; +this slice explicitly includes seven binaries / 29 files, including Axum and +Linux ARM64. Its original four public names stay unchanged; no v1 index is emitted. +The Node producer helpers and inline publisher are interim exceptions to the +retained Rust-producer constraint, replaced by slices 2.5 (recipes/inspection), +2.6 (composition/SPDX) and 2.8 (publication). The frozen Status and tool versions +below describe #1262, not current Phase 1 implementation status. + +Current tool pins: Rust 1.98.1, Node 24.20.0, Fastly CLI 16.0.0, Viceroy 0.21.0; +verification uses GitHub CLI 2.102.0. The reusable release-build.yml is the signer. +Slice 2.3 immutable build facts are reused from introducing commit +`bfb697404551a28217af8906024506095bcd9559` and revision +`6d0efda7210983021ab3c9efe8a0a3190e9eb81b`, as retained at the frozen source. +Core's resolver and new CLI/Fastly owner build scripts validate +`TRUSTED_SERVER__RELEASE_BUILD=1` and the full lowercase source SHA. +Cargo supplies the owning target/package; features supply the variant. Tags and +run IDs stay external. CLI now labels package/source/target/variant; Fastly queues +the same immutable facts through existing startup diagnostics in reuse mode only; single mode retains the source response header without per-request build-fact logs. The source audit +permits exactly this release control, GIT_VERSION and Cargo-owned BUILD_TARGET, +not deployment values. CLI source-resource cleanup remains deferred. + +Axum binding is runtime-only: `EDGEZERO__ADAPTER__HOST` defaults to loopback; +containers can set `0.0.0.0`, with runtime `EDGEZERO__ADAPTER__PORT` or the compatible `PORT` override. Both values are validated, and conflicting values fail startup. The smoke uses +bridge networking and reaches the process through a published host port. +TLS termination, graceful shutdown and request/resource limits remain follow-up. + +Publication requires `RELEASE_PUBLICATION_ENABLED=true` only after maintainers +confirm immutable releases and the v\* tag ruleset. There is no settings token or +prepublication settings API call, following the reviewed signing boundary; +postpublication immutable=true readback fails loudly. Hyphenated tags are +prereleases and never Latest. Stable tags are normal releases with `make_latest: legacy`, allowing GitHub to choose Latest by version/date rather than forcing backports to Latest. Eligibility of +rc-branch tags is an unresolved maintainer decision; Phase 1 does not implement +the destination branch-ancestry/ruleset validator or authorize rc tags. + +The phase labels on requirement headings below apply only to the reduced Phase 1 subset described above; all additional clauses and requirements marked Phase 2+ remain deferred. The original assurance profiles, custom verifier, retained evidence graph and reproduction gates do not block the first Phase 1 release. The [implementation plan](../plans/2026-10-13-attested-release-implementation.md#phasing--2026-10-22) contains the acceptance mapping and exhaustive slice inventory. + +### Phase 1 implementation checklist + +- [x] Copy reviewed spec, plan, feasibility and schemas from `122b78664b1235c717e9208fd200f9a9b0c335df`; preserve linked historical evidence. +- [x] Add `.github/workflows/release.yml` as caller/publisher and reusable `release-build.yml` as builder/signer: seven tag builds, isolated provenance/SBOM signing, draft/upload/publish and immutable readback; optional read-only PR builds. +- [x] Generate intermediate CycloneDX from the selected Cargo target after its locked release build and npm's installed tree from that same build; merge without reference collisions, convert to published SPDX 2.3 and bind the final binary hash. Record conservative build-dependency coverage, including npm build tools; do not claim exact retained-byte or native-source closure. +- [x] Enforce fresh npm install, prohibit skip/prebuilt overrides, audit compile-time config inputs and reject planted operator values in every binary. +- [x] Document exact GitHub CLI provenance/SBOM and immutable-asset verification, native baselines and deployment of verified Fastly bytes; update CLI guide, navigation and changelog. +- [x] Run pure local tests, actionlint, format/domain checks, touched build gates, independent and security review; push this branch and open only the requested draft PR. + +| #1235 acceptance item | Phase 1 evidence | +| ----------------------------------------------------- | ---------------------------------------------------------------------- | +| Reviewed spec and compile-time input list | Retained R-CONFIG/R-INPUTS, this phasing section; #1262 review history | +| Tag publishes immutable binaries, SBOMs and checksums | release.yml caller/publisher and release-build.yml signer | +| Provenance, SBOM predicate and asset verification | release-verification.md; exact predicate and asset checks | +| Fresh JS; fail missing npm/install/skip | Existing main build.rs plus clean npm ci and release preflight | +| Deploy exact bytes | Manual verified Fastly package route; no reproducibility claim | +| Immutable setting and protected tags | Maintainer gate; no setting changes by this PR | +| Operator documentation | CLI guide, release-verification, CHANGELOG | + +### Complete remaining-work slice map + +The [machine-readable inventory](binary-attestations-slice-map.json) assigns every one of 434 source-diff paths exactly once, with full source commit citations. It is an ownership map, not automatic cherry-pick boundaries: shared Cargo manifests/locks, entry points and workflows require staged hunks reconciled with main. No source work is discarded. Source files later reused in Phase 1 are additionally identified in the PR; the remaining hunks stay with their assigned slice. + +| Phase / PR suffix | Scope | +| ----------------- | --------------------------------------------- | +| 2.1 | strict documents policy preflight source | +| 2.2 | process and native sandbox | +| 2.3 | immutable build facts | +| 2.4 | js inventory composition materials two builds | +| 2.5 | recipes inspection manual package | +| 2.6 | composition spdx | +| 2.7 | collection workflow jobs candidate index | +| 2.8 | publication owner | +| 2.9 | candidate ci same commit validation | +| 2.10 | consumer cli | +| 2.11 | docs and format misc | + +All PR titles begin `Binary attestations: phase 2.N –`; all branches begin `feature/binary-attestations-phase2-N-`. Stack slices in order except the consumer CLI, which may branch directly from Phase 1. Keep every later slice closed: no signing triggers, production selector or qualification bypass. The source PR remains frozen and open until the user decides it is superseded. Cloudflare and Spin distribution, plus full Axum consumer/platform qualification, follow these slices; Phase 1 already distributes the seven binaries described above. Task 14 remains Phase 2.9. Human gates are immutable releases/tag protection and explicit authorization of the first real tag; signing/publication evidence is retained in the compatibility audit and release PR. + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Publish and verify the four canonical Fastly/`ts` artifacts required by #1235, with complete evidence and a validated manual Fastly production route. + +**Architecture:** A small domain-specific host-only Rust release tool builds and describes bytes; POSIX wrappers select its independently approved native bootstrap; a reusable workflow signs only validated outputs; one caller publishes the complete immutable distribution. A host-only Trusted Server verifier separately establishes build evidence, publication and consumer authorization before retaining verified bytes. Packaging and integrated EdgeZero deployment are separate milestones that consume these boundaries. + +**Tech Stack:** Rust 1.95.0/2024, existing workspace serde/serde_json/error-stack/derive_more/clap/sha2/log/tempfile/toml/gix/libc/flate2 as needed, qualified Rust schema/archive/binary parsers, Node 24.12.0/npm 11.6.2, Vite/Rollup, SPDX 2.3, GitHub CLI 2.97.0, actions/attest 4.2.2, Fastly CLI 15.1.0. CLI consumer tasks retain their existing tokio/reqwest architecture. + +--- + +## Authority, baseline and scope + +Planning baseline: `44c5b5620bfb1f82e5b959ab5ba8ec69c7091e52`, inspected 2026-10-13. The governing [contract](../specs/2026-10-09-1235-release-attestation-design.md), [feasibility register](../specs/2026-10-12-1235-release-attestation-feasibility.md) and [index schema](../specs/schemas/release-index-v1.schema.json) remain drafts awaiting maintainer approval. Delivery direction on 2026-10-13 is to develop/review the spec, plans and implementation together on `spec/1235-release-attestation` in PR #1262, with no new Trusted Server PR. The reviewed plan guides implementation here; maintainer contract/code approval remains required before merge, and the plan does not qualify a release or authorize live publication/deployment. Task 1 records this delivery contract and the unchanged qualification prerequisites. + +#1214 and #1213 are present at this baseline. Extend their merged private JS output/prebuilt interface and source-version mechanism; do not reimplement the historical audit snapshots. Source deployment currently delegates to EdgeZero and no release workflow/verifier exists. + +The initial cohort is exactly: + +| Canonical asset | Required assurance | Blocking qualification | +| ----------------------------- | ---------------------------------------------------------- | ---------------------------------------------------------------------------------- | +| `trusted-server-fastly.wasm` | `provenance-sbom-repeatable`; default, no reusable sandbox | Final complete-byte A/B equality, real SBOM/runtime and verified manual submission | +| `ts-x86_64-unknown-linux-gnu` | `provenance-sbom` | glibc ceiling 2.34; clean UBI 9, Ubuntu 22.04, Debian 12 startup | +| `ts-aarch64-apple-darwin` | `provenance-sbom` | Explicit loader floor 13.0; actual macOS 15 ARM execution | +| `ts-x86_64-apple-darwin` | `provenance-sbom` | Same loader floor; actual macOS 15 Intel execution | + +Every selected canonical Build A, SBOM, signature and publication check is mandatory. CLI Build B is a bounded measurement, not a shipping equality requirement. No Linux ARM64, installer packages, other adapter releases, retained-input archive, operator reconstruction or official offline deployment is added by this plan. + +Two linked plans keep independent work out of these closure criteria: + +- [CLI packaging attestation integration](2026-10-13-cli-packaging-attestation-integration.md): reconcile #1292, package exact canonical bytes and authenticate member identities. Later releases only after the required spec/schema amendments are approved. +- [Fastly prebuilt EdgeZero integration](2026-10-13-fastly-prebuilt-edgezero-integration.md): upstream prepared payload, integrated online production/staging and durable recovery; AC-8 is separate from initial #1235 closure. + +## Rust migration and current execution boundary + +The user's 2026-10-14 correction prohibits introduced Python release tooling. The [migration design addendum](../specs/2026-10-14-release-tooling-rust-migration.md) selects `crates/trusted-server-release`: a Rust 2024/1.95.0 host-only workspace member, excluded from default members and independent of core, CLI and JS crates. The existing host-only codegen member is the precedent. The tool is internal producer tooling, not a fifth canonical asset, a generic attestation framework or a publisher abstraction. `ts` remains the consumer in Tasks 17–20: CLI → core → JS makes the CLI unsuitable as the JS/bootstrap producer. + +Preserve `collect-js.mjs` and the implemented Node composition hook. POSIX wrappers use an independently approved-source, OS-native Rust toolchain to build/select the release tool before executing it; a colocated SHA file or self-reported source label supplies no authority. Developer Cargo commands below establish local behavior only. Production needs the authenticated bootstrap, full qualified source/dependency/tool closure and reviewed native OS adapters. No language migration waives native SDK/tool acquisition or authenticating the original Fastly CLI archive through independently reviewed vendor-source evidence. + +- [x] **M0: Remove superseded tooling.** Introduced Python release code and validator requirements are removed. Deleted prototype behavior is not implemented or fixed by removal; pending Rust regressions below remain mandatory. +- [ ] **M1: Port primitives and preflight (Tasks 2–3).** Strict document/schema utilities, fixed policy/environment checks, local Git object/worktree authentication and the helper-closure fingerprint are implemented. Git commands use bounded process supervision and a shared command deadline. Independently authenticated bootstrap/tool acquisition and qualification remain incomplete; the closure fingerprint supplies no acquisition authority. Register each private test module before applying the nonzero-selection guard. +- [ ] **M2: Port material/process/OS boundaries (Tasks 7–9).** Establish original archive/Git identities, bounded child execution, kill/reap/cleanup, private staging, source/object read-only protections and real native denial controls. Preserve independent all-cohort remote receipts separately from rehashing tools actually installed in the current execution environment. +- [x] **M3: Restore real JS orchestration (Task 7).** Perform two fresh offline installations, actual lifecycle observations and denied missing-tool probes, all tracked bundle comparisons and both full registered JS/type-check suites. Only actual successful execution constructs a private typed completion bound to source, tools, settings and retained A bytes; parsed JSON, paths or booleans cannot construct it. The full Linux A/B execution test passed in ordinary PR CI on 2026-10-15 (1,275/1,275 tests per side, 13 byte-identical modules, six private roots removed); see the [two-build evidence report](../../../scripts/release/qualification/2026-10-15-js-two-builds.md). This is candidate CI execution, not hosted release qualification. +- [ ] **M4: Restore canonical recipes and inspection (Tasks 8–9).** Candidate recipes are implemented for all four assets and pass ordinary CI with `release_qualification: false`; qualification, the closed `build.sh` interface and the production reproduction profile remain pending. Compose actual Cargo preparation, compile, final acceptance and copying; collect Task 10 metadata/tree/native evidence through that same boundary before hosted qualification. Local tiny artifacts cannot qualify the full product or actual macOS 15/GNU cohort. +- [ ] **Then Tasks 10–22.** Implement the remaining Rust producer modules, workflow contracts, consumer verification and manual route in dependency order. Complete every selected qualification and full CI gate before release enablement/handoff. + +The producer helper MUST itself be built and executable inside the selected GNU builder at glibc <=2.34, using an independently approved image/toolchain/material channel. An ambient Ubuntu 24 build cannot be assumed to load in UBI 9. Bind helper target, loader/runtime, source/dependency closure and actual native executable identity before execution. macOS helpers require the approved native CPU/SDK/floor-compatible chain. Initial shell/hash/Rust/Cargo authority is independent of the tool; record and authenticate rustup only if it is actually used (direct signed-component installation does not require it); the tool cannot authenticate itself. Native Rust helper/staged POSIX drivers implement socket and lifecycle probes, without language substitutions. + +Current implementation includes strict documents/policy, local source/preflight checks, bounded process supervision, owning build facts, checked JS inventory and Node composition. Original archive authentication, bounded private extraction and lifecycle declaration review are implemented. Final-artifact inspection and experimental native boundaries are implemented, with separate consumer evidence verification. The experimental macOS profile now kills and rejects descendants that escape the process group, including with a scrubbed environment, by identifying its sandbox instance after each run; it remains unqualified and does not own descendant cleanup the way a container does. Native boundary controls pass ordinary Ubuntu 24.04 and macOS 15 CI; they do not qualify complete release recipes or authenticated tool selections. Original npm and Cargo Git material verification and two-installation JS orchestration are implemented; the full Linux A/B execution test passed in ordinary PR CI. Candidate recipes for all four assets are implemented and pass ordinary CI (unqualified, `release_qualification: false`). Qualification, the closed `build.sh` interface, the production reproduction profile, authenticated bootstrap/acquisition and hosted qualification remain incomplete. Production wrappers and qualified selection remain closed. + +## Historical progress references + +Original task progress is preserved at source commit `122b78664b1235c717e9208fd200f9a9b0c335df` in this plan's original path. It is evidence for later slices, not Phase 1 completion. Last confirmed full green: `b6f5d92e4`, run `39639053390`; four-asset candidate join: run `39577910126`. Other cited runs: `39273777887`, `39290139247`, `39417433680`, `39420011152`, `39540057202`. + +## Work breakdown and ownership + +Use successive focused commit/review groups within PR #1262: (1) input/fact/JS guards, (2) qualified recipes and composition, (3) collection/signing/publication, (4) verifier/manual route/docs. Each depends on its predecessor; tag enablement waits for all four. No new Trusted Server implementation PR is created. Build scripts can be proved locally before production YAML exists. No intermediate PR may enable an incomplete release trigger. + +| File group | Responsibility | +| ----------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- | +| `scripts/release/release-cohort.json`, `release-tools.json` | Reviewed fixed identities, profiles, approved RC branches and qualified environment pins | +| `crates/trusted-server-release/Cargo.toml`, `src/main.rs` | Host-only producer dispatch, with no product-crate dependencies | +| Release tool `src/documents.rs`, `policy.rs`, `preflight.rs`, `source.rs` | Strict bounded JSON, fixed contracts, authenticated source and explicit environment | +| Release tool `src/materials.rs`, `process.rs`, `boundary.rs`, `preparation.rs`, `javascript.rs`, `qualification.rs` | Original acquisitions, bounded execution, real OS isolation, fresh JS and native qualification | +| `scripts/release/bootstrap.sh`, `build-js.sh`, `build.sh`, `collect-js.mjs` | Approved native Rust bootstrap, fixed POSIX adapters and retained Node observer | +| Release tool `src/recipe.rs`, `inspect.rs`, `composition.rs`, `spdx.rs`, `collection.rs`, `distribution.rs`, `publication.rs` | Fixed recipes, actual composition, SPDX, transfers, distribution and sole-owner publication support | +| Release tool `src/package.rs` | Bounded trusted-source manual Fastly inspection; later approved packaging adapters | +| `.github/workflows/release-validation.yml`, `release-build.yml`, `release.yml` | Same-commit gates; isolated builds/signing; sole draft/publication owner | +| `crates/trusted-server-cli/src/release/` | Host-only consumer verifier: policy, strict documents, bounded gh, GitHub publication and evidence graph | +| `crates/trusted-server-cli/src/commands/release.rs` | Public read-only `ts release verify` adapter | +| Release tool private tests, `tests/commands.rs`, `tests/fixtures/`; CLI/core/JS tests | Producer negatives, public-command process tests, consumer fixtures and actual qualification | + +Execute each listed negative fixture as a separate red/green microcycle: write one case, run its focused filter, implement the smallest relevant behavior and rerun it, then move to the next case. A task groups related work for review/commit; its fixture list is not one giant implementation step. + +Task 2 utilities and the retained Rust/Node guard work are implemented. Remaining Rust module names below are proposed, not existing APIs. Create only modules needed by the current stage, without placeholder files. Private unit tests live alongside `src/.rs` under `#[cfg(test)]`, or in registered `src//tests.rs`; fixture data lives in `crates/trusted-server-release/tests/fixtures/`. `tests/commands.rs` drives the public binary. Do not make internal APIs public merely for integration tests. Use concrete error-stack reports, top-level imports, descriptive assertions, documentation on public APIs and project commit conventions. + +Keep direct dependency features minimal. Reuse workspace dependencies only where an actual stage needs them; the producer has no dependency on core, CLI or JS. Maintained TAR/ZIP, schema and binary-format parsers are justified by existing bounded archive, Draft 2020-12/Draft 7 and ELF/Mach-O/core-Wasm requirements. Research exact maintained versions from primary sources and qualify semantics/acquisition before pinning; this plan invents no replacement version or digest. Library parsing does not replace archive limits or source authentication. + +### Task 1: Freeze approved scope and reconcile the packaging owner + +**Phase: 2+ (historical destination task).** Phase 1 uses the reduced implementation checklist above, rather than completing this task. + +**Files:** Modify this plan and the governing spec only for approved corrections; reference PR #1292's `docs/superpowers/specs/2026-10-12-863-cli-release-packaging-design.md` and `docs/superpowers/plans/2026-10-12-cli-release-packaging.md` after their owning branch is available. + +- [ ] Read the current spec revision; record its commit and the same-branch/same-PR delivery direction. Work through reviewed tasks in PR #1262; obtain maintainer contract/code approval before merge. Do not require a separate spec merge or open another implementation PR. +- [ ] Record one publication owner, `.github/workflows/release.yml`, and one signing identity, same-revision `release-build.yml`. #1292 MUST NOT independently publish an overlapping immutable tag while #1235 assets are still pending. +- [ ] Confirm the current four-artifact cohort and three CLI baselines. Initial #1235 delivery may precede packaging on a different new tag. Adding package records or ARM64 requires the linked plan's approved delta, never a covert schema expansion here. +- [ ] Settle one strict producer control channel: `TRUSTED_SERVER__RELEASE_BUILD=1`, injected by trusted recipe after environment sanitation, recorded as an input, rejected if present with another value. This is a fact/input guard, not the prohibited parallel JS fallback mode. Name the checked prebuilt inventory input `TRUSTED_SERVER__TSJS_PREBUILT_INVENTORY`. +- [ ] Confirm permission policy remains runtime-variable; conflicting #1045/#1084 code blocks release enablement. Record the owner and dependency rather than designing a new permission transport. +- [ ] Review milestone exits with maintainers in PR #1262. Commit any approved documentation delta as a focused commit on this branch with `Clarify release implementation prerequisites`. + +**Exit:** An engineer can identify approved source, scope, producer controls and the single release authority; no unspecified cross-PR ownership decision remains. + +### Task 2: Establish Rust producer fixtures and strict document utilities + +**Phase: 2+ (historical destination task).** Phase 1 uses the reduced implementation checklist above, rather than completing this task. + +**Create:** `crates/trusted-server-release/Cargo.toml`, `src/main.rs`, `src/error.rs`, `src/documents.rs`, private registered `documents::tests`, `tests/commands.rs`, `tests/fixtures/README.md`. +**Modify:** workspace `Cargo.toml` membership only; keep default members unchanged. Retain `scripts/release/tests/fixtures/README.md` as the migration pointer. + +- [x] Write private Rust tests for duplicate keys at every depth, nonfinite/out-of-range JSON, invalid UTF-8/surrogates/BOM/trailing controls, unsafe names, depth 65, oversized role inputs, negative/noninteger lengths and ambiguous duplicate identities. Include lexical integer overflow in opaque nested fields and top-level values (`18446744073709551616`, `-9223372036854775809`), decimal and exponent float overflow, exact integer boundary controls, and large number-like text inside strings with escaped quotes/backslashes that MUST NOT be rejected as a numeric token. +- [x] Register tests, derive the host target as below, and run `cargo test -p trusted-server-release --target "$HOST_TARGET" documents::tests` with the nonzero-selection guard. Expect compilation/assertion failure for absent behavior, not an empty successful selection. +- [x] Implement bounded byte-first loading, SHA-256 of original bytes, safe flat names, deterministic output and atomic writes. Use a recursive Serde visitor/seed that rejects duplicate object keys **before insertion**, rather than deserializing into `serde_json::Value` first. A representative parser core is: + +```rust +use std::fmt; + +use serde::de::{DeserializeSeed, Error as _, MapAccess, SeqAccess, Visitor}; +use serde_json::{Map, Number, Value}; + +struct StrictSeed { + depth: usize, +} + +impl<'de> DeserializeSeed<'de> for StrictSeed { + type Value = Value; + + fn deserialize(self, deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + deserializer.deserialize_any(self) + } +} + +impl<'de> Visitor<'de> for StrictSeed { + type Value = Value; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("bounded unambiguous JSON") + } + + fn visit_map(self, mut access: A) -> Result + where + A: MapAccess<'de>, + { + if self.depth > 64 { + return Err(A::Error::custom("JSON container depth exceeds 64")); + } + let mut object = Map::new(); + while let Some(key) = access.next_key::()? { + if object.contains_key(&key) { + return Err(A::Error::custom("duplicate JSON key")); + } + let value = access.next_value_seed(StrictSeed { + depth: self.depth + 1, + })?; + object.insert(key, value); + } + Ok(Value::Object(object)) + } + + fn visit_seq(self, mut access: A) -> Result + where + A: SeqAccess<'de>, + { + if self.depth > 64 { + return Err(A::Error::custom("JSON container depth exceeds 64")); + } + let mut values = Vec::new(); + while let Some(value) = access.next_element_seed(StrictSeed { + depth: self.depth + 1, + })? { + values.push(value); + } + Ok(Value::Array(values)) + } + + fn visit_bool(self, value: bool) -> Result { + Ok(Value::Bool(value)) + } + + fn visit_str(self, value: &str) -> Result { + Ok(Value::String(value.to_owned())) + } + + fn visit_string(self, value: String) -> Result { + Ok(Value::String(value)) + } + + fn visit_i64(self, value: i64) -> Result { + Ok(Value::Number(Number::from(value))) + } + + fn visit_u64(self, value: u64) -> Result { + Ok(Value::Number(Number::from(value))) + } + + fn visit_f64(self, value: f64) -> Result + where + E: serde::de::Error, + { + Number::from_f64(value) + .map(Value::Number) + .ok_or_else(|| E::custom("nonfinite JSON number")) + } + + fn visit_unit(self) -> Result { + Ok(Value::Null) + } +} +``` + +The implemented utility chooses the mandatory role byte limit before reading/allocating and reject BOM/invalid encoding. Before `deserialize_any`, validate numeric tokens across the **whole document**, including opaque fields and top-level values, with a string-aware lexical scanner that skips quoted text and handles escapes. Negative integer tokens MUST fit `i64`; nonnegative integer tokens MUST fit `u64`. Decimal/exponent tokens MUST parse as finite `f64`; overflow fails. `serde_json` otherwise converts some overflowing integer tokens to finite floats, so the visitor's finite-float check alone is insufficient. Use the existing scanner in `crates/trusted-server-js/build/inventory.rs` as a behavioral reference without adding a JS-crate dependency. Then start with `StrictSeed { depth: 1 }` and require `serde_json::Deserializer::end()` so grammar/trailing data fails. Closed role validators additionally enforce stricter safe-integer limits and reject negative/noninteger lengths. The representative visitor above is the ambiguity parser, not the complete strict front end; schema validation never substitutes for lexical prevalidation, byte bounds, exact-original hashing or semantic joins. + +- [x] Qualify a maintained Rust schema evaluator. The locked `jsonschema` 0.58.6 is a locally checked evaluator candidate, not an approved production tool pin: use minimal features, explicit dialect and offline construction, with only known retained schemas and no arbitrary schema-file CLI override. Governing schemas require Draft 2020-12 `prefixItems`/`items` positive/negative cases; the official SPDX 2.3 schema uses Draft 7. Disable remote/file retrieval and reject unregistered references. +- [x] Retain the official SPDX schema at source commit `aadf3b0b8dbbabdb4d880b0fc714255fea436ff7`, path `schemas/spdx-schema.json`, 45,312 bytes and SHA-256 `239208b7ac287b3cf5d9a9af23f9d69863971102a5e1587a27a398b43490b89b`. Confirm raw-byte identity and dialect before use. Candidate compatibility or a schema-valid toy document does not establish complete real-artifact coverage. +- [x] Rerun documents/private atomic-write negatives and command-process tests; run host-target clippy/format. Commit only after authorized review with `Add strict Rust release document utilities`. + +### Task 3: Encode cohort, tools and fail-closed release preflight + +**Phase: 2+ (historical destination task).** Phase 1 uses the reduced implementation checklist above, rather than completing this task. + +**Retain:** `scripts/release/release-cohort.json`, `release-tools.json`, `crates/trusted-server-cli/release-policy.json`. +**Create as needed:** release tool `src/policy.rs`, `preflight.rs`, `source.rs` and private registered tests; wire the approved POSIX bootstrap adapter without weakening pending selections. + +- [ ] Port cases for missing/extra artifacts, profiles changed after failure, unknown target/feature, missing pins and every skip value (`""`, `"0"`, `"1"`). Include wrapper, Cargo config, NODE_OPTIONS, native selectors, unexpected ignored/nested validation files, modes/links and source mutation. +- [ ] Run `cargo test -p trusted-server-release --target "$HOST_TARGET" preflight::tests` and `source::tests` separately with the nonzero-selection guard; expect the missing Rust behavior to fail. +- [ ] Preserve exact tracked producer/consumer policy bytes, concrete fixed identity/profile/action/bounds and the raw source-owned tools input. Runtime consumer enforcement remains Tasks 17–20. No downloaded policy override or silent schema/cohort/capability amendment is permitted. +- [ ] Map four asset IDs to fixed package/bin/target/features; never accept JSON commands. Preserve Rust/Node/npm, gh 2.97.0, attest 4.2.2/full SHA and Fastly 15.1.0. Populate binary/image/SDK digests only from authenticated acquisition and Task 8 measured review. The production preflight/selector requires all qualifications by default and MUST fail on pending/null pins; omitting a flag MUST NOT return success. A separately named audit diagnostic MAY describe incomplete inputs but MUST NOT construct authorization state. No JSON qualified boolean or dummy digest is accepted. +- [ ] Audit the actual inherited environment before clearing it; reject skips and unreviewed channels, then build an allowlisted child environment. Do not hide hosted/setup overrides under `env -i`. Accept vetted setup exports such as `RUSTFLAGS`/`VCPKG_INSTALLATION_ROOT` only with explicitly reviewed, qualified values and path identities; otherwise reject. Remove publisher prefixes and inject only approved commit/control facts. Derive epoch from approved Git committer time. Check tracked and unexpected source-only files, including the full JS validation library, modes/links before and after execution; preserve narrow reviewed coordination exceptions. +- [ ] Select generic and target-suffixed AWS-LC settings, bundled SQLite, Cargo config/home layers, sysroot/tool paths and Vite mode. Set `AWS_LC_SYS_USE_SYSTEM=0`, `LIBSQLITE3_SYS_USE_PKG_CONFIG=0`; reject alternate selectors rather than setting weaker defaults. +- [ ] Authenticate approved source and acquisitions into private typed states. Keep independent all-cohort remote qualification receipts separate from current-environment installation identity: rehash the actual selected native binaries/SDK inputs immediately before use. Serialized `authenticated`/`qualified`/`completed` booleans, source labels or checksum files cannot construct either state. +- [ ] Rerun negatives, including fictional operator-config independence and wrong-bootstrap source; run host lint/format. Commit `Define fixed Rust release inputs and preflight policy`. + +**Interface:** after independently approved native bootstrap, `trusted-server-release preflight --cohort scripts/release/release-cohort.json --source-commit --require-qualified`; exit 0 only for a complete selected contract, otherwise fail before preparation/signing. A developer may exercise it with `cargo run -p trusted-server-release --target "$HOST_TARGET" -- preflight --cohort scripts/release/release-cohort.json --source-commit "$APPROVED_SOURCE_COMMIT" --require-qualified`, which provides no official tool or release assurance. + +- [ ] Reject unlisted RC branches and a malicious candidate cohort that lists itself; require independently reviewed tracked branch/governance inclusion, actual remote-tip ancestry and active ruleset/bypass validation. Current eligibility is exactly `["main"]`; reject local remote-ref substitutions. +- [ ] Audit the true Git common directory, including linked worktrees, for alternates, grafts, shallow/replacement/config/environment channels. Explicitly reject unsupported linked indirections rather than partially checking them. Preserve bounded detailed Git errors. Reject an untracked extensionless `rust-toolchain` before bootstrap/rustup selection, including alongside the tracked TOML pin. Join approved source to the original JS inventory commit. + +### Task 4: Reconcile immutable build facts and CLI source resources + +**Phase: 2+ (historical destination task).** Phase 1 uses the reduced implementation checklist above, rather than completing this task. + +**Modify:** `crates/trusted-server-core/build.rs`, `build_support/git_version.rs`, `tests/git_version_resolve.rs`, `src/lib.rs`; `crates/trusted-server-cli/src/run.rs`, `src/prebid_bundle.rs`. +**Create:** core `src/build_info.rs`; CLI `build.rs`, `tests/release_build_facts.rs`. +**Modify when adding the owning facts surface:** `crates/trusted-server-adapter-fastly/src/main.rs`; retain existing version-header/service-version behavior. + +- [x] Add strict full-commit tests: missing/malformed/branch/tag values fail in release mode without local-git fallback; development behavior stays covered. Add two external tags/same source yielding identical facts, owning-target/variant checks and compiled-path fallback rejection. +- [x] Run `cargo test -p trusted-server-core --test git_version_resolve` and `cargo test -p trusted-server-cli --target "$HOST_TARGET" --test release_build_facts`; expect the new behavior to fail. +- [x] Extend the merged #1213 helper using the approved release control. Derive target from Cargo `TARGET` through `TRUSTED_SERVER__BUILD_TARGET`, package version from the owning product, and default/reuse variant at the Fastly owner. Expose typed immutable facts without importing verifier/publisher keys into core. Do not embed tag, run ID, time or final checksum. +- [x] Set clap's version text to `ts package= source= target= variant=default`. Read Fastly owning package/target/variant facts through existing startup diagnostics. Remove unread variant and source-owner re-emissions; core remains the source identity owner. Official releases compile out `find_js_lib_dir`'s literal developer checkout fallback; preserve explicit/current-checkout resource discovery. Keep the tracked example config as a fictional scaffold, never active settings. +- [x] Run core fact/template tests, CLI tests/clippy and Fastly version-header tests. Outside-checkout help/version/config-init MUST pass; Prebid source-dependent commands return documented prerequisites. Commit `Derive immutable release build facts`. + +### Task 5: Authenticate the existing prebuilt JS interface + +**Phase: 2+ (historical destination task).** Phase 1 uses the reduced implementation checklist above, rather than completing this task. + +**Modify:** `crates/trusted-server-js/build.rs`, `build/bundle_set.rs`, `src/lib.rs` test inclusion. +**Create:** `crates/trusted-server-js/tests/release_prebuilt.rs`. + +- [x] Write process/fixture tests for prebuilt mode plus any skip presence, absent inventory, wrong source/digest/size, duplicate IDs, unsafe filenames, missing/empty/extra modules and source changed during copy. +- [x] Run `cargo test -p trusted-server-js --target "$HOST_TARGET" --test release_prebuilt` with the nonzero-selection guard; expect current prebuilt behavior to fail these guards. +- [x] Extend the existing bundle-set validator; require `TRUSTED_SERVER__TSJS_PREBUILT_INVENTORY` in release mode and validate exact input and private copied bytes. Track both paths/control values with Cargo rerun directives. Reject skip before selecting prebuilt/source path; no TSJS_RELEASE_BUILD parallel mode and no read from old `dist/`. +- [x] Preserve ordinary development's supported fresh source build and #1214's inventory-absent prebuilt path, including unrelated non-bundle extras and supported directory aliases. An inventory-present development build validates inventory source syntax/hash but MUST NOT compare it with the override/tag/branch display label. In release Rust jobs require prebuilt inputs and prohibit npm invocation; a fake failing npm proves it is never run. +- [x] Run JS crate tests, Fastly/CLI target checks and prebuilt negatives. Commit `Bind release JS inputs to a checked inventory`. + +### Task 6: Collect JS contributions without changing bundle bytes + +**Phase: 2+ (historical destination task).** Phase 1 uses the reduced implementation checklist above, rather than completing this task. + +**Modify:** `crates/trusted-server-js/lib/build-module-options.mjs`, `build-all.mjs`. +**Create:** `lib/test/release-composition.test.mjs`, `scripts/release/collect-js.mjs`. + +- [x] Write temporary IIFE fixtures for type-only/external Prebid import, eliminated module, transitive npm contributor and plugin/virtual helper. Build with/without recording and assert identical final JS bytes. +- [x] Run `cd crates/trusted-server-js/lib && npx vitest run test/release-composition.test.mjs`; expect missing recording behavior. +- [x] Add an opt-in Rollup contribution hook at `moduleBuildOptions()`. Join source/package origins to the lockfile, retain installed/lifecycle/build-tool materials separately, and hash final files after Vite/esbuild. Suppress ambient `.env` loading through the reviewed release invocation; explicit production mode alone is not sufficient. +- [x] Emit sorted version-1 inventory with source, module names/lengths/digests and origins; ambiguous contributing ownership fails. State conservative post-Rollup reachability limits. +- [x] Run byte-preservation and ownership negatives plus the full JS test/format checks. Commit `Record release JS composition evidence`. + +### Task 7: Prepare and compare two independent JS builds + +**Phase: 2+ (historical destination task).** Phase 1 uses the reduced implementation checklist above, rather than completing this task. + +**Retain/modify:** `scripts/release/build-js.sh`, `bootstrap.sh`, `collect-js.mjs`. **Create:** release tool `src/materials.rs`, `process.rs`, `boundary.rs`, `preparation.rs`, `javascript.rs` and private registered tests; add public process fixtures to `tests/commands.rs`. + +- [x] Write tests for archive/revision mismatch, stale extracted tree, prohibited install download, two bundles differing by one byte, test failure and changed source. Assert no generated output exchange between A/B. Prohibited download and missing-tool denial are actual runtime controls exercised in the full Linux fixture; that fixture also asserts each side exposes only its own private roots and shares only read-only originals and tools. +- [x] Run `cargo test -p trusted-server-release --target "$HOST_TARGET" materials::tests`, `process::tests`, `boundary::tests`, `preparation::tests` and `javascript::tests` separately with the nonzero-selection guard; expect missing Rust orchestration. +- [x] Preparation verifies registry/git/npm materials and required native esbuild package archives, then freshly materializes each installation. Run pinned `npm ci --offline --include=dev --no-audit --no-fund` with verified freshly prepared caches in two independent workspaces under the selected denial boundary; review/record lifecycle scripts and fail any lifecycle tool fetch. Rust migration MUST reprove this with actual full local execution and fresh fixtures; Task 8 MUST qualify the actual hosted JS boundary before Task 15 may use it officially. No restored node_modules/generated JS. Read-only original sources/object stores and authenticated archive/tool inputs remain outside private installation outputs. Timeouts/cancellation/pipe limits kill and reap descendant process groups; bounded Docker stop/remove and all temporary-profile/resource cleanup run on success and failure. Bundle under qualified network denial; compilation-time downloads fail. The passing run used an ordinary-CI Docker denial boundary; qualifying that boundary remains Task 8. +- [x] Compare final core and all tracked integration files byte-for-byte. Retain only A plus its original inventory after equality. Construct a private typed `JsBuildCompleted` only from the actual successful two-install/test/build/control/comparison path, binding authenticated source/tools/settings, inventory raw digest and retained module bytes. Rehash at handoff; a checked inventory or JSON completion assertion cannot substitute. Unconditionally inject `TRUSTED_SERVER__RELEASE_JS_TESTS=1` into BOTH actual A/B Vitest processes, independent of inherited developer flags; run every registered test/type check and reject a zero selected suite; verify source-only trees remained intact. +- [x] Prove a missing prepared tool cannot fetch a substitute; rerun negatives and full JS suite. Require both actual fresh full registered JS/type-check suites and all tracked final bundles before `Build and compare shared Rust release JS inputs`; fixture-only counts remain separate. Landed as `Build and compare two fresh offline JavaScript installations` plus follow-up fixes; Linux CI evidence is in the [two-build evidence report](../../../scripts/release/qualification/2026-10-15-js-two-builds.md). + +- [x] Port actual regressions for immutable macOS source/object regular files with `nlink == 1`; reject pre-existing hard links and prove write-through/restoration cannot bypass the boundary. Restrict signals to `same-sandbox`: own-child positive, outside same-user sibling and orchestrator denial negatives. Parent/child/grandchild install lifecycle, Cargo/compiler/proc-macro/native children MUST inherit the same OS-denied controls. `nlink == 1` scans, pre-existing hard-link rejection, run-time hard-link denial, run-time mutation detection, `same-sandbox` signal positives/negatives, grandchild socket denial and escaped-descendant socket denial pass on macOS 15 CI. A descendant that leaves its process group, even with a scrubbed environment, is identified by its per-run sandbox instance, killed and the run rejected; only processes started during the run are eligible. R-JS uses the single shared Linux JS job, so no macOS install-lifecycle path exists; Task 9 must reprove Cargo/compiler/proc-macro/native child inheritance through its actual recipes. +- [x] Port Docker boundary regressions: use Docker's default private PID namespace (no invalid `--pid=private`), and actually execute positive/denial/recipe controls instead of testing argv alone. Keep BOTH host seccomp profiles and an empty Docker config disjoint from every container-writable root; scan before creating them and rehash BOTH before EVERY host command. Select fixed authenticated Docker client/config/Unix daemon, no TTY/plugins/context or environment overrides, and read-only anchors. Reject escaping and dangling aliases in the CONTAINER namespace. Linux container tests execute actual positive, denial and confined commands in ordinary CI. Cleanup `stop`/`rm` still rehashes every input first: a changed client or config blocks the command, while a changed seccomp profile (which those commands do not read) is recorded as a failure and cleanup proceeds. +- [x] Bound child pipes/deadlines; timeout/cancellation/group kill MUST reap descendants and clear token/provider/injection environment. Record real errno without inventing `ETIMEDOUT`. Use host-independent tool/path/environment fixtures; no ambient shim or interpreter supplies trust. + +### Task 8: Qualify real platform environments before fixing recipes + +**Phase: 2+ (historical destination task).** Phase 1 uses the reduced implementation checklist above, rather than completing this task. + +**Retain:** `scripts/release/qualification/results.schema.json` and reviewed seccomp baseline/license. **Create:** release tool `src/qualification.rs`, native probe dispatch in `src/boundary.rs`, private registered tests; update `release-tools.json` only with authenticated measured pins. + +- [ ] Write schema/preflight fixtures rejecting wrong architecture, missing SDK/tool digest, failed startup, permitted confined sockets and invented host compatibility. +- [ ] Run `cargo test -p trusted-server-release --target "$HOST_TARGET" qualification::tests` with the nonzero-selection guard; expect unimplemented Rust qualification checks. +- [ ] Execute throwaway full recipes on the actual proposed GitHub-hosted environments: digest-pinned GNU builder <=2.34, matching macOS 15 ARM/Intel, explicit loader floor 13.0. Record host, rustup paths, compiler/linker/SDK/native identities. Docker absence on a developer Mac is not replaced by an inferred Linux result. +- [ ] On each recipe run an unconfined socket success control and a confined denied socket probe, then full compilation under the same qualified boundary. Linux container uses process network isolation; macOS profile MUST permit required filesystem/process/tool activity while denying sockets. Archive/tool acquisition is separate and authenticated; actual offline install lifecycle hooks execute under denial. +- [ ] Record actual startup/link evidence and complete Fastly runtime request, not only host health. Missing Intel execution or GNU baseline stops qualification. A different platform/cohort requires reviewed spec changes, not a silent downgrade. +- [ ] Pin authenticated observed digests/bootstrap data and add result validation; commit `Qualify controlled release build environments`. + +**Gate:** This task produces measured pins, not placeholder configuration. It can run only with the separately authorized disposable hosted resources. It does not publish official tags or deploy a real publisher service. + +Implementation sequencing: review the local validation framework and candidate boundaries first, then implement the shared Task 9 recipes and Task 10 composition captures. Complete the authenticated candidate/production adapters and shared-JS producer binding before measuring the executable closure. Hosted qualification MUST invoke those same recipes with independently authenticated candidate selections; it cannot require its own future qualification result. Production selection remains closed until the hosted evidence and measured pins are reviewed. Local framework completion does not satisfy the hosted checkboxes above. + +- [ ] Join observations to the reviewed catalog at the approved source: exact selection digest, tool versions/authentication methods, complete helper compiled closure and every path-dependency manifest, including `crates/trusted-server-openrtb/Cargo.toml`, tool-version files, authenticated executable digests, SDK identities and raw receipts. A code change invalidates the closure; labels/checksums cannot self-qualify. Hash the actual selected shell; do not assume `/bin/sh --version` works on dash. Unsupported pending selections keep production closed. +- [ ] Derive container resource limits no higher than actual host capacity (a private Ubuntu runner may have two vCPUs). A Cargo jobs A=4/B=2 variation does not authorize `--cpus 4` on that host. Use actual controls and whole-product builds, not deleted prototype claims. + +### Task 9: Implement fixed fresh Rust recipes and native inspection + +**Phase: 2+ (historical destination task).** Phase 1 uses the reduced implementation checklist above, rather than completing this task. + +Fastly host-tool selection increment (2026-10-22): the Linux x86_64 candidate +plan explicitly selects `/usr/bin/cc` as its host Rust linker driver and +`HOST_CC`, `/usr/bin/ar` as `HOST_AR`, and the measured Rust tree's +`lib/rustlib/x86_64-unknown-linux-gnu/bin/rust-lld` as the Wasm linker. +The host-triple Cargo linker setting applies to host artifacts during an +explicit-target build; no unstable host configuration or compiler wrapper is +introduced. Host C selectors do not establish target C compilation or prove +that the selected drivers executed. Existing GNU and Apple selections stay +separate. + +The existing staged measurement helper gains a fixed private Fastly mode, +without a new command in `main.rs`. It runs before and after Cargo in the +existing denied sibling sandbox, excluding the writable target root. It checks +the actual Rust host, resolves and hashes selected files inside the image, +and independently hashes the compiler's reported assembler candidate. The +receipt binds the helper, exact plan and build/measurement boundary identities; +changed before/after observations fail. This changes only the opaque +`platform.native_tools` receipt payload, not the checked composition fields or +candidate-index companion formats. Production selection remains closed. + +**Retain/modify:** `scripts/release/build.sh` and approved native bootstrap. **Create:** release tool `src/recipe.rs`, `inspect.rs`, private registered recipe/native/final-Fastly tests and public-command process fixtures. + +- [ ] Test unknown asset/variant, reused target output, source/material mutation, unselected native library, GLIBC symbol ceiling violations, runner-only paths and Mach-O deployment-target mismatch. Add role-specific variation fixtures: missing selected B variation, normalized values misreported as varied, unsupported user variation claimed as passed and incorrect observed concurrency/umask. +- [ ] Run `cargo test -p trusted-server-release --target "$HOST_TARGET" recipe::tests` and `inspect::tests` separately with the nonzero-selection guard; expect missing Rust recipe/inspection behavior. +- [ ] Implement a closed interface: `build.sh --asset --role A|B --source-commit --js-dir --js-inventory --materials --output `. Select package/target/features from reviewed source; JSON never supplies executable commands. +- [ ] Implement checked A/B ambient profiles in the recipe and record them in `release-tools.json`: different external checkout roots, A umask 022/B umask 027, UTC versus a qualified alternate timezone, distinct qualified locale settings, different observed start times and A jobs 4/B jobs 2 within the qualified insensitive envelope. Vary execution user only where Task 8 proves support; otherwise record it unsupported without claiming it changed. Capture both ambient inputs and effective compiler values after sanitation/remapping; fixed internal locale/path/epoch values are normalized, not varied. No unreviewed caller overrides can choose profiles. Tests assert genuinely observed supported differences and reject dishonest/omitted records. +- [ ] Apply Task 8's qualified OS/container denial profile in the actual production `build.sh`, not just the qualification prototype. Every production invocation MUST record a successful unconfined socket control and denied confined socket probe, then execute Cargo, compiler, proc-macro, build-script and native-tool children inside that same denied boundary. Authenticated archive/tool acquisition alone may fetch materials; offline installation lifecycle hooks and all compilation run under denial. A build-script/lifecycle download-attempt fixture MUST fail, and a missing or ineffective confinement proof rejects the output; `--frozen` is not the boundary. +- [ ] Build explicit packages with `--release --frozen --message-format=json-render-diagnostics` after preparation. Keep `debug = 1`. Capture compiler/build-script output separately from diagnostic stderr; copy only final output after any reviewed transformations. Do not invoke Fastly source build/annotation. Never strip after evidence/signing. +- [x] Enforce final-byte path checks inside recipe completion using its own bound host/execution roots, not only candidate fixtures. Require bounded nofollow size/hash verification, reject `/boundary/` and actual build roots, and require a remapped prefix. For GNU builds, close unbound writable `/tmp` and shared-memory storage and reject image-declared volumes. Keep macOS's temporary-path inspection unchanged. Docker execution starts only an acknowledged full container ID and confirms cleanup by that ID. +- [x] Prove the refined GNU boundary with actual denied writes, successful bound-`TMPDIR` write/read/removal, the real timeout-cleanup regression, full fresh JS and Cargo builds, final CLI inspection and all three clean startup images. The observed Chromiumoxide `/tmp/aws/lib` lookup constant is runtime data, not evidence of build identity; its actual ELF context was measured before refining the GNU-only path policy. Passed in ordinary PR CI with identical Linux CLI canonical/reproduction bytes and all three startups; see the [Linux CLI candidate report](../../../scripts/release/qualification/2026-10-16-linux-cli-candidate.md). These ordinary-CI results do not qualify production pins or authenticated acquisition. +- [ ] Inspect ELF/Mach-O libraries and paths, run clean Linux startup fixtures and real ARM/Intel macOS startup outside checkout. With differing fictional operator config removed by preflight, require identical sanitized effective source/tool/build inputs for every artifact and reject embedded operator sentinels/config fallback. Require complete executable equality for Fastly; preserve native nonmatching outcomes as measurements rather than adding a native repeatability shipping gate. Separately show runtime configuration can change without rebuilding. +- [x] Before accepting each final Fastly A/B output, validate actual core-Wasm format with a pinned maintained validator and run a positive publisher-origin response/injection fixture through Viceroy using those exact final bytes. The fixture MUST NOT invoke Cargo/Fastly build or modify the wasm. Assert final hash unchanged, retain runtime outcome, and test non-wasm/component/wrong-ABI/health-only success rejection. These are required producer gates, separate from Task 8 throwaway qualification and later live deployment. Implemented and accepted for both canonical and reproduction outputs in ordinary PR CI, with a closed-origin control rejected; see the [runtime gate report](../../../scripts/release/qualification/2026-10-16-fastly-runtime-gate.md). The Viceroy digest is recorded, not yet independently pinned. +- [ ] Run recipe/native negatives and one actual artifact per selected environment. Commit `Build and inspect canonical release executables`. + +Progress (2026-10-16, no Task 9 exit yet): the closed recipe, checked A/B ambient profiles, role umasks, path translation, source replacement, strict Cargo message selection and `RecipeCompleted` are implemented. Ordinary PR CI built `trusted-server-fastly.wasm` role A through `PreparedRecipe::execute` inside the Linux denial boundary and proved build-script, grandchild and procedural-macro denial; see the [confined Cargo candidate report](../../../scripts/release/qualification/2026-10-16-confined-cargo-candidate.md). That first build used unauthenticated acquisition and `cargo vendor` manifests. The producer now normalizes Git package manifests itself from the verified Git tree; later builds record eight producer-normalized and zero caller-derived manifests, and an evidence-only differential against `cargo vendor` found no differences (see the [producer-normalized manifests report](../../../scripts/release/qualification/2026-10-16-producer-normalized-manifests.md)). Acquisition remains unauthenticated. The plan now remaps the checkout, vendor, Cargo home, Rust tool (sysroot) and target roots to fixed prefixes through one `--config target..rustflags` entry, which merges after reviewed target rustflags (environment rustflags stay rejected, a tracked `build.rustflags`, which target rustflags would supersede, is refused, as is any extensionless `.cargo/config`). The remapped roots must be pairwise disjoint, because rustc applies the last matching prefix, and the remap is bound by the plan digest. On Linux both roles see identical container paths (`/boundary/...`), so the checkout-root variation is host-only; the remap is proven on the final Fastly bytes by a scan that rejects `/boundary/` and every host build root and requires a fixed remapped prefix. Literals built from `env!("OUT_DIR")` or `env!("CARGO_MANIFEST_DIR")` are not remapped by rustc and are caught by the same scan. A pure comparison requires one role A and one role B Fastly build over identical source, lock, materials, JS handoff and tools, an honest ambient pairing and complete final-byte equality, reporting sizes, digests, the first differing offset and its Wasm section otherwise. The ordinary CI job now builds roles A (umask 022, four jobs) and B (test-only alternate profile: umask 027, two jobs, distinct checkout root and start time) from one JS handoff and compares them. Both builds were byte-identical in ordinary PR CI, and the scan found the remapped prefixes and no host or container paths; see the [canonical and reproduction equality report](../../../scripts/release/qualification/2026-10-16-fastly-canonical-reproduction-equality.md). The Linux `ts` candidate now builds through the GNU baseline, passes final-path/ELF inspection and all three clean startup images, and has a byte-identical reproduction; see the [Linux CLI candidate report](../../../scripts/release/qualification/2026-10-16-linux-cli-candidate.md). Viceroy final-byte acceptance is also proven in ordinary CI as recorded above. The two Apple `ts` candidate recipes and native startups have since passed in ordinary CI (runs 39273777887 and 39290139247, recorded below); qualified pins remain open, and production role B stays unqualified. Qualified and production selections stay rejected by the execute gate. Only the `cfg(test)` ordinary-CI candidate selection accepts the macOS boundary, which uses post-run-reaped descendant cleanup (approved 2026-10-19); that is candidate evidence, not Task 8 qualification, and production code cannot name that selection. In code, role A is `Role::Canonical` (Build A) and role B is `Role::Reproduction` (Build B); only `Role::parse` and `Role::spec_label` keep the external `A`/`B` spelling used by `--role` and the tracked `ambient_profiles` keys. + +### Task 10: Record selected Cargo units and native materials + +**Phase: 2+ (historical destination task).** Phase 1 uses the reduced implementation checklist above, rather than completing this task. + +Fastly selected-file evidence (2026-10-22, local implementation; hosted proof +pending): the receipt retains selected and resolved paths, SHA-256 and size for +the host C compiler driver, host archiver, configured host assembler candidate +and selected Wasm Rust linker. The host Rust linker selects the same measured +C driver. SPDX projects these four raw files only as build tools, with no +invented acquisition, download, license or executed-helper claims. The bound +Rust installed-tree measurement remains separate. + +After successful denied measurement, the compiler, archiver and linker +host-path gaps can be removed for this explicitly selected Linux profile. +The assembler is merely compiler-configured, so its role gap remains. Expected +candidate SPDX gaps are exactly `fastly-original-acquisition-unverified`, +`fastly-tool-closure-unmeasured`, `native-material-coverage-incomplete`, +`native-tool-host-path-unresolved:assembler` and +`native-tool-host-path-unresolved:build_tools`. Missing or inconsistent required +measurements fail rather than manufacturing evidence. Other profiles retain +their existing gaps. This does not complete Task 10, original acquisition, +qualification or qualified SPDX generation. The next hosted Fastly candidate +must confirm these gaps and the unchanged real producer-receipt check before +this increment is described as integrated. + +Local verification passed 216 pure tests: 70 SPDX, 68 composition, seven Fastly +tool tests, 18 GNU measurements, 14 recipe-plan, 13 tool-selection, four +source-replacement, 20 completion and two unchanged candidate-index golden +tests. The explicit-selection assertion failed against the previous bare +driver selection and passed after restoring the implementation. Filesystem +tests independently check raw SHA-256/size, changed bytes and escaping aliases; +parser tests reject malformed compiler queries. Strict host-target +all-target/all-feature Clippy, all-feature documentation and formatting passed. +Independent specification and quality reviews found no remaining blockers. +The minimal Fastly expectations in `recipe/tests/plan.rs` were explicitly +authorized; their Linux branch and real boundary execution remain hosted checks. +No process-spawning or ignored tests were run locally. + +Remaining dependency audit (2026-10-22, `41560cfd9`): the open capture exit +below requires conservative native/tool coverage. The specification does not +require an exact compiler DAG or mandatory execution tracing: an inventory of +all permitted relevant inputs, coupled with enforced access containment, can +also establish closure. The current recipes do not establish that containment. +The next implementation must address these dependencies rather than treating +another successful snapshot hash as closure: + +| Remaining evidence | Dependency and current limitation | +| --------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Fastly host tools and supporting inputs | At audit head `41560cfd9`, five host-tool roles were unresolved. The increment above measures selected Linux host compiler/archiver/linkers while retaining assembler, build-tools and full-closure gaps. Other Fastly profiles keep all five role gaps. Supporting inputs still require Task 9/10 closure work; GNU CLI receipts do not cover Fastly automatically. | +| GNU tool/input closure | Task 9 must constrain or account for relevant inputs beyond the inventory roots; Task 10 must inventory and join that closure. `recipe/gnu/image.rs` explicitly leaves other files available. Configured helper paths are observations, not exhaustive dependency coverage. | +| Apple tool/library closure | Task 9 must constrain or account for relevant inputs outside measured directories; Task 10 must measure and join them. `recipe/apple.rs` permits broad developer-directory access. Measuring the selected toolchain's entire `usr/lib` does not resolve outside-directory or transitive dependency inputs. | +| Unreadable GNU privileged helpers | Tasks 9/10 must obtain measured content or establish reviewed, enforced noncontribution/exclusion treatment for the three helpers. Acquisition evidence alone cannot close their content gap. Retain metadata-only observations and the gap until proven; do not infer hashes or expand privileges. | +| Original acquisition | Tasks 3/8 must bind authenticated Rust/tool/SDK/image distributions to the selected inputs. Installed-tree hashes do not authenticate original acquisition; pending pins in `scripts/release/release-tools.json` remain pending. | +| Platform qualification | Task 8 must qualify the actual platform, boundary and recipes against the selected closure. Passing ordinary-CI candidates does not construct the closed qualified tool or sandbox types. | +| Complete owning composition | Task 10 must resolve the first four material-coverage rows before supplying complete composition evidence. Acquisition and production qualification are independent additional requirements; complete measured coverage alone would not open `spdx::generate_from_completed`, which deliberately rejects current candidates. | + +Paths in the table are relative to `crates/trusted-server-release/src/` unless +shown otherwise. Content measurement, dependency closure, authenticated original +acquisition and qualification are distinct proofs. Acquisition and qualification +rows describe production dependencies, not prerequisites for measuring complete +material coverage. None is substituted for another, and this audit changes no +completeness or qualification state. + +**Create:** release tool `src/composition.rs`, private `composition::tests`, `crates/trusted-server-release/tests/fixtures/composition/`. + +- [x] Add fixtures for host/target dual-role packages with different unit features, ambiguous source IDs, dev-only graph entries, unknown native origin, system substitution and prepared-but-discarded CLI JS. +- [x] Run `cargo test -p trusted-server-release --target "$HOST_TARGET" --bin trusted-server-release composition::` with the nonzero-selection guard; the scoped 2026-10-21 run selected and passed 63 pure tests. +- [x] Join explicit-target compiler/build-script messages with metadata/tree and selected material identities; preserve each observed unit's features/context. Package union features cannot stand in for effective per-unit features. Retain raw observations for audit; do not claim an exact compiler dependency DAG. +- [ ] Capture AWS-LC/ring/SQLite native source and output archive hashes/link directives, Rust sysroot and external platform libraries. Validate generic/target-specific selection against the recipe. Distinguish embedded components, external requirements and build tools. +- [x] Inspect final files conservatively for complete JS module strings. Record absence explicitly; Fastly and CLI do not need the same retained set. Do not force unused JS into CLI. + +Scoped audit and hardening (2026-10-21): the checked items above describe existing +implemented collectors, not newly established full coverage. Named fixtures in +`composition/tests.rs`, `graph/tests.rs`, `native/tests.rs` and +`javascript/tests.rs` already exercise the listed cases. The dual-role feature +cases are synthetic; retained real cross-build fixtures establish host-only +membership, not a captured dual-role build or exact compiler dependency DAG. +AWS-LC/ring/SQLite observations already bind verified original distributions, +prepared source/subtrees, consuming contexts/features, static output archives +and link directives. Rust installed-tree measurements and logical external +runtime requirements remain distinct from original acquisition and complete +native/tool coverage; that capture exit stays unchecked. + +A new negative fixture replaces a native archive between checking its magic and +hashing its path. The previous implementation accepted a valid header from one +inode and a digest from a different non-archive file. The collector now hashes +the checked header and remaining bytes through the same held descriptor, with +bounded reads, owning-attempt checks and matching final descriptor/path metadata. +No reported path or digest becomes trusted without local measurement. + +GNU receipts already record incomplete closure and unqualified acquisition. +Candidate SPDX now names both limitations as `gnu-tool-closure-unmeasured` and +`gnu-original-acquisition-unverified`, independently of the existing +`gnu-privileged-helper-content-unmeasured` gap. These names are absent for +non-GNU receipts and remain sorted/unique. No serialized receipt fields, schemas, +workflow assertions, completeness flags or production constructors changed. +Hosted Fastly/Linux/Apple candidate validation and emitted-gap inspection are +recorded below for `e91d75b74`. Complete coverage and qualification stay +false; this does not complete Task 10 or touch the parallel Task 12 adapter. + +Local verification for this increment passed 63 composition tests with the +nonzero-selection guard and 62 SPDX tests, strict native all-target/all-feature +Clippy, documentation generation and formatting. The inode-swap regression and +each new GNU gap assertion failed before its corresponding fix and passed after. +Independent spec and quality review approved the scoped changes; plan wording +was corrected to distinguish the implemented collector from the historical +missing-collector test step. No child-process or ignored tests were run locally. + +Hosted gap follow-up (2026-10-21): [run 39565110219](https://github.com/IABTechLab/trusted-server/actions/runs/39565110219) +has head `7ab7f96e71c896b214e6d09e760b7da7ea22d1b4`. Completed candidate logs +for Fastly [119105403584](https://github.com/IABTechLab/trusted-server/actions/runs/39565110219/job/119105403584), +Linux CLI [119105404005](https://github.com/IABTechLab/trusted-server/actions/runs/39565110219/job/119105404005) +and Apple ARM [119111703005](https://github.com/IABTechLab/trusted-server/actions/runs/39565110219/job/119111703005) +record built PR merge source `f5bac2786585169cf597139069d05dd47513d9f1` and +`material_native_coverage_complete: false`. Linux alone reports both +`gnu-tool-closure-unmeasured` and `gnu-original-acquisition-unverified`, alongside +its existing privileged-helper and incomplete-native-material gaps. Fastly and +ARM report neither new GNU gap; their existing host-tool/Apple exclusions remain. +At this evidence check Intel [119111702865](https://github.com/IABTechLab/trusted-server/actions/runs/39565110219/job/119111702865) +was still running its denial fixture. Its final candidate gap list and the overall +run result remain pending; no all-four confirmation is inferred. + +Intel follow-up (2026-10-21): successful [run 39577910126](https://github.com/IABTechLab/trusted-server/actions/runs/39577910126), +head `d774ee47c274257fa0a088280f7df37ed530020c` (including `04be89638`), +completed Intel candidate [119154065062](https://github.com/IABTechLab/trusted-server/actions/runs/39577910126/job/119154065062). +Its built PR merge source is `fb42d8268af93c011959d032c81bfa581306c7c2`. +The final candidate SPDX reports only `apple-coverage-excluded:toolchain_libraries`, +`apple-tool-closure-unmeasured` and `native-material-coverage-incomplete`; +neither GNU closure/acquisition gap appears, and +`material_native_coverage_complete` and `release_qualification` remain false. +Together with the three completed logs above, this closes the four-platform +gap-placement follow-up across the recorded runs, not complete native coverage. + +Progress (2026-10-19, partial Task 10 only): candidate recipes retain an unfiltered Cargo metadata identity catalog and run an explicit-target tree query under the same denied boundary and selected tool/environment/source replacements as the build. Completion joins actual compiler messages to exact metadata package/target identities, preserves observed host/target unit features and build-script outputs, and requires the selected final executable to appear exactly once. Raw metadata/tree and normalized observations, including query plan digests, are retained privately and rehashed at handoff; candidate measurements keep `material_native_coverage_complete: false`. Ordinary CI at `456d15005` passed Fastly A/B equality and final-byte Viceroy acceptance with 255 observed packages, 377 units and 39 build-script executions per side, and Linux CLI capture/startup with 486 packages, 649 units and 62 build-script executions. Those runs precede the new component receipts below; they do not validate their full-product integration (later runs 39273777887, 39290139247 and 39417433680 do, as an ordinary-CI candidate). Apple full-product acceptance has since passed in ordinary CI (runs 39273777887 and 39290139247, recorded under Task 9 and below). + +The candidate completion path now also reconciles normal/build metadata and tree edges against observed compiler membership, rejects ambiguous source joins and dev-only paths, and keeps potential graph membership separate from actual compiled units. It does not claim an exact compiler unit dependency DAG. Verified package origins join opaque compiled IDs to approved workspace manifests or original registry/Git materials. Reviewed AWS-LC, ring and bundled SQLite collectors bind original source, prepared source trees, static output archives, observed features and link directives; unknown native contributors and system substitutions fail. Final-byte JS checks classify complete retained modules conservatively: Fastly requires every prepared module, while CLI absence remains build-material evidence. Bound source checks cover every tracked input before/after execution and immediately before sealing. Native working inputs are rehashed at sealing; retained immutable receipt bytes are verified before cleanup and at subsequent handoffs. No retained-input archive is implied. Tool/sysroot/SDK/external-requirement coverage, all-cohort real component validation and qualification remain open, so these components do not complete Task 10. + +The verified vendoring path records a read-only package index on `PreparedCargoMaterials`, keyed by vendor directory. Registry entries retain the locked source, original archive SHA-256 and prepared manifest SHA-256; Git entries additionally retain the complete selector/revision, repository/package trees, package path, committed manifest blob/SHA-256 and any producer-normalized manifest SHA-256. Entries are constructed only after package writing succeeds. The origin join selects only observed packages from this larger prepared-material index; preparation alone never establishes contributor membership. + +A read-only full-workspace graph check caught and fixed a real input-size mismatch: the CLI's `--no-dedupe` tree contains 163,493 occurrences, exceeding the separate 65,536 compiler-message limit. Trees now have their own 1,048,576-occurrence bound within the unchanged 16 MiB byte bound. An indexed name/version lookup preserves conservative ambiguity rejection. The actual Fastly and Linux CLI potential trees parse successfully; this parser check uses synthetic selected-root membership and does not establish actual full-product compiled membership for the new receipts. + +Candidate platform receipts additionally retain the measured installed Rust tree digest and explicit Cargo/compiler/selected target standard-library roles. Typed Apple exports retain the measured clang, archiver, linker and SDK-settings identities and versions, with explicit exclusions for SDK headers, stubs, frameworks, helper tools, libLTO and compiler resources (narrowed for measured selections; see the 2026-10-21 Apple toolchain note below). Accepted ELF/Mach-O/Wasm facts supply artifact-bound observed runtime requirements; these are logical dependencies, not installed runtime-library hashes or provenance. The collector independently checks artifact, target and Apple SDK agreement, compares the retained Rust digest with its bound identity, and rechecks inputs before retaining evidence. Linux host-PATH native tools remain unresolved, original acquisition is not inferred, and coverage remains incomplete. The component/integration verification batch passes 252 focused tests, native and Linux cross-target Clippy, formatting and documentation generation; independent review passed. All four candidate cohorts have since executed these receipts in ordinary CI (runs 39273777887 and 39290139247, recorded below); qualification remains pending. + +Ordinary CI at `3fef2c5f0` subsequently passed the complete Fastly composition/platform receipt path, A/B byte equality and Viceroy acceptance. Its Linux CLI candidate stopped at a generic native-feature rejection. Investigation found a concrete mismatch with the locked AWS sources and resolved CLI feature graph: `all-bindings` is optional, and the selected `prebuilt-nasm` feature cannot activate prebuilt objects on the selected Linux/macOS platforms. The narrowly corrected policy accepts these reviewed profiles while retaining original-source, archive, environment and static-link checks. New regressions cover the profiles and bounded package/context/feature diagnostics. Full native candidate reruns remain required; the original generic error did not identify the exact failing unit. + +The corrected receipt path subsequently passed all four ordinary-CI candidate recipes in [run 39273777887](https://github.com/IABTechLab/trusted-server/actions/runs/39273777887), over merge source `cace4c508159bc7502d359e0cce49fef47d61f8b`. Fastly, Linux CLI and Apple ARM completed byte-identical A/B builds and their required runtime checks. Intel completed its mandatory canonical build, inspections and native startup; its optional reproduction exhausted the confined 25-minute wall budget while compiling and was recorded as a measurement-only build failure. No Intel equality result is inferred. These are candidate integration results for that source, not authenticated acquisition, complete composition coverage or qualification of subsequent changes. + +Typed owning access now rechecks retained observations and requires sealed component receipts before exposing observed units, verified package origins and potential graph facts. The typed view borrows its owner and exact normalized-receipt digest; it changes no raw receipt bytes and grants no completeness capability. Its real recipe checks run after source/target cleanup. Selected SDK-content enforcement now stages a separately owned copy with independent regular files, preserves safe internal aliases and modes, binds a structured-tree digest into Apple identity, and selects that copy through explicit C/Rust sysroot arguments, immutable boundary roots, remaps and final-byte path rejection. The [SDK input audit and smoke](../../../scripts/release/qualification/2026-10-19-apple-sdk-inputs.md) records the local installed-SDK pass and subsequent hosted ARM/Intel candidate acceptance. Compiler/helper/sysroot and original-acquisition closure remain separate work. + +Hosted follow-up (2026-10-20): [run 39290139247](https://github.com/IABTechLab/trusted-server/actions/runs/39290139247) finalized success for head `df4a6e5c30fe8c93df68748ead77a3bc559d16b2`, building merge source `506d8ead69757556e1bd8ed29b578d0257d05169`. All four actual candidate fixtures accepted sealed typed composition, verified Rust-package SPDX projection and retained candidate exports that survive build-root cleanup. Fastly, Linux CLI and Apple ARM reproduced identically; Fastly also passed final-byte Viceroy acceptance. Intel canonical inspection/startup/export passed, while optional reproduction failed on its time bound, so no Intel equality evidence exists. Observed package/unit/build-script counts were Fastly `255/377/39`, Linux and ARM `486/649/62`, and Intel canonical `487/650/62`. Both Apple recipes used the detached SDK. This closes the previously pending hosted integration checks for these specific increments; full native/tool closure, complete SPDX generation, authenticated acquisition and release qualification remain open. The receipts retain both completeness and qualification as false. + +Apple and Intel candidate details (2026-10-20, verified from job logs; see the [Apple CLI candidates report](../../../scripts/release/qualification/2026-10-20-apple-cli-candidates.md)): in both runs the ARM recipe built byte-identical canonical and reproduction executables and every canonical Apple build, inspection and native startup passed. The Intel optional reproduction failed in both runs, for different recorded reasons: run 39273777887 exceeded the confined build wall-clock deadline while compiling, and run 39290139247 recorded "source preparation cancelled or timed out". The later runs 39417433680 (`bfab8ef20`) and 39420011152 (`1719cf15f`) recorded Intel reproduction `identical`, with the Intel candidate test taking 3,552 and 3,305 seconds against its 60-minute test deadline. Because the timing margin is thin and no code change addressed it, this is a measurement, not a resolved Intel equality gate; native B stays optional. All of this is ordinary-CI candidate evidence with `release_qualification: false`. The macOS boundary reports `descendant_cleanup: "post-run-reaped"`, which only the `cfg(test)` ordinary-CI selection accepts. + +The subsequent GNU increment replaces ambient compiler selection for the Linux CLI candidate with source-owned `/usr/bin/gcc` and `/usr/bin/ar` selectors for both host and target C builds, an explicit Rust linker driver, and exact AWS-LC cc-backend controls that prevent fallback to CMake. A staged Rust helper runs inside the same denied Docker boundary before and after Cargo. It records selected/configured compiler, assembler, archiver, linker and compiler-resource identities plus a bounded conservative installed-image file inventory, including headers/libraries and reviewed loader/alternatives configuration. Image hardlinks use a separate held-descriptor policy; strict owned-tree and host-document policies remain unchanged. The receipt binds the helper, boundary, profile and invocation, rejects drift, and remains retained through composition sealing. Installed-file coverage does not establish which helpers executed, authenticated acquisition, or complete native closure. Those flags remain false, and the actual GNU helper/profile integration has since run in hosted CI, passing at `bfab8ef20` after two fixes (see the GNU snapshot and measurement notes below). + +GNU snapshot of reviewed root-only helpers (2026-10-20, `e5a780d5c`): the hosted Linux CLI candidate at `7be86d935` failed with "cannot open image file: Permission denied" (job 118460869596, run 39371724671), because the unprivileged helper cannot open three root-only privileged helpers in the UBI 9 builder. The snapshot now accepts a permission-denied file only at the exact reviewed paths `/usr/libexec/utempter/utempter` (setgid), `/usr/sbin/unix_update` and `/usr/sbin/userhelper` (setuid), owned by root with no group/other write and no other-read bit and unchanged since first inspection. Each is recorded by mode, owner and size rather than content and listed as `content_unmeasured_files`, which must be a sorted subset of the reviewed paths. Every other unreadable file, including an execute-only compiler helper, still fails closed, and selected build tools must stay readable and hashed. This records a coverage gap; it measures no helper content and qualifies nothing. + +GNU measurement in a sibling sandbox (2026-10-20, `bfab8ef20`): the next hosted run, at `e5a780d5c`, failed because the post-build image measurement ran in the build boundary, whose unchanged pre-run writable scan rejects Cargo hard links inside the target directory ("has 2 hard links; a link outside the root would bypass write denial", job 118494982595, run 39382624785). Both GNU measurements now run in a sibling Linux container boundary derived from the build boundary with the same image, settings, read-only and tool roots and every writable root except the target directory. The sibling gets a fresh fingerprint, records the boundary it derived from and proves denial with the staged probe. Cargo queries and the build keep the build boundary and proof; receipts record both fingerprints. Ordinary-CI run 39417433680 (head `bfab8ef20`) passed the Linux CLI candidate (job 118609761876): byte-identical reproduction and three clean startups, recorded in the [GNU follow-up report](../../../scripts/release/qualification/2026-10-20-linux-cli-gnu-follow-up.md). This is candidate evidence only. + +Apple toolchain identity (2026-10-21, uncommitted, Tasks 9/10 candidate only): the ordinary-CI Apple selection now also measures, in place below the selected `clang`'s toolchain `usr` directory, a structured-tree digest of the helper directory beside `clang` (files, modes and relative alias targets), the single versioned clang resource directory (`lib/clang/`; any second real version directory, foreign alias or other entry fails closed) and the SHA-256 of `lib/libLTO.dylib` (missing, aliased or non-regular fails closed). The trees are bounded by entry, file and total byte limits and reject special files and absolute, dangling or escaping aliases. Vendor hard links are accepted because the tree is not owned; every file is rehashed instead. Nothing is copied, executed or resolved through `xcrun`. The receipt is part of the Apple identity record and therefore the plan digest, and it is rechecked at every Apple verification point and at evidence sealing. Candidate exports drop the `tool_helpers`, `liblto` and `clang_resource_directory` exclusions only when the receipt is present, and add a `toolchain_libraries` exclusion for other toolchain libraries such as `libtapi`. The `apple-tool-closure-unmeasured` gap stays: which helpers clang actually ran is unmeasured, and the developer directory remains a broad read/execute system root. The platform SPDX projection adds `libLTO` and the two trees as components. Pure fictional fixtures cover this locally; the real Xcode layout is exercised only by the ordinary-CI macOS jobs, which have not yet run it. It grants no qualification. + +Apple measurement follow-up (2026-10-21): the earlier uncommitted/unexecuted +status above is superseded by committed `7d045ac83` and the successful ARM/Intel +candidate jobs in run 39540057202. Their candidate entries retain +`apple-coverage-excluded:toolchain_libraries` and `apple-tool-closure-unmeasured`; +passing the measurement path does not establish complete closure or acquisition +authenticity. See the [candidate index report](../../../scripts/release/qualification/2026-10-21-candidate-release-index.md). + +Timing follow-up (2026-10-21): the Intel optional reproduction timeout recorded +in run 39273777887 recurred in run 39540057202, after historical successful +Intel comparisons. The ordinary-CI selection now supplies a 45-minute confined +Cargo wall through `ToolSelection::build_wall` (`b4c8e0c0f`), rather than the fixed +25 minutes. See the [schedule/cache report](../../../scripts/release/qualification/2026-10-21-native-reproduction-schedule.md). +The running speedup CI is not yet a completed acceptance result; neither deferral +nor the changed budget establishes repeatability, complete coverage or qualification. + +GNU receipt follow-up (2026-10-21): capture now validates the exact nine selected +file roles and eight compiler-query identities, not only their counts. Selected +drivers match the fixed profile; configured cc1, collect2, assembler, linker and +compiler-runtime files join their recorded query paths. The raw sysroot response +is rechecked against the supported image-root forms. Bare assembler/linker names +must match the fixed image candidate's resolved path, digest and size; `/bin` +aliases of the same file and explicit alternative image-helper paths remain +supported. These are installed/configured observations, not execution tracing or +general PATH-resolution proof; tools-root helpers remain outside this profile. + +The audit found existing AWS-LC/ring/SQLite collection already retains original +registry archive identities, prepared source/subtree hashes, static-link +directives and rehashed output archives. Rust tool trees, measured Apple tools/ +SDK inputs and GNU installed/configured files already project into SPDX. No new +native source fact, external-library identity or authenticated acquisition is +invented by strengthening the receipt checks. Native material coverage stays +incomplete. The existing Fastly unresolved-host-tool, GNU closure/acquisition/ +privileged-helper-content and Apple closure/toolchain-library gaps remain open; +complete sysroot/tool execution closure and external platform inputs still need +additional owner measurements and qualification. Hosted acceptance of this +guard is recorded below; none of these changes completes Task 10. + +Scoped verification: nonzero guards selected 18 pure GNU tests, 67 pure SPDX +tests and two unchanged producer golden checks; all passed. Strict host clippy, +documentation and owned-file formatting pass. Independent spec and quality +reviews approve this increment without blockers, including preservation of +explicit GCC paths and same-identity aliases. No child-process or ignored tests +were executed locally. The earlier Task 14 run is not evidence for this guard. + +Hosted guard acceptance (2026-10-21): [Run Tests 39616416485](https://github.com/IABTechLab/trusted-server/actions/runs/39616416485) +and [Run Format 39616416604](https://github.com/IABTechLab/trusted-server/actions/runs/39616416604) +succeeded at head `e91d75b7447c9611d6e1ccb5dc7c2dce07377ff6`. Candidate +logs record built PR merge source `624cef618ffadbfad72c41c9072d8c5f5c1f4f64`. +Actual SPDX gaps were inspected for all four assets: + +- Fastly [119278571153](https://github.com/IABTechLab/trusted-server/actions/runs/39616416485/job/119278571153): + `native-material-coverage-incomplete` and `native-tool-host-path-unresolved:` + for `archiver`, `assembler`, `build_tools`, `compiler` and `linker`. + Canonical and reproduction SPDX report the same gaps and identical document + bytes; document equality remains a measurement. +- Linux CLI [119278571303](https://github.com/IABTechLab/trusted-server/actions/runs/39616416485/job/119278571303): + `gnu-original-acquisition-unverified`, `gnu-privileged-helper-content-unmeasured`, + `gnu-tool-closure-unmeasured` and `native-material-coverage-incomplete`. +- Apple ARM [119287733613](https://github.com/IABTechLab/trusted-server/actions/runs/39616416485/job/119287733613) + and Intel [119287733492](https://github.com/IABTechLab/trusted-server/actions/runs/39616416485/job/119287733492): + `apple-coverage-excluded:toolchain_libraries`, `apple-tool-closure-unmeasured` + and `native-material-coverage-incomplete` on each asset. + +Every candidate keeps `material_native_coverage_complete: false` and SPDX +`release_qualification: false`; optional native reproductions are absent, not +invented successes. No unexpected gap difference was found. This closes hosted +acceptance of `e91d75b74`, not Task 10/11 completeness. That run predates +`5a98c18ad`'s strict real-receipt parser; parser-inclusive acceptance is recorded +in the 2026-10-22 note below. + +Cargo script presence hardening (2026-10-21): graph sealing now requires every +observed custom-build package to have execution evidence, and the execution +package/context set to match the observed non-custom consumers of those +packages. A compiled host script cannot substitute for its target execution; +dual-role packages need evidence in both consuming contexts. Multiple executions +within one context remain permitted, and effective unit features remain separate. +This is package/context presence consistency, not proof that every feature-specific +execution within a context was retained or an exact compiler dependency DAG. +No serialized receipt field, SPDX rendering, completeness flag or collector +fixture changes. Coverage remains incomplete. + +Two negative fixtures were observed failing for their intended reasons before +the production checks: missing execution evidence and a host execution with only +a target consumer. A dual-role regression also covers a missing second context, +matching host/target executions and multiple executions in one context. Scoped +nonzero guards passed 66 composition tests, 67 SPDX tests, 18 GNU tests, 46 +collection tests and two unchanged producer golden checks. The collection +transfer module was excluded because it includes subprocess/network fixtures; +one other ignored collection test was not executed. Strict host clippy, +documentation and owned-file formatting passed. Independent review found no +blocker and confirmed the package/context limitation stated above. Real-build +acceptance of this graph guard is recorded in the 2026-10-22 note below. + +Selected Apple library measurement (2026-10-21): the in-place toolchain receipt +now records the SHA-256 of the exact `usr/lib/libtapi.dylib` when present, +alongside the existing `libLTO`, helper and resource measurements. A missing +leaf is recorded explicitly as `libtapi_sha256: null`; unreadable files, aliases, +special files and exhausted bounds are errors. Present files use the existing +no-follow descriptor/stamp checks, a 128 MiB limit and the attempt's cancellation +and deadline. Remeasurement rejects content changes, appearance and disappearance. +The new field is inside the collector's opaque `platform.apple.toolchain` +subtree; all strictly checked receipt fields remain unchanged. + +This measures one selected toolchain library, not its execution or loading. +Other toolchain libraries, runtime inputs, original acquisition and the full +tool closure remain unresolved. The existing `toolchain_libraries` exclusion, +`apple-tool-closure-unmeasured` gap and incomplete coverage flags are retained. +Hosted acceptance of this additional measurement is recorded below for +`39639053390`; run `39637275502` contains the preceding graph checks only. + +Verification of this increment selected and passed 66 composition, 68 SPDX, +19 Apple toolchain, 18 GNU, 46 collection and two unchanged producer golden +tests with nonzero guards. The collection transfer module and ignored tests +were excluded under the local execution restrictions. Strict host clippy, +documentation and owned-file formatting passed. Independent spec and code +quality reviews approved the increment. Review also strengthened the absence +check: a missing or aliased parent cannot be recorded as a missing library. +The existing vendor-tree intermediate-directory swap limitation still applies; +no stronger isolation guarantee is claimed. + +Hosted receipt acceptance (verified 2026-10-22): [Run Tests 39637275502](https://github.com/IABTechLab/trusted-server/actions/runs/39637275502) +succeeded at head `416b43f43c5d7e69c08bca32753240abded8523c`, building PR +merge source `17251a872e295e4f13047efd14b10077944620fb`. [Run Tests 39639053390](https://github.com/IABTechLab/trusted-server/actions/runs/39639053390) +succeeded at head `b6f5d92e4bc4483f81b1e0d8a376e7d67c9da8fd`, building PR +merge source `c7b1a9cb04ab8abf8e4fa2d5198c4c1337a96b1f`. Format, +Integration Tests and CodeQL also succeeded at both heads. All four actual +candidate fixtures selected and passed one test each in both runs. Each invokes +the unconditional real-receipt check in `check_owned_export_after_cleanup`; +none of the eight job logs contains the receipt-check panic. This establishes +producer/collector receipt acceptance for these builds, not complete coverage. + +The full job logs were inspected for exact candidate SPDX gap lists: + +- Fastly: [416 job 119351108924](https://github.com/IABTechLab/trusted-server/actions/runs/39637275502/job/119351108924) + and [b6f job 119357467783](https://github.com/IABTechLab/trusted-server/actions/runs/39639053390/job/119357467783). + Both roles report `native-material-coverage-incomplete` and + `native-tool-host-path-unresolved:` for `archiver`, `assembler`, `build_tools`, + `compiler` and `linker`. +- Linux CLI: [416 job 119351108938](https://github.com/IABTechLab/trusted-server/actions/runs/39637275502/job/119351108938) + and [b6f job 119357467960](https://github.com/IABTechLab/trusted-server/actions/runs/39639053390/job/119357467960). + Exactly `gnu-original-acquisition-unverified`, + `gnu-privileged-helper-content-unmeasured`, `gnu-tool-closure-unmeasured` + and `native-material-coverage-incomplete`. +- Apple ARM: [416 job 119360930044](https://github.com/IABTechLab/trusted-server/actions/runs/39637275502/job/119360930044) + and [b6f job 119367262345](https://github.com/IABTechLab/trusted-server/actions/runs/39639053390/job/119367262345). + Apple Intel: [416 job 119360930038](https://github.com/IABTechLab/trusted-server/actions/runs/39637275502/job/119360930038) + and [b6f job 119367262261](https://github.com/IABTechLab/trusted-server/actions/runs/39639053390/job/119367262261). + Each reports exactly `apple-coverage-excluded:toolchain_libraries`, + `apple-tool-closure-unmeasured` and `native-material-coverage-incomplete`. + +No GNU gaps leak onto Fastly or Apple. Every candidate retains +`material_native_coverage_complete: false` and SPDX `release_qualification: false`. +Fastly canonical/reproduction SPDX documents are byte-identical in each run; +SPDX equality remains measurement-only. Native reproductions are deferred and +their SPDX comparisons remain null. Both Apple jobs in the `b6f` run record a +non-null `apple_developer_tools.toolchain.libtapi_sha256`, confirming the new +measurement ran on both architectures. The unqualified four-asset candidate +index job also succeeded in the `b6f` run. No collector edits or field-mismatch +workaround was needed. Tasks 10/11 remain partial. + +Selected Apple library-directory inventory (2026-10-22): extend the in-place +toolchain receipt to the entire selected `usr/lib` directory. A read-only +metadata inventory of local Xcode 16.4 (the version recorded by both hosted +Apple candidates) found 6,205 entries, 2,601,020,072 regular-file bytes, a largest +file of 354,743,440 bytes and 41 contained relative aliases. This establishes +local sizing only, not hosted performance or qualification. + +The existing vendor-tree collector measures every file, directory, mode and +alias target under that root with its own `toolchain-library-directory` digest +domain. Limits are 512 MiB per file, 4 GiB per tree and 20,000 entries. Reads +retain the existing cancellation/deadline, no-follow, stable-descriptor and +alias-containment checks. The receipt's new `library_tree` stays inside opaque +`platform.apple.toolchain`; strictly checked collector fields are unchanged. +Every verification rehashes the tree: prior metadata equality cannot substitute +for fresh content measurement. No digest cache or extra attempt time is added. + +The `toolchain_libraries` exclusion now denotes unresolved dependency/loading +coverage and libraries outside the measured selected directories. It does not +mean the selected `usr/lib` files are unhashed. Host runtime, dynamic loading, +original acquisition and full closure remain unresolved, so no completeness +or qualification flag changes. This inventory deliberately overlaps the +separate LTO/TAPI/resource identities and proves neither which files the tools +read nor which bytes contributed to the final artifact. Its extra hashing cost +must be accepted by a later hosted run under the existing attempt budgets; +the earlier green runs do not validate this increment. + +Local verification passed 66 composition, 68 SPDX, 25 Apple toolchain, 18 GNU, +46 collection and two unchanged producer golden tests, each with a nonzero +selection guard. The collection transfer module and ignored tests were excluded +under the local execution restrictions. The added-library regression first +failed against the previous collector and then passed with the full inventory. +Fixtures also cover nested entries, edits/removals, alias targets, unsafe files, +limits, cancellation, expiry and structured SPDX rendering. Strict host clippy, +documentation and owned-file formatting passed. Spec review corrected two stale +comments so exclusions describe unresolved coverage rather than claiming the +newly inventoried files are unhashed. Independent code quality review found no +blocking findings and confirmed that hosted read-cost acceptance is still needed. + +Main-merge candidate compatibility (2026-10-22): main `43a25619a` upgrades Rust +to 1.98.1, Node to 24.20.0, Viceroy to 0.21.0 and `libsqlite3-sys` to 0.38.2. +Run `39716862538` failed before the real candidates could complete: three +preparation jobs rejected the old Node pin, nine npm-observer tests rejected +the old Node/npm pins, and both native release-tool suites detected the stale +Viceroy pin. These failures provide no acceptance of the library-directory +inventory above. + +Align the pending compiled/JSON tool and recipe pins with main, including npm +11.19.0 observed in the source-selected Node distribution. Original acquisition, +digests and qualification remain pending/null. The JS execution/settings checks, +fixture preparation and paired producer/collector Viceroy checks remain exact +version checks; no range, bypass or fallback is introduced. The user authorized +the minimal collector version check and fixture update. Receipt fields, nullable +members and enum tags are unchanged, and candidate index goldens are untouched. + +Review of the new SQLite wrapper confirms the bundled `sqlite3` archive and +source subtree remain the same selection, with default feature +`min_sqlite_version_3_34_1` replacing `min_sqlite_version_3_14_0`. Update only its +reviewed wrapper version/features; AWS-LC 0.42.0 and ring 0.17.14 stay fixed. +The native implementation version and native license are not inferred from the +wrapper version. Source-lock and source-tool-pin regressions first fail against +the stale rules, then guard the aligned cohort. + +Source review of npm 11.19.0 found the intercepted lifecycle entry points and +spawn adapter byte-identical to npm 11.6.2; the new script-policy defaults leave +the current reviewed hooks enabled. Do not add an `--allow-scripts` override. +Actual lifecycle, runtime, native-source and receipt acceptance require a new +hosted run; no local process-spawning tests are authorized by this migration. + +Migration verification: 370 selected pure tests pass, including unchanged +candidate-index goldens and rejection of the previous Viceroy version. Strict +host-target clippy, documentation, read-only workspace rustfmt, changed-file +Prettier, JavaScript syntax and diff checks pass. Independent specification/security +and code-quality reviews found no blocking findings. Hosted validation remains +pending for the actual npm lifecycle, four candidate builds and their receipts. + +Git dependency compatibility (2026-10-22): run `39720199554` at `0bf452370` +passes ordinary/native suites and the Linux JS handoff, but candidate jobs +`119631656292`, `119631656310` and `119635856319` reject the Git-sourced `esi` +dependency introduced by main. Its locked commit contains package-local LICENSE +and README.md links to regular files in the repository parent. Both the original +Git closure parser and the package vendorer previously rejected mode `120000`. + +Retain canonical link blobs as inert authenticated objects in the complete +locked commit closure. During package vendoring, resolve only direct relative +file links through the same repository tree, rehashing every traversed object +and the target under existing budgets. Copy target bytes and executable mode +into regular files through the existing checksum writer; never create or follow +filesystem links. Reject absolute/escaping targets, malformed or oversize text, +missing targets, directory or chained aliases, aliased manifests and submodules. +Reject parent traversal after descending into a target component rather than +collapsing paths with potentially different link semantics. This changes no +receipt fields, origin identities, acquisition authority or qualification claims. + +The positive pure vendoring regression first fails at the original unsupported +mode check, then passes with authenticated parent-file bytes. Pure regressions +also cover unsafe targets, manifest aliases, target executable mode and object +tampering. Full original-object collection and actual four-candidate builds +remain hosted checks; local process-spawning tests are not run. + +Local verification selects and passes 186 pure tests: 42 vendoring, 67 +composition, 68 SPDX, two candidate-index goldens, four authentication and three +entropy-reader tests. Strict host-target clippy, documentation, read-only +workspace formatting and changed-document Prettier pass. Independent +specification/security and code-quality reviews report no blocking findings. + +Hosted follow-up: run `39724280588` at `0c67be66e` exposes an outdated +whole-tree rejection fixture in macOS job `119644526058`: it still labels mode +`120000` as unsupported. Keep its unrelated-subtree rejection assertion and +replace that entry with a still-forbidden `160000` submodule. The separate new +collection fixture requires retention of authenticated inert link blobs. This +process-spawning collection regression remains a hosted check. + +Security verification: CodeQL run `39723446088` scans merge `eca28ba934`, +containing entropy correction `d4408d2e0`. All analyses pass and alerts 224–230 +are fixed, with no relocated entropy warning or open release-tool finding. +Existing core/adapter/JS baseline findings are separate. This is security +acceptance of the entropy correction, not candidate-build qualification. + +Cargo 1.98 compatibility (2026-10-22): run `39725113066` rejects canonical +Linux (`119647122855`) and Fastly (`119647122894`) candidates solely because a +`registry` directory exists in their private Cargo homes. Source review of +Cargo's 1.98 registry constructor and source-replacement loader shows that the +original registry is initialized before replacement with the vendor source. +Its backup-exclusion helper may create an empty directory or the exact +177-byte `CACHEDIR.TAG`, without fetching registry data. Directory presence +alone therefore cannot establish a source bypass. + +The recipe now admits only an absent registry, a direct empty directory or a +direct directory holding only that exact tag. A held directory and matching +before/after inode and metadata checks bind inspection; the existing bounded +no-follow hasher requires a stable, single-link regular tag with the exact size +and SHA-256. Registry `index`, `cache`, `src`, unexpected children, aliases, +changed tags and all Git state remain rejected. No state is deleted or repaired; +offline execution, source replacement and verified package-origin joins remain +unchanged. The tag's upstream URL is runtime compatibility bytes, with a narrowly +reviewed domain suppression; it is never fetched. + +The empty-directory regression first fails at the former presence-only gate, +then passes. Verification selects 177 pure tests, including eight dedicated +bookkeeping cases and 13 recipe tool tests, with nonzero-selection guards. +Strict host-target clippy, documentation and read-only workspace formatting +pass. Independent specification/security and code-quality reviews find no +blockers. Actual candidate builds and their receipts still require hosted +validation after this correction. + +Hosted native acceptance (2026-10-22): run `39729655151` at `5d194d00f` +builds merge `a8c33bcc9990a4eb6520a8afd0f7dd2641c25111`. Linux CLI job +`119661528635`, Apple ARM job `119667144194` and Apple Intel job `119667144562` +all pass their actual producer fixtures, retaining composition receipts with +native completeness and release qualification false. Linux/ARM record +495 packages, 659 units and 61 build-script executions; Intel records +496 packages, 660 units and 61 executions. The real-receipt assertions pass. + +Both Apple legs measure 6,205 selected toolchain library entries and +2,601,020,072 regular bytes, with digest +`da141ea11bf363ffcf61e620ccf28cf72224a7439d3e08e5cc945df01eb2333e`. +Their fixtures complete in 1,277.15 and 2,279.81 seconds respectively. This +accepts the inventory increment under the existing cohort budgets; no separate +inventory timing is logged, so its isolated cost is not established. Both +retain exactly `apple-coverage-excluded:toolchain_libraries`, +`apple-tool-closure-unmeasured` and `native-material-coverage-incomplete`. +Linux retains exactly `gnu-original-acquisition-unverified`, +`gnu-privileged-helper-content-unmeasured`, `gnu-tool-closure-unmeasured` and +`native-material-coverage-incomplete`. These results do not complete Tasks 10/11. + +The same run's only failing job is Fastly `119661528580`: canonical Wasm +inspection rejects `fastly_async_io`. The Cargo bookkeeping barrier is passed; +no Fastly runtime, reproduction, SPDX or four-asset index acceptance is inferred. +Review of locked `fastly-sys` 0.13.1 and project async request call paths binds +this module to `select`/`is_ready`; pinned Viceroy 0.21.0 registers it. Admit only +that exact module. Other unreviewed SDK modules and component-style imports stay +rejected, along with non-function imports, invalid Wasm, wrong entry ABI and +missing debug information. A pure two-function regression first reproduces the +failure and passes after the policy update. Actual Fastly acceptance remains +a hosted check after the fix. + +The scoped ABI verification passes 223 pure tests, including all 68 artifact +inspection tests, plus strict host-target clippy, documentation, read-only +workspace formatting and plan Prettier. Independent specification/security and +code-quality reviews find no blockers. No runtime gate, receipt field, +qualification flag or companion golden changes. + +### Task 11: Generate complete artifact-specific SPDX + +**Phase: 2+ (historical destination task).** Phase 1 uses the reduced implementation checklist above, rather than completing this task. + +JavaScript declaration guard (2026-10-22): projected JavaScript facts contain no +measured declared-license evidence. Reject any supplied declaration at the +JavaScript origin boundary, matching the existing native-origin restriction, +instead of allowing a renderer input to manufacture a declaration. A pure +red/green regression injects MIT into all six supported original/role pairs; +the existing valid-document byte digest remains unchanged. Unknown JS licenses +continue to render as `NOASSERTION`; no new license measurement, receipt field, +companion format or completeness claim is introduced. + +Scoped verification passes 187 pure tests, including all 69 SPDX tests and +unchanged candidate-index goldens, plus strict host-target clippy, documentation, +read-only workspace formatting and plan Prettier. Independent specification and +code-quality reviews find no blockers. The corrected Git collection fixture is +compiled locally but is run only in hosted CI under the process-test restriction. + +**Create:** release tool `src/spdx.rs`, private `spdx::tests`, `crates/trusted-server-release/tests/fixtures/spdx/`. + +- [x] Write cases for incorrect root digest, missing native coverage, ambiguous package identity, unknown license, duplicate SPDX IDs, dangling relationships, uncompiled dev dependencies, type-only/virtual JS and oversize predicates. +- [x] Run the pure `spdx::` suite with the nonzero-selection guard. The scoped 2026-10-21 run selected and passed 66 tests; this validates renderer/candidate behavior, not the closed qualified generator. +- [ ] Emit SPDX 2.3 JSON from Task 10 normalized evidence. Root checksum is final executable SHA-256; contributors/materials/tools/external requirements have distinct relationships. Unknown licenses use NOASSERTION; missing contributing origin/coverage fails. Validator uses a pinned authoritative SPDX schema. +- [ ] Generate full real Fastly and all three CLI SBOMs; schema-valid toy fixtures are not sufficient. Validate size <=16 MiB and document equality semantics: reject duplicate keys, ignore object order, preserve array order. +- [ ] Re-run fixtures plus actual coverage inspection. Commit `Generate validated per-artifact SPDX evidence`. + +Fixture exit audit (2026-10-22, `41560cfd9`): the first checkbox records existing +written regression cases, not complete qualified generation. The cases span +owner validation, projections, rendering and the JS collector: + +| Required case | Existing regression | +| ------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Incorrect root digest, duplicate SPDX IDs and dangling relationship | `spdx/tests.rs::rejects_root_digest_changes_duplicate_identities_missing_origins_and_dangling_edges` rejects each input mutation. `stable_ids_and_document_bytes_do_not_depend_on_input_component_order` checks unique emitted package SPDX IDs. Candidate merge tests additionally reject cross-part duplicates and dangling/cross-part edges. | +| Missing measured native coverage | `composition/native/tests.rs::native_spdx_requires_each_reviewed_consuming_context_and_unambiguous_features` clears measured contributors and requires projection rejection. `rejects_unknown_native_scripts_and_missing_native_execution` rejects missing captured build-script output. These tests do not establish complete platform coverage. | +| Ambiguous package identity | `composition/tests.rs::ambiguous_or_missing_package_and_target_joins_are_rejected` rejects duplicate metadata. `identical_names_and_versions_from_distinct_sources_remain_separate` and `spdx/cargo/tests.rs::original_identity_distinguishes_complete_registry_and_git_facts` preserve distinct source identities. Native tests reject original/context aliases. | +| Unknown license | `spdx/tests.rs::unknown_licenses_use_noassertion_and_never_become_unsupported_expressions` verifies `NOASSERTION` for absent and unknown declarations. Native and JS origin tests reject manufactured declarations. | +| Uncompiled dev dependencies | `composition/graph/tests.rs::catalog_dev_only_entries_never_create_contributors` excludes potential-only packages; `spdx/cargo/tests.rs::package_edges_require_both_observed_endpoints_and_skip_dev_kinds` excludes their projected dependency edges. | +| Type-only JS | `crates/trusted-server-js/lib/test/release-composition.test.mjs` checks that a type-only import and a zero-length rendered namespace are omitted. `javascript/contributions/tests.rs` rejects a zero rendered-length contribution. | +| Virtual JS | The same JS collector file rejects an unknown virtual producer and checks recognized CommonJS helper ownership. `spdx/tests.rs::generated_js_requires_a_known_producer_and_retains_generated_bytes_identity` rejects a missing producer while preserving generated-byte identity. | +| Oversize predicates | `spdx/tests.rs::rejects_an_spdx_document_exceeding_the_attestation_byte_limit` and `spdx/candidate/tests.rs::candidate_bounds_reject_documents_over_sixteen_mebibytes` enforce the output bound. | + +Rust paths above are relative to `crates/trusted-server-release/src/`. Running +only the SPDX and composition selections does not execute the JS collector +cases. No new test API, duplicate fixture, schema change or candidate-index +golden change is needed to close the written-cases checkbox. Complete native/tool +coverage, qualified generator integration, the producer/consumer contract and +full real four-asset SBOM exits remain open. + +Audit verification: fresh pure selections passed 69 SPDX and 67 composition +tests, each with a nonzero selected count. Strict host-target all-target/all-feature +Clippy, all-feature documentation, read-only workspace formatting and this plan's +Prettier check passed. JS collector cases were inspected, not rerun locally. +Independent specification and quality reviews supported the fixture checkbox; +their corrections explicitly separate acquisition/qualification from material +coverage and identify the emitted-ID uniqueness assertion. + +Progress (2026-10-19, partial renderer foundation): a private pure renderer validates executable root identity, component origins and roles, stable package/relationship identities, license declarations and bounded output against the existing pinned SPDX 2.3 schema. Structured-tree evidence stays in explanatory source information rather than raw-file checksums. Fictional fixtures exercise structural/semantic acceptance; schema validity supplies no signing or qualification authority. The closed (qualified) generator entry point still rejects every current candidate; a separate private candidate-only assembly (see the 2026-10-20 notes below) renders an unqualified document that cannot become the closed type. There is no functioning generation command, no caller-controlled completeness flag and no JSON constructor for complete inputs. Typed Cargo/JS contributor export, complete native/tool coverage, producer/consumer contract fixtures and full real Fastly/three-platform CLI SBOMs remain required; the renderer does not satisfy those exits. + +The owning JS handoff now exports typed conservative source/npm/generated contributions, material observations and tool joins after rehashing all retained files before and after export. The export preserves the original inventory digest, distinguishes npm archive integrity from installed-tree evidence, checks generated producer identities and rejects conflicting source hashes. It supplies no license inference, final-function attribution or complete-coverage capability. The real two-installation JS fixture exercises this export after its private working roots are removed; the full two-installation test (`preparation::tests::full::real_two_fresh_js_builds`) that carries the new export assertion passed in ordinary CI, run 39290139247 (job 118188353041, 680.82 seconds; head `df4a6e5c3` descends from `b99a17695`). Complete native/tool closure and qualified SPDX generation remain open; typed Cargo, native and platform projections and the candidate-only whole-document assembly are recorded below. + +The Cargo adapter now derives renderer component identities from approved workspace paths or verified original registry/Git identities, excluding opaque Cargo IDs and temporary roots. Host proc macros and build scripts have build-tool roles; supporting host libraries remain build materials, while target units have conservative Rust-contributor roles. Only package roles are combined; effective unit features remain separate borrowed observations. Potential graph edges are emitted only between observed endpoints and retain their potential-package meaning. Registry archive SHA-256 values identify actual original archives; registry index locations are not invented download URLs, and Git object/tree identities remain provenance information rather than raw SHA-256 checksums. The complete generator stays closed while the remaining native/tool and whole-artifact integrations are unavailable. The shared real-candidate helper exercises the owner adapter; all four canonical cohort checks passed in run 39290139247, as recorded under Task 10. + +The next JS adapter joins that sealed final-byte membership to checked original source/npm/generated facts. Complete bundles receive a containment relationship; positive Rollup-rendered inputs remain conservative observations before esbuild minification, and absent CLI bundles remain build materials. npm archive SHA-512 integrity is separate from installed-tree SHA-256 provenance. Generated wrappers retain their checked Vite producer, and unknown licenses remain `NOASSERTION`. The adapter consumes the completed recipe's frozen original inventory, without depending on the preparation attempt's later lifetime. The JS projection ran inside the shared real-candidate checks of ordinary-CI run 39417433680 (head `bfab8ef20`, every candidate job succeeded; verified from job conclusions, as per-assertion output is not logged). The complete generator stays closed. + +Native and platform adapters now also borrow the sealed completed owner. Native projection requires exact observed wrapper/context/features and verified original-source joins, rejects missing contexts and duplicate aliases of the same original, and keeps original registry archive checksums separate from structured source trees and measured static output archives. Wrapper versions do not assert native implementation versions. Platform projection preserves measured Rust trees, Apple tool files/SDK materials and GNU installed/configured identities with their acquisition and coverage limitations. Logical runtime requirements retain accepted artifact/target/format facts without invented installed-runtime checksums. Source, artifact, target, tool-tree, GNU boundary/plan/profile and Apple SDK joins are checked. Private working paths and opaque Cargo IDs stay out of projected identities. The shared real-candidate helper exercises both adapters, with hosted validation of these assertions in ordinary-CI run 39417433680 (all candidate jobs succeeded; verified from job conclusions, not per-assertion output). Complete generation remains closed until full owning composition coverage and whole-document integration are available. + +Candidate whole-document SPDX (2026-10-20, `1719cf15f` and `073704cc7`): a private `candidate_from_completed` joins the Cargo, JS, native and platform projections of one completed recipe into a single validated SPDX 2.3 document, returned as a separate `CandidateSpdx` with no conversion into the closed `GeneratedSpdx` and no generation command. The source commit must match the build, the platform receipt must equal the composition view's, and the root package must be the policy asset's verified workspace package at the same commit and version. Parts merge only with unique identities and edges inside their own part. The creation comment marks the document as an unqualified candidate and lists the platform coverage gaps, which must not be empty. The root checksum equals the rehashed artifact, the creation time derives from the source epoch and documents stay within the SPDX size bound. `073704cc7` runs it on every real completed artifact in the Fastly, Linux CLI and Apple candidate tests, checks the gaps each platform must report (unresolved Fastly host tools, the reviewed GNU privileged helpers, the Apple exclusions and unmeasured tool closure) and retains document digests and sizes as measurements; Fastly canonical-versus-reproduction document identity is a measurement only. Hosted status: ordinary-CI run 39420011152 passed for `1719cf15f`, which contains the assembly but not the real-build checks. The run for `073704cc7` (Run Tests 39452090186) was still in progress when this note was written, so no hosted result exists for the real-artifact SPDX checks yet. Task 11 remains open: complete native/tool coverage, qualified generation, the producer/consumer contract and full real SBOMs for all four assets are still required. + +Renderer validation follow-up (2026-10-21): JavaScript originals now require +exactly one compatible role before rendering relationships. A complete bundle +may be retained or build material; a rendered contribution may be conservative +input or build material; preparation/tool material may be a build tool or build +material. Crossed, empty, duplicate and mixed roles fail. This prevents a material +record or pre-minification contribution from manufacturing `CONTAINS` evidence. +The normal completed-owner projection already selects these roles correctly; +this is validation hardening, not evidence of a real recipe misclassification. + +An integrated negative fixture rendered unsupported containment before the guard +and rejects it afterward. All six accepted pairs are covered. A fixed valid +document retains its pre-change SHA-256 +`2a79890ec323d58b8c6b8f304b06977c8355c8a5c341301b9782a82ddc489ee8`. +Both pure producer golden checks pass against the unchanged candidate index and +companions; no golden, schema or companion format was rewritten. + +License regression tests confirm the existing conservative behavior: the four +explicitly reviewed exact declarations retain their original spelling/order; +missing or unreviewed declarations, unsupported expressions and an observed +`NONE` use `NOASSERTION`, preserving bounded observed text in license comments. +No license conclusion, file-license analysis, wrapper-to-native license inference +or complete SPDX license-catalog support is asserted. This change does not add +license facts or infer missing origins. Existing named native/tool/acquisition +gaps remain present, and complete qualified generation remains closed. + +The local suite passes 66 pure SPDX tests plus the two pure golden checks. +Real-artifact acceptance is recorded under Task 10 for `e91d75b74`. Complete +native/tool coverage, complete qualified per-artifact SBOMs and the remaining +Task 11 exits stay unchecked. Parallel Task 12 code and its fixtures are untouched. +Task 11 resumes after recording Task 14's hosted preservation result and open +maintainer decision below. Task 14 remains open; this ordering does not waive +either task's coverage, authority or qualified-generation requirements. + +Native license boundary follow-up (2026-10-21): the renderer rejects a native +implementation license declaration when the measured native original has no +such license fact. The owning adapter already supplies no declaration; this +closes an invalid-input path rather than correcting a demonstrated real-build +misclassification. The integrated regression covers all three reviewed wrappers +in host and target contexts. Native license declarations and conclusions remain +`NOASSERTION`; wrapper declarations cannot substitute for native license evidence. +No license catalog is expanded and no existing measured package or relationship +was found missing from its projection. Complete artifact-specific generation +still requires the unmeasured closure/input facts and qualified owning adapters. +Valid rendering, companion formats and candidate golden fixtures are unchanged; +all four candidate jobs accepted the strengthened guard in the `e91d75b74` +run recorded under Task 10. Complete generation remains closed. + +Selected Apple library projection (2026-10-21): a present measured `libtapi` +digest projects as a build tool with a raw-file SHA-256. Recorded absence +creates no package. The projection asserts no original download, license, +runtime bytes or execution, and retains `NOASSERTION` through the existing +renderer. Other package projections, relationship policies and candidate golden +fixtures are unchanged. This adds measured bytes to candidate SPDX without +opening complete qualified generation or closing Task 11. Hosted receipt and +candidate SPDX acceptance for both increments is recorded under Task 10 in the +2026-10-22 note; all named coverage gaps remain present. + +Selected Apple library-directory projection (2026-10-22): retain the new +structured inventory as a build material with no raw-file checksum, original +download or license claim. Candidate SPDX retains the same named coverage gaps. +The measured directory is an input inventory, not an asserted executable +dependency set; complete qualified SPDX generation remains closed. + +### Task 12: Bind transfers and enforce selected comparisons + +**Phase: 2+ (historical destination task).** Phase 1 uses the reduced implementation checklist above, rather than completing this task. + +**Create:** release tool `src/collection.rs`, private `collection::tests` and exact transfer fixture data. + +- [ ] Test stale/wrong-run/source transfers, reused A/B artifact IDs, transfer hash warning, traversal/duplicate ZIP members, missing A, changed inventory, missing supported B variation or dishonest normalized/unsupported claims, absent final-byte runtime proof, missing/failed production socket controls or an unconfined build invocation, and a one-byte final wasm difference. +- [ ] Run `cargo test -p trusted-server-release --target "$HOST_TARGET" collection::tests` with the nonzero-selection guard; expect missing validator. +- [ ] Accept only expected upload-step outputs: IDs/archive digests mapped by trusted workflow dependencies to job/attempt/role/recipe. Re-download exact current-run artifacts and validate REST repository/run/source plus transfer bytes. Artifact API does not independently identify the producing job/attempt. Verify bounded declared ZIP members before extraction. +- [ ] Match expected role-specific ambient profiles from the approved recipe against observed A/B records before signing. For mandatory Fastly A/B, all qualified supported dimensions MUST differ as selected; normalized values remain explicitly identified and omitted supported dimensions fail. Successful native comparisons use their selected observed profile and result binding, but a bounded native B failed/unavailable before environment setup records unavailable fields honestly and cannot be rejected for lacking observations it never produced. Add that exact nonblocking fixture; all native A qualification/composition remains mandatory. Verify final-byte core-format/publisher-request results for both Fastly outputs. Fresh-job identity alone cannot satisfy the variation or runtime contract. +- [ ] Compare complete final Fastly files with no normalized/excluded regions. For native B produce exactly matched/mismatched/failed/unavailable within its deadline; native A failures always block. Record ambient variations and normalized internal values honestly. +- [ ] Preserve A as canonical only after all mandatory requirements pass. Add explicit tests that optional B failure cannot hide any A/composition/JS failure. Commit `Validate release transfers and comparison outcomes`. + +Progress (2026-10-19, partial candidate implementation): 18 focused tests cover trusted exact-ID transfer tuples, raw artifact REST corroboration, complete mandatory A/Fastly B records, bounded optional native B outcomes, ambient/runtime/composition bindings and adversarial ZIP32 inputs. The in-memory ZIP reader checks declared flat members, exact lengths/digests, CRCs, central/local agreement and complete DEFLATE stream termination. Independent spec and quality review passed after fixing a truncated-stream acceptance finding. Imported records remain diagnostic data and cannot construct local recipe, confinement, runtime or signing capabilities. A qualified dependency constructor is deliberately uninhabited. Superseded in part (2026-10-20): the bounded HTTP transport and a candidate producer export now exist (see below), and upload-action ZIP interoperability passed in ordinary CI. The trusted workflow dependency adapter and hosted qualification remain required before this task can close. + +A subsequent exact-ID, read-only audit checked one actual ordinary-CI upload archive (242,907 bytes; SHA-256 `ef2714df5e38c09a011cf25fb37268830de2f03426b81ee646b7b287989401cc`) against unchanged REST metadata and its archive digest. Its 16 regular files use version-needed 2.0, DEFLATE, signed ZIP32 descriptors, Unix regular-file mode and no extras/comments/ZIP64. These observed header properties now have a synthetic regression, bringing collection tests to 19. The JS handoff's nested bundle/inventory paths violated the intentional flat-member policy when this was written; the candidate producer described below since exports the declared flat layout. One small archive's format compatibility does not authenticate its producing job, qualify the trusted dependency adapter or prove every upload size/profile. + +The owning recipe now retains typed socket-control/probe observations after successful denial evaluation, with boundary-instance fingerprint checks at capture, completion and checked access. Diagnostic receipts cannot reconstruct local denial capabilities; explicit pure-test fixtures have no measured receipt. A separate approved-source recipe-definition identity is shared across A/B, while the typed exact invocation digest preserves role-specific paths, jobs, environment and umask. Both retained getters rehash owned outputs, and real candidate helpers exercise them after source/target cleanup. This closes two producer-export prerequisites without implementing the final export/transport adapter or granting release authority. The combined verification batch passes 322 focused tests with independent review; actual hosted validation of these new prerequisites remains pending. + +A candidate producer now copies four flat files into owned storage: `artifact`, `composition.json`, the original `tsjs-inventory.json` and `candidate-build-record.json`. Checked access requires sealed composition receipts, the approved-source recipe definition and measured socket observations; it records actual single-build ambient conditions and keeps the shared recipe digest separate from the exact invocation digest. The recipe freezes checked JS facts and exact original inventory bytes while the preparation owner is valid, retains them independently at completion, and verifies their source/digest and retained file at subsequent handoffs. This prevents a later optional native B timeout from invalidating A's evidence through an expired JS preparation lease. Every copied file is rehashed, and altered, extra, missing or linked members are rejected. This explicitly unqualified diagnostic format still differs from the strict collector's contract: runtime acceptance, ambient pair claims, complete composition closure, authenticated bootstrap and trusted workflow producer identity remain declared gaps. All four canonical recipes proved the previous three-file export's lifetime in run 39290139247; the fourth member (`candidate-build-record.json`, introduced in `7bcf93e50`) and JS projection assertions have since passed in the candidate jobs of ordinary-CI runs 39417433680 and 39420011152 (verified from job conclusions, not per-assertion output). Pure diagnostic fixtures do not establish collector compatibility. No strict collector rule or qualified dependency constructor is relaxed. + +The Fastly path can additionally retain its actual final-byte Viceroy acceptance in the diagnostic record. Acceptance now binds the build role as well as approved source and final artifact size/hash, so equal A/B bytes cannot substitute one role's runtime run for the other's. Checked export rehashes the completed owner before/after formatting bounded observations, including actual Viceroy/fixture/configuration and publisher-response identities; JSON remains output only. Both real Fastly runtime owners are retained through export and cleanup checks. This removes the Fastly runtime-export gap only when the actual owning acceptance is supplied. Native startup export, ambient pair claims, strict collector integration, qualification and workflow authority remain open; native records continue to declare absent runtime acceptance. The Fastly job passed in ordinary-CI run 39417433680 (job conclusions verified). + +Further candidate implementation (2026-10-20): native startup now produces an owning acceptance after executing and cleaning up the exact copied final CLI. Its expected version output derives from the selected package's verified approved workspace origin and sealed composition, rather than the release helper's package version. Linux acceptance requires the three selected distributions and distinct actual local image identities; these images remain unqualified. Apple acceptance checks the actual host cohort, CPU and final Mach-O minimum OS before execution. Checked diagnostic export binds source, role, asset, final size/hash and package version, retaining actual bounded startup observations after build-root cleanup. Optional native B without a successful startup retains an explicit missing-acceptance gap. Independent spec and quality review found no outstanding native-startup findings; these assertions ran in the native candidate jobs of ordinary-CI run 39417433680 (job conclusions verified; not per-assertion output). Both Linux and Apple startup accepted outputs are recorded in the dated reports. + +The read-only HTTP backend now binds exact selected IDs/names/archive digests to strict raw REST repository/run/head metadata. It uses fixed-origin authenticated API requests and one manually checked, credential-free redirect to the supported Azure Blob storage host subset, with compiled WebPKI roots, no proxy or automatic redirects, asynchronous DNS, bounded streaming, cancellation and per-response/attempt deadlines. The unchanged flat ZIP reader remains the extraction gate. Local fictional TCP/TLS tests cover credential separation, changed metadata, denied redirects, transformed/truncated/oversized bodies, cancellation and injected-CA rejection. A test-only ordinary-CI hook exercises this backend without constructing trusted dependencies. Real upload-action interoperability, the trusted workflow dependency adapter, ambient pair claims, strict producer/collector integration, complete composition coverage and hosted qualification remain required; Task 12 is still open. + +An ordinary-CI producer/receiver fixture now uploads four inert fictional flat members using the pinned upload action and passes its exact ID/name/archive digest through job outputs. A separate read-only receiver downloads that original archive twice through the actual backend, corroborates current-run REST metadata and verifies every member's bytes using the unchanged ZIP reader. Fork PRs skip these jobs. The fixture distinguishes the API's PR head from checkout's synthetic merge and grants neither source approval nor release authority. The guarded test has since passed in hosted CI: `collection::transfer::ordinary::real_uploaded_archive` in job 118609812109 of run 39417433680 (0.77 seconds), with the producer job 118609761722 also succeeding. Independent transport spec review and transport/workflow quality review found no actionable findings. This small upload is interoperability evidence only; it cannot establish large-artifact coverage, complete composition or qualified producer identity. + +Local verification of this batch passed 853 native unit tests (seven ignored external/helper fixtures), 13 command tests, strict native Clippy, documentation generation, Rust/Markdown/YAML formatting, actionlint and staged domain lint. A full-suite failure exposed inherited nonblocking accepted sockets in the fictional macOS HTTP fixture; fragmented-header red/green and aborted-header regressions now cover the corrected test server. Independent review found no remaining findings. Linux cross-target Clippy on this Mac cannot compile the newly needed TLS C dependencies without a Linux C cross-compiler; hosted Linux validation remains required. No candidate or release qualification is inferred from these local checks. + +Cross-job candidate-entry transfer (2026-10-21, implemented; first hosted pass in run 39577910126): each real candidate job (Fastly, Linux CLI and both macOS legs) now exports its rechecked entry as six flat files: the final executable `artifact`, `tsjs-inventory.json` and the four unsigned companions. It writes a separate CI-internal transfer file and uploads the files with the pinned upload action under `candidate-entry---` (1-day retention, fork PRs skipped). A describe step merges the exact upload ID, name and archive digest with the producing job's own run attempt into a job-output descriptor; the macOS matrix uses per-leg keys. The new `test-release-candidate-index` job (`contents`/`actions` read only, token only in the download step, restore-only cache) runs the ignored `distribution::ordinary::real_four_job_candidate_index`. It strictly parses the four descriptors, retrieves each exact upload through the unchanged bounded backend and ZIP reader with REST run/head/name/digest corroboration, and rehashes the executable against build-evidence `artifact_sha256`/`artifact_size`. Existing bounds cover the largest measured executable (Linux CLI about 285 MB; 512 MiB member, 768 MiB archive). The descriptor is not a release schema and grants no trusted workflow dependency: companions keep `null` workflow/transfer identities and the `trusted-workflow-transfer-not-bound` gap. Pure tests cover descriptor parsing, export writing and transfer rejections. The trusted dependency adapter, ambient pair claims and qualification remain open, so Task 12 stays open. + +### Task 13: Assemble and validate the exact evidence graph + +**Phase: 2+ (historical destination task).** Phase 1 uses the reduced implementation checklist above, rather than completing this task. + +**Create:** release tool `src/distribution.rs`, private `distribution::tests`, `crates/trusted-server-release/tests/fixtures/index/`. +**Schema boundary:** governing index v1 raw bytes remain immutable; any change requires an explicit reviewed amendment. + +- [ ] Port positive and adversarial schema fixtures: exact four artifacts, unknown capabilities and structurally compatible but currently forbidden `fastly-staging`, duplicate names/keys, source/cohort/recipe mismatch, unsafe names/newlines, altered lengths/digests and role-specific limits. +- [ ] Run `cargo test -p trusted-server-release --target "$HOST_TARGET" distribution::tests` with the nonzero-selection guard; expect missing assembly/semantic checks. +- [ ] Assemble unsigned companions from successful observed outputs, then artifact provenance/SBOM bundles, then the signed index. Index names/hashes all companions and shared JS inventory; exclude own digest/bundle. Recipe/rebuild records are authenticated informational data, not executable programs or independent trust. +- [ ] Require SPDX parsed predicate/document equality plus separate file hashes. Producer verifies every exact expected provenance subject; no additional unapproved code subject. SHA256SUMS covers every other published asset and not itself. +- [ ] Cross-check schema and semantic joins on all real artifacts before signing/rehearsal. Commit `Assemble the authenticated release distribution`. + +Progress (2026-10-20, partial candidate index): a private `distribution` module assembles an unqualified candidate release index. `candidate_entry_from_completed` accepts only a canonical completed build at its approved source with the matching actual acceptance (Fastly runtime plus canonical/reproduction equality, or native startup), renders the candidate SPDX itself and renders build-evidence, build-recipe and rebuild-check companions. Each companion carries `"release_qualification": false`, sorted non-empty gaps and `null` workflow run and transfer IDs. Fastly records `matched` only from the checked equality; native assets record `unavailable`. `assemble_candidate_index` requires each fixed asset exactly once with one source commit, producer policy identity and inventory, emits artifacts sorted by name with no timestamps, and rechecks the rendered bytes against the retained v1 schema plus mirrored consumer semantics. Fixed per-asset identity comes from a compiled accessor equal to the schema row, so `fastly-staging` cannot appear. The tag is the sentinel `v0.0.0-unqualified-candidate`, the bytes are named only `candidate-release-index.json`, and both signature bundle slots are `AwaitingSignature`: they render the true empty-file record and their `Signed` variant is uninhabited. There is no command, writer or signer. Golden fixtures under `crates/trusted-server-release/tests/fixtures/index/candidate/` are byte-compared by the producer and run through the CLI's unsigned consumer joins. Each entry derives `policy_version` and `policy_sha256` from the same approved policy bytes, as the consumer does, and rejects a version other than the compiled one. The CLI verifier now rejects size-0 provenance and SBOM bundle records for every indexed artifact immediately after parsing the index, and again for each consumed artifact, so the candidate cannot pass full verification. Follow-up (see Task 15): once real signing exists, the consumer must also reject the sentinel tag and `"release_qualification": false` documents. Pure fixtures cannot exercise a successful completed-build entry, because they have no composition receipts or actual acceptances. Only owner wiring rejections are covered locally, and real four-asset assembly is deferred to Tasks 12 and 15. Signing, SHA256SUMS, provenance subject checks and qualification remain open. + +Hosted candidate-entry follow-up (2026-10-21): `4483e8256` wires independent +real canonical-entry checks into the candidate jobs. Run 39529922087 completed +Fastly, Linux CLI and Apple ARM jobs, but Intel and the overall run were cancelled. +Run 39540057202 subsequently passed all four per-asset entry checks for source +`d0cd53343fcbe1fb6f06eafc009e306e24b1245d`, with the same JS inventory digest +in all four jobs. The [candidate index report](../../../scripts/release/qualification/2026-10-21-candidate-release-index.md) +retains exact job/entry identities and distinguishes native measurement results +from the entries' deliberately `unavailable` rebuild outcome. This establishes +per-asset integration, not real cross-job four-entry assembly. Task 12's trusted +transfer adapter is the next join prerequisite; complete native/tool coverage, +authenticated acquisition, signing, publication and qualification remain separate +unfinished exits. Task 13 remains partial. + +Real cross-job assembly (2026-10-21, implemented; first hosted pass in run 39577910126): the +collector job rebuilds each transferred entry with the test-only +`CandidateArtifactEntry::from_ordinary_ci_transfer`, the one documented +exception to the production-only constructor. Production is unchanged. The +rebuild reruns every producer join over the transferred bytes and derives +`policy_version`/`policy_sha256` from the consumer policy read out of a +retained clean clone authenticated at `github.sha`. Fastly must be `matched` +with reproduction equal to canonical, native assets `unavailable`. Gaps must be +sorted, unique, a superset of the fixed entry gaps and identical across +companions. Every companion is rehashed, and each rebuilt entry must recheck to +the producing job's entry digest. The unchanged `assemble_candidate_index` then +writes `candidate-release-index.json`, the inventory and sixteen companions. The +collector prints the index and its SHA-256, the CLI's ignored +`ordinary_ci_assembled_candidate_passes_unsigned_consumer_joins` runs the +consumer's unsigned joins and empty-bundle rejection over those files, and the +directory is uploaded as 7-day unqualified candidate evidence. It runs only when +all four candidate jobs succeed and is not a required check. A pure round trip +reproduces the golden index byte for byte, alongside rejection, descriptor and +writer tests. The first hosted all-four assembly passed in run 39577910126 at +merge source `fb42d8268`: index SHA-256 +`4872ef96086e93f720187f363e7262ae2921e663fbc9a531f2a42cc5d803b14d`, accepted by +the consumer's unsigned joins (see the +[four-asset candidate join report](../../../scripts/release/qualification/2026-10-21-four-asset-candidate-join.md)). +The preceding run 39567710071 rejected its first download because the collector +passed one 20-minute deadline to a transport capped at 15 minutes; +`d774ee47c` bounds each download separately. Signing, publication and +qualification stay open. + +Pair comparison (2026-10-21, `de52e66fd`): the collector also rejects a +canonical/reproduction pair sharing one boundary instance and treats an equal +host locale or timezone as a missing supported variation. New pure tests +isolate the pair check per dimension, record every optional native +reproduction outcome, show no reproduction state hides a mandatory canonical +failure, and require Fastly runtime proof for both roles. + +Collector follow-ups (2026-10-21): + +- `f17377a4e`: an optional native reproduction without an upload is an + observed job (started flag, terminal conclusion, bounded elapsed/deadline) + with no upload identity fields; the collector derives `unavailable` (queued + at the deadline, skipped, cancelled before any step) or `failed` (started + then cancelled or still running, failure, timed out, success without an + upload) and rejects contradictory, premature or unbounded observations. Each + dependency channel accepts only its own build roles before any fetch, and the + Fastly runtime record must carry its record's role. That role is + producer-asserted, so it guards an honest pipeline; a per-run Viceroy + identity remains open. +- `15893ef1e`: the earlier "exact invocation" check compared a fingerprint + forced equal to the boundary instance, so it only proved distinct instances. + Records now carry `execution_fingerprint` (boundary instance, bound to the + denial receipt) and a separate `invocation_digest` from the exact Cargo plan + digest (program, arguments including jobs, environment, role checkout root, + umask); the pair must differ in both. A test-reachable producer renderer + emits this strict record from producer-owned facts. +- `5a98c18ad`: the collector parses the producer's real sealed composition + receipt instead of a fixture-only summary, keeping every earlier check and + adding materials, target, package-join, Fastly embedding, coverage-claim and + platform-shape bindings. Real candidate tests feed each real receipt through + the same check, so producer/collector drift fails ordinary CI. + +Still open: the trusted workflow adapter that fills observed job states and +upload identities; job/attempt identity supplied by the workflow; native +startup acceptance has no collector slot; native canonical input digests are +only compared against a reproduction; an invalid uploaded native reproduction +rejects the whole collection rather than recording `failed`; and the +`failed`/`unavailable` mapping is not yet normative in the spec. + +### Task 14: Expose trusted same-commit validation + +**Phase: 2+ (historical destination task).** Phase 1 uses the reduced implementation checklist above, rather than completing this task. + +**Create:** `.github/workflows/release-validation.yml`, release tool private workflow-contract tests (register alongside the production policy/dispatch module; do not expose a test-only public API). +**Modify:** `.github/workflows/test.yml`, `format.yml` only to extract/reuse existing required jobs without losing existing triggers/gates. + +- [ ] Add static workflow fixtures for wrong SHA/event/ref, unlisted RC branch, malicious candidate allowlist, forged local remote refs, missing governance/ruleset/bypass approval, skipped/path-filtered gates, arbitrary commands, excessive permissions and any floating external action ref in the complete release/validation invocation. +- [x] Run the pure `workflow_contract` suite with the nonzero-selection guard. The scoped 2026-10-21 run selected and passed 19 tests; three release-binary-only command tests and 45 pure Node tests also passed. These checks do not establish hosted release eligibility. +- [ ] SHA-pin every external action in release.yml, release-build.yml and all called release validation jobs, including checkout/setup/cache/upload/download and attest. Preserve readable version comments, audit dependencies and reject floating @v\*/main refs. Local workflow calls are allowed only at the caller's checked source revision. Existing test/format action refs MUST NOT be inherited unreviewed into this path. +- [ ] Expose reviewed workflow_call or explicit jobs running every current AGENTS gate at the tag SHA. Maintain PR/main tests; RC is permitted only by exact reviewed cohort branch entries. Validate strict SemVer/lightweight tag independently; annotated tags fail. +- [ ] Wait up to 90 minutes for this invocation's required terminal validation results; fail cancelled/skipped/timeout results with rerun guidance. Do not trust arbitrary named successful checks or unrelated main commits. +- [ ] Include JS build/tests/format, all Rust aliases/parity/CLI/build-digest, the explicit host-only release-tool test/clippy gate (or reviewed aliases), docs/Markdown and `scripts/lint-wasm-clock.sh`. Commit `Validate release source at the exact tag commit`. + +Progress (2026-10-21, partial Task 14): `release-validation.yml` exposes only +`workflow_call` with one required source SHA. Its explicit jobs cover the current +AGENTS gates, host release-tool Linux/macOS tests and clippy, and existing parity, +codegen, JS prebuilt, documentation and trust-test checks. Every checkout selects +the supplied SHA with persisted credentials disabled. Both external actions use +reviewed commit pins and version comments; no composite or local workflow is +inherited. Shell Rust setup follows the existing native release-tool approach +without introducing a floating action or shared cache. + +The read-only `check-workflow-contract --workflow ` command checks exactly +one reviewed workflow and its fixed sibling helper. It executes nothing, performs +no network access and reports both source eligibility and release qualification +as false. Inputs are bounded to 1 MiB before parsing; held regular-file reads use +no-follow/nonblocking open. Raw workflow, canonical parsed structure and helper +digests bind commands, permissions, conditions, action pins and each version +comment. YAML references, merge keys, duplicate/non-string keys, tags and +unsupported ambiguous spellings fail. The restricted parser conservatively +rejects reference indicators outside literal run blocks, even in quoted scalars. +Formatting or helper changes require explicit review and pin updates. Additional +files and local invocation graphs remain unsupported; Tasks 15–16 must extend +this closed contract when their actual caller/signing workflows exist. The YAML +dependency reuses the workspace package; the lockfile adds only its dependency +edge, with no new package. + +The runtime helper requires the same full source/workflow SHA, official repository +identity, tag-push event, exact caller/ref and strict bounded SemVer. Fixed-origin +bounded read-only REST requests observe the unpeeled lightweight tag, actual +remote `main` tip and ancestry; neither local refs nor candidate branch entries +provide authority. No RC branch is approved. Admission records observations and +`source_eligibility: false`, allowing quality checks to run without authorizing a +release. The parallel waiter enumerates this run attempt's required terminal jobs, +rejects missing/duplicate/wrong-source results and non-success conclusions, and +uses an original-run, queue-inclusive 90-minute window with monotonic remaining +budgets for requests and polling. The first bounded REST read obtains the trusted +run creation time. Rerunning jobs cannot reset that window; diagnostics distinguish +reruns within it from an expired window requiring a fresh trusted invocation. + +Even if every quality gate succeeds, the terminal helper fails because an +independently approved governance/ruleset/bypass profile is not established. +No approval schema, ruleset identity or bypass actor is invented. Consequently +Task 14 remains open for independent governance authority and live callable +acceptance, and the remaining checklist exits stay unchecked. No tag/release +trigger, signing, publication, settings change or qualified capability is added. + +Preservation check: all 16 pre-existing `test.yml` jobs and five `format.yml` jobs +retain their exact names, conditions, dependencies and matrices. `test.yml` only +appends the pure helper-contract job; `format.yml` is byte-identical. Thus existing +triggers, path classification, caches, native-reproduction selectors, four +candidate legs and the candidate-index collector remain unchanged. The before/ +after listing was compared against `d774ee47c` before editing. + +Local verification passes the 19 pure Rust cases, three checker command cases, +45 pure Node tests, strict host clippy, documentation, owned-file rustfmt and +Prettier, and the forbidden-action guard. Modified workflows pass actionlint. +The all-workflow actionlint run reproduces three existing diagnostics in unchanged +`deploy-docs.yml`/`docs-proposal.yml` (unquoted output path and locally unsupported +concurrency/permission keys); the baseline at `d774ee47c` reports the same three. +The broad `.github` Prettier check also reports ten unchanged baseline files; +the owned workflow/helper files pass the pinned docs configuration. No unrelated +workflow or shared-tree formatting is changed. Hosted preservation/contract +results after this push remain pending; pure fixtures are not a live invocation. +Independent spec and quality reviews approve this scoped foundation; the narrow +official REST-host suppression also received explicit review. Review fixes cover +Node extraction quoting, least-privilege permissions, documented REST attempt +responses, tag bounds, per-request remaining budgets and composed source-API +coverage. Task 14 is not marked complete by those approvals. + +Hosted preservation follow-up (2026-10-21): successful [Run Tests 39596973649](https://github.com/IABTechLab/trusted-server/actions/runs/39596973649) +has head `ac07047ed2b4f1bbe0e1f2e4aac9f845e70b7c7c`, including `2b642b509`. +All 20 expanded test jobs succeeded, including Fastly candidate +[119211581826](https://github.com/IABTechLab/trusted-server/actions/runs/39596973649/job/119211581826), +Linux CLI [119211581675](https://github.com/IABTechLab/trusted-server/actions/runs/39596973649/job/119211581675), +Apple ARM [119219615235](https://github.com/IABTechLab/trusted-server/actions/runs/39596973649/job/119219615235), +Apple Intel [119219615180](https://github.com/IABTechLab/trusted-server/actions/runs/39596973649/job/119219615180), +the index collector [119250788447](https://github.com/IABTechLab/trusted-server/actions/runs/39596973649/job/119250788447) +and helper contracts [119211581539](https://github.com/IABTechLab/trusted-server/actions/runs/39596973649/job/119211581539). +The collector's built source is PR merge `1c78dd4556b03ddaa369680f2248edac1761bcd8`, +distinct from the run head. Matching [Run Format 39596973672](https://github.com/IABTechLab/trusted-server/actions/runs/39596973672) +passed all five jobs. Comparison with `d774ee47c` confirms the same 16 original +test and five format definitions, names, conditions, dependencies and matrices; +only the helper job was appended. Matrix expansion accounts for 19 original test +instances plus the helper. This establishes ordinary-CI preservation, not a live +release-validation invocation or governance/source authority. + +**Unresolved maintainer decision before Task 14 can advance:** + +1. Select an independently governed approval record and its approving maintainers. + Approve exact repository/organization ruleset identities, active enforcement, + ref inclusions/exclusions and restrictions covering `refs/tags/v*`, `main` + and every future exact reviewed RC branch. Specify release-tag creation + authority and effective update/deletion restrictions. Eligible branches remain + exactly `["main"]`; a candidate entry or name pattern cannot approve an RC. +2. Approve exact branch-required check identities, their expected reporting + applications and branch update policy. Separately approve the release + invocation's mandatory same-commit gates: admission, Fastly, native/parity, + Cloudflare/Spin, both host release-tool jobs, JavaScript, docs and terminal + eligibility. Record actual caller-qualified names when Tasks 15/16 define the + caller. Another source/run/attempt or reporting application cannot satisfy + these gates by reusing a successful check name. +3. Approve an empty bypass list or exact actor IDs, actor types, modes and scope + of every exception, including inherited organization rules and tag creation + authority. Record who reviews changes and approves exceptions. Administrator + status, visible settings and candidate assertions do not establish approval. +4. Approve the independent trust root and read-back evidence. Validation must + obtain bounded official API observations of repository identity, applicable + repository/inherited ruleset details, active enforcement and ref scope, + effective required-check/application bindings and exact bypass actors/modes. + Compare with the independently approved record, binding its reviewed revision + and approvers. Retain the unpeeled remote tag, actual approved branch tip and + ancestry, and this source/workflow/run/attempt's complete terminal gate results. + Missing, unreadable, truncated, ambiguous or changed approval/settings + evidence must deny eligibility; settings cannot approve themselves. + +These are decisions to obtain, not an implemented approval profile or settings +mutation. No ruleset ID, check configuration, bypass actor or approver is selected +here. GitHub describes rule applicability and application-bound checks in +[available rules](https://docs.github.com/en/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/available-rules-for-rulesets). +The terminal job continues refusing eligibility. Stop governance work here until +maintainers approve the record and Tasks 15/16 supply the reviewed caller and an +authorized live invocation; bounded Tasks 11/10 resume meanwhile. + +### Task 15: Orchestrate independent builds and isolated signing + +**Phase: 2+ (historical destination task).** Phase 1 uses the reduced implementation checklist above, rather than completing this task. + +**Create:** `.github/workflows/release-build.yml`; extend workflow contract fixtures. + +- [ ] Test never-started/queued or scheduler-cancelled optional native B jobs as well as pre-step failure: the trusted collector MUST emit a bounded failed/unavailable measurement with absent observations represented honestly and no fabricated upload identity. Confirm the workflow cannot wait indefinitely or hide a mandatory A failure. +- [ ] Test topology: shared JS A/B, separate fresh Fastly A/B, three mandatory native A jobs, bounded optional native B jobs, validated current-run collector and no signing credentials in executable/lifecycle jobs. Require job configurations to apply Task 9 role profiles and publish observed variation/runtime records; tests reject default-only A/B jobs or collectors that bypass these gates. +- [ ] Run `cargo test -p trusted-server-release --target "$HOST_TARGET" workflow_contract` with the nonzero-selection guard; expect absent graph. +- [x] (from Task 13) The consumer rejects the candidate sentinel tag `v0.0.0-unqualified-candidate` (exact match) as the first check of `verify_index` and `verify_build_evidence`, which every online and offline path reaches. It also rejects any consumed build-evidence, build-recipe or rebuild-check companion declaring `"release_qualification": false` or a non-boolean value, after the companion bytes are authenticated against the signed index; an absent field or `true` is accepted because the R-INDEX contracts do not define the field. Both rules hold independently of the empty-bundle rule: a golden candidate index with non-empty bundle records is still rejected (`ac07047ed`). Making the field normative in the spec's companion contract needs a reviewed amendment. +- [ ] Call local reusable workflow at source revision. Build jobs are read-only. Collector uses Task 12 outputs; signing grants only contents-read/id-token-write/attestations-write, plus actions-read when needed. No artifact-metadata write for file attestations. Caller grants enough permissions for called signing jobs but its own build jobs explicitly narrow them. +- [ ] Invoke pinned attest 4.2.2 twice per canonical asset: exact executable/SBOM/evidence/recipe/rebuild provenance subjects, then executable subject + SPDX predicate. Save named bundles, validate all proofs/joins and sign index after collection. Do not construct caller predicates or sign source supplied commands. +- [ ] Rehearse actual certificates/toolkit fields in a disposable repository with test-only policy fixtures. Verify repository IDs, exact SAN, issuer, build/caller/source/event/run fields for both predicate types. Commit `Sign validated release outputs through the reusable builder`. + +Progress (2026-10-21, partial, collector logic only): `f17377a4e` implements +the collector side of the never-started/queued/cancelled and pre-step-failure +bullet as pure logic (see Task 12 collector follow-ups): no upload identity can +exist for such a job, outcomes are derived from bounded observed job states, +and a missing mandatory canonical rejects collection under every optional +state. The bullet stays open until the trusted workflow adapter supplies real +job observations and `release-build.yml` bounds the wait; no workflow, signing +or publication was added. + +### Task 16: Publish through one immutable-release owner + +**Phase: 2+ (historical destination task).** Phase 1 uses the reduced implementation checklist above, rather than completing this task. + +**Create:** `.github/workflows/release.yml`, release tool `src/publication.rs` and private `publication::tests`. + +- [x] Add fake API cases for matching/conflicting draft, missing/altered asset, concurrent rerun, lost response, already published tag, mutable/unknown final state and permission differences on approved RC commits. +- [ ] Run `cargo test -p trusted-server-release --target "$HOST_TARGET" publication::tests` and `workflow_contract` separately with the nonzero-selection guard; expect absent publication owner. +- [ ] Keep tag-scoped concurrency without cancellation. Draft, upload, re-download and validate the exact asset set before publication. Match source/bytes for draft resume; never overwrite published assets/tags. Publication has contents-write only, no OIDC/provider/settings token. +- [ ] Require maintainer governance approval and immutable/tag-rule setup before enabling tags; afterward read immutable=true and verify GitHub release statement/membership with the normal token. A confirmed mutable release fails publication, is removed only when the workflow release-write role authorizes that still-mutable cleanup, and produces an incident with the cleanup outcome. Never delete an immutable release. Unknown state produces an incident and read-only reconciliation; it MUST NOT trigger blind deletion. A lost publish response also triggers read-only reconciliation before another mutation. +- [ ] Exercise all failure paths in an authorized disposable public repository and record measured evidence. Production tag/permissions require separate release authorization. Commit `Publish complete immutable attested releases`. + +Progress (2026-10-21, `9bd8be2c2`, closed pure owner): `src/publication/` +implements the sole-owner state machine against a `ReleaseApi` trait with an +in-process fake (33 tests). It refuses a tag that has a published release or +points at another commit, resumes a draft only on matching tag, commit and +bytes, re-downloads and validates the exact asset set within bounds derived +from the index asset set and role limits, re-checks the tag and draft before +publishing, and reconciles lost responses read-only. After publication it +requires an immutable release whose observed tag, commit and statement +membership equal the intended release; a confirmed mutable release is removed +only with cleanup authority after a fresh read, with an incident carrying the +API failure. Immutable or unknown releases are never deleted. Publication holds +only a contents-write marker built from the caller-attested job permissions. +There is no real API adapter, `release.yml`, `workflow_contract` publication +fixture, command or token handling, so the remaining bullets stay open; partial +uploads after lost responses need a manual recovery runbook. + +### Task 17: Implement strict consumer documents and embedded policy + +**Phase: 2+ (historical destination task).** Phase 1 uses the reduced implementation checklist above, rather than completing this task. + +**Modify:** CLI `release-policy.json` from Task 3. +**Create:** CLI `src/release/mod.rs`, `error.rs`, `policy.rs`, `documents.rs`, `src/release/tests/mod.rs`, `src/release/tests/documents.rs`, `tests/fixtures/release/`. +**Modify:** CLI `src/lib.rs`, `Cargo.toml` (host-only sha2/schema validator with justification). + +- [x] Before the first filtered run, wire the host-only `mod release` in CLI `src/lib.rs`, the production child modules in `src/release/mod.rs`, `#[cfg(test)] mod tests` there, and `mod documents` in `src/release/tests/mod.rs`. Add the test cases before checking selection; Rust does not discover this directory automatically. Keep these internals private. +- [x] Test closed schema/kind/profile/action combinations, structurally valid but policy-forbidden `fastly-staging`, duplicate/deep/oversize JSON, self-selected policy, historical policy acceptance/retirement, altered approved-source policy bytes, allowlisted digest paired with the wrong source, unsupported tool and CLI-as-server substitution. +- [x] Run `cargo test -p trusted-server-cli --target "$HOST_TARGET" release::tests::documents`; expect the intended missing implementation or assertion failure. When it compiles, use the nonzero-selection guard below before accepting red or green; a successful run selecting zero tests is a failure of this plan, not a pass. +- [x] Implement byte-first duplicate-rejecting parsers and the same schema/semantic fixtures as producer. Embed reviewed policy bytes; compute their SHA-256 externally at runtime. Policy contains no own digest. First bind producer-policy bytes to the independently approved source commit through the fixed official repository path `crates/trusted-server-cli/release-policy.json` (independently approved source snapshot or fixed official GitHub Contents API acquisition from that exact independently approved full commit); never obtain this authority from the release index or an arbitrary supplied path. Match its exact-byte digest/version, then accept only current policy bytes or the reviewed historical allowlist. For offline checks, independently transfer the approved source-policy snapshot alongside other trust inputs. Downloaded metadata never changes consumer policy. +- [x] Follow [R-POLICY](../specs/2026-10-09-1235-release-attestation-design.md#r-policy): fetch only `https://api.github.com/repos/IABTechLab/trusted-server/contents/crates/trusted-server-cli/release-policy.json?ref=`, `Accept: application/vnd.github.raw+json`, explicit read-only credential, 1 MiB maximum and attempt deadline. Reject arbitrary host/path or credential-forwarding redirects. Add source/API mismatch, unsupported producer v2 under consumer v1, reviewed historical v1 under newer consumer, and current-consumer-bound enforcement negatives. +- [x] Keep the already registered tests active and require nonzero selected counts on every focused rerun; process tests use public CLI dispatch rather than making internals public. Keep concrete release errors as Report; convert to the existing String dispatch boundary only at the command adapter. Do not introduce a new generic attestation crate or publisher types. +- [x] Fix repository/owner IDs, supported combinations, exact gh/action contract and all R-BOUNDS limits. Re-run tests/clippy. Commit `Parse release evidence under embedded consumer policy`. + +### Task 18: Implement the bounded isolated gh runner + +**Phase: 2+ (historical destination task).** Phase 1 uses the reduced implementation checklist above, rather than completing this task. + +**Create:** CLI `src/release/gh.rs`, `src/release/tests/gh.rs`. + +**Modify:** CLI `src/release/mod.rs`, `src/release/tests/mod.rs`. + +- [x] Register the new gh production modules and `mod gh` test module before the first focused run. Apply the nonzero-selection guard; missing module registration or zero matches MUST never report a passing security check. + +- [x] Write fake-child tests for hangs/cancellation, simultaneous stdout/stderr saturation, malformed/truncated JSON, exit failure, multiple attestations, token precedence, host/config overrides and log redaction. +- [x] Run `cargo test -p trusted-server-cli --target "$HOST_TARGET" release::tests::gh`; expect absent runner. +- [x] Resolve gh once through trusted PATH/explicit trusted installer configuration, require 2.97.0, and retain that executable target. Pass fixed argument vectors with exact certificate identity; never arbitrary forwarded verifier flags. Accept explicit GH_TOKEN then GITHUB_TOKEN, pass only GH_TOKEN, isolate home/config/state, disable prompts, reject loader injection and keep provider credentials out. +- [x] Concurrently drain bounded pipes: 120 seconds per child, 15 minutes per attempt, 128 MiB stdout/1 MiB stderr. Kill/reap on either limit/cancellation. Use real OS fake-child fixtures, not mocks that omit pipe pressure. Failed exit never yields trusted parsed claims. +- [x] Re-run runner negatives and an approved signed public fixture under the pinned gh binary. Actual attest 4.2.2 provenance and SPDX signatures, isolated token-free replay, offline SPDX replay and signed release output were measured; see [consumer qualification report](../../../scripts/release/qualification/2026-10-15-consumer-real-fixtures.md). Commit `Bound and isolate release verification subprocesses`. + +### Task 19: Verify signatures, evidence joins and official publication + +**Phase: 2+ (historical destination task).** Phase 1 uses the reduced implementation checklist above, rather than completing this task. + +**Create:** CLI `src/release/evidence.rs`, `github.rs`, `artifact.rs`, `src/release/tests/verification.rs`. + +**Modify:** CLI `src/release/mod.rs`, `src/release/tests/mod.rs`. + +- [x] Register the new evidence/github/artifact production modules and `mod verification` test module before the first focused run. Apply the nonzero-selection guard; missing module registration or zero matches MUST never report a passing security check. + +- [x] Add a valid graph and independently altered edges for index, all provenance subjects, SPDX subject/document/root, JS prepared/retained records, policy/source/run/caller identities, signed names, REST lengths/immutability and tag object type. +- [x] Run verification tests; expect no implemented evidence graph. +- [x] Verify index provenance before trusting fields. Obtain only approved official GitHub host/repository data with explicit read-only credential and bounded responses. Validate lightweight source target, source approval, independently acquired fixed-path producer policy bytes at that commit, exact signed release names/digests and actual/index/REST sizes. Primitive gh success is insufficient. +- [x] Check verified certificate numeric IDs, exact SAN/issuer/build config/digest/trigger/run URI in both proofs, and provenance build type/caller/external parameters/resolved source/invocation. SPDX lacks SLSA caller fields; use its certificate plus subject/document constraints. +- [x] Separate BuildEvidenceVerified, PublicationVerified and consumer ActionAuthorized states; constructors remain private. A retained verified-release-asset owns private bytes/handle and source/policy/role evidence. Read-only CLI verification returns a VerificationReport, not deployment authorization. The linked integration plan adds the private action-scoped VerifiedArtifact only after concrete context approval; an asserted path/hash cannot construct it. Stream bounded asset responses into retained files, enforcing each role limit while downloading; do not buffer a 512 MiB executable through the gh JSON pipe or check limits only after writing it. Rehash before later use and reject offline official-deploy construction. +- [ ] Official project first-use remains pending: upstream attest 4.2.2 formats and both real predicate signatures are measured in the report above, but third-party fixtures cannot satisfy the official repository/index policy. Exercise first-use verification using an independently trusted previous verifier or source-built verifier from independently approved source, never the downloaded executable verifying itself. Commit `Verify complete official release evidence before authorization`. + +### Task 20: Expose read-only CLI verification and bootstrap instructions + +**Phase: 2+ (historical destination task).** Phase 1 uses the reduced implementation checklist above, rather than completing this task. + +**Create:** CLI `src/commands/release.rs`, `tests/release_cli.rs`, `src/release/offline.rs`, `src/release/tests/offline.rs`, `docs/guide/release-verification.md`. +**Modify:** CLI `src/commands/mod.rs`, `src/run.rs`, `src/release/mod.rs`, `src/release/tests/mod.rs`; `docs/guide/cli.md`, `.vitepress/config.mts`. + +- [x] Register `mod offline` in both the private production and test roots before its first focused run. Register the public command module/dispatch before process tests; run `release::tests::offline` with the nonzero-selection guard in addition to the CLI process target. +- [x] Test the required parse path `ts release verify v1.2.0 --asset ts-aarch64-apple-darwin --expected-source-commit `, invalid source/asset/unsupported offline-deploy flags and no provider/config/build hooks on all failure paths. +- [x] Run `cargo test -p trusted-server-cli --target "$HOST_TARGET" --test release_cli`; expect unknown command. +- [x] Add the command adapter to the reusable host verifier. The initial public surface stays the spec's verify/tag/asset/expected-source contract; any cache/export/output flag MUST be documented and reviewed, not smuggled into an implementation example. +- [x] Document trusted gh installation and independent source/previous-verifier bootstrap before first executing downloaded ts. A source-built verifier provides approved-source trust, not official binary provenance for itself. Describe exact policy checks after primitive gh signatures; do not instruct self-verification or checksum-only installation. +- [x] Add an internal build-evidence-only offline verification path, reusing parser/policy/subject/SPDX joins and bounded gh runner with independently approved roots/source/digest. Test it with networking denied and token absent; its result cannot construct publication/deployment authorization. No initial public offline flag is added. Document the pinned primitive commands and the manual companion/document/policy checks needed beyond their exit status; offline success is build evidence only, no official release membership or offline deploy. Run CLI helper, docs lint/format/build and malformed/timeout/zero-provider-write fixtures. Commit `Expose official release verification and trusted bootstrap`. + +### Task 21: Validate and document manual Fastly production consumption + +**Phase: 2+ (historical destination task).** Phase 1 uses the reduced implementation checklist above, rather than completing this task. + +**Create:** release tool `src/package.rs`, private `package::tests`, bounded manual Fastly archive fixtures and public command tests. +**Modify:** `docs/guide/release-verification.md`, `docs/guide/cli.md`, `CHANGELOG.md`; correct obsolete config-embedding comments in source/test/setup files identified below. + +- [x] Test accepted four-member layout, generic manifest-only archive, swept-in files, reserved/empty package name, traversal, links, duplicates, wrong module/manifest, malformed/non-core wasm and max-size bounds. +- [x] Run `cargo test -p trusted-server-release --target "$HOST_TARGET" package::tests` with the nonzero-selection guard; expect absent inspector. +- [ ] Use a fresh private directory, independently bootstrapped approved-source `trusted-server-release inspect-fastly-package`, an allowlisted deployment-only manifest and verified wasm. `compute pack --wasm-binary` has two output archives; select named archive only after exact layout/content checks. Pack/validate accepting a file does not prove valid Wasm or application execution. Verify valid core format and positive publisher-request fixture separately. +- [x] Inspector takes the approved expected executable digest and manifest identity from the successful verification/context procedure, rehashes both the retained archive and its actual module member immediately before submission, and never trusts a digest supplied by the archive. Document trusted-source acquisition for the helper; it is not a separately authenticated release asset. +- [x] Document observed service/base-version bindings and explicit production command with --service-id, --version and --no-default-domain. Explain manifest precedence, automatic clone/activation, possible same-hash skip and inability to supply integrated staging/preactivation guarantees. Serialize manual operator changes. No compute publish/source build is permitted. +- [ ] Run a separately authorized disposable Fastly service submission; compare submitted member and actual returned version, including skip/uncertain cases. Record live proof before declaring AC-9 passed. No customer context or provider credentials enter producer jobs. + + This exit remains unchecked: no live service submission is authorized in the + local Task 21 increment. Inspector/command tests and instructions do not supply + submitted-member, returned-version or application-runtime observations. + +- [x] Correct false config-embedding text in `.github/actions/setup-integration-test-env/action.yml`, `.github/workflows/test.yml`, integration runtime/framework/parity helpers, browser infra helper and Spin app/middleware/route comments; behavior changes remain separate. Commit `Document and validate verified-byte Fastly deployment`. + +- [x] Update `docs/guide/getting-started.md`, `edgezero.md` and `error-reference.md`: canonical `x-ts-version` is full source identity, external tag belongs to the index, and development retains #1213 display fallback. Annotate the historical #1213 goals. Validate manual retained-byte packaging/submission as AC-9/D6 and operator guidance as AC-9/D8; upstream EdgeZero delay MUST NOT block this manual route. Context/preactivation/staging/recovery remain AC-8. + +Local implementation (2026-10-21): `inspect-fastly-package` selects only the +named archive derived from a separately approved manifest, checks its four raw +TAR members without extraction and validates actual module bytes with the pinned +core-Wasm inspector. The initial manifest profile is deliberately narrow: exactly +`manifest_version = 3` and a portable nonreserved `name`, in canonical two-line +form without comments. It accepts no build hooks, setup payloads or local fixtures. +Held-file/path identity checks and repeated archive/member hashes precede the +diagnostic report; expected hashes are independent inputs and grant no authority. +Pure archive fixtures and command tests exercise local inspection only. + +The guidance documents trusted helper bootstrap, separate retained verified-byte +acquisition, service/base-version observations and explicit production limits. +`ts release verify` still has no public export option. The source-header split +and historical #1213 development fallback remain documented. The checked guidance +item above records the text implementation, not completion of its AC-9/D6 live +validation dependency. An actual pinned Fastly CLI pack/inspect compatibility run, +positive application-request runtime acceptance in this manual operation, and +separately authorized disposable-service submission remain unobserved here. +The pack/private-directory exit and live submission exit stay unchecked; AC-9 +and Task 21 are not complete. No production selector, workflow trigger, transfer +adapter, signer, deployment code or provider resource was changed. + +Local verification passed 17 pure package tests with a nonzero-selection guard, +both new public-command tests (spawning only the release binary), strict native +all-target/all-feature Clippy, documentation generation, formatting and workflow +lint. The acceptance command failed before dispatch existed and passed afterward; +a non-UTF-8 report-path panic was separately reproduced and corrected. Independent +spec and quality review found no remaining findings. No sandbox/container tests, +Fastly CLI process or provider submission were run locally. The complete hosted +CI matrix and actual pinned-pack compatibility remain separate validation. + +### Task 22: Full validation, qualification evidence and release handoff + +**Phase: 2+ (historical destination task).** Phase 1 uses the reduced implementation checklist above, rather than completing this task. + +**Modify:** the implementation PR's evidence record and docs; no automatic production mutation. + +- [ ] Run the complete command block below and collect exit statuses/log summaries. On local certificate/localhost sandbox failures rerun the same tests with needed access; do not weaken TLS or alter assertions. +- [ ] Rehearse hosted source authorization, shared JS, Fastly A/B, all three native A/SBOMs, optional B terminal failures, actual exact-action signatures, immutable draft/publication and full consumer verification together. Toy/synthetic component proofs cannot replace this complete composition. +- [ ] Recheck permission/config separation, governance/rules/bypass authority, bootstrap docs, actual platform evidence and live manual submission. Missing evidence blocks tag enablement; native B equality, packaging or upstream integration do not add hidden blockers. +- [ ] Review against AC-1–AC-7, AC-9–AC-10; keep AC-8 separate. Use @superpowers:requesting-code-review and @superpowers:verification-before-completion. Maintainer approval precedes any production tag. +- [ ] Commit validated documentation/evidence with `Record initial release qualification`; update existing PR #1262 with implementation and validation evidence only when the required checks pass; never create a replacement implementation PR. + +## Threat-to-negative-proof trace + +| Threat | Existing test families | Plan negative proofs | +| ------ | ---------------------- | ------------------------------------------------------------------------------------------------------------------ | +| TH-1 | T-8, T-9 | Tasks 13, 17, 19: byte/subject/companion tampering and publication membership | +| TH-2 | T-8, T-9, T-10 | Tasks 17–20: independent source-policy acquisition, roots, historical identity and offline limits | +| TH-3 | T-2, T-3, T-4, T-5 | Tasks 3, 7–11: source/common-Git/env audit, actual lifecycle/compile denial, native and JS coverage | +| TH-4 | T-7 | Tasks 3, 14, 16: unlisted/self-approved RC, remote-ref substitution, governance bypass, skipped/wrong-SHA gates | +| TH-5 | T-4, T-7 | Tasks 8, 12, 15: authenticated tools/full closure, real boundary, current-run transfers and separated signer | +| TH-6 | T-9, T-11, T-12 | Tasks 18–21 and linked AC-8: exact gh bootstrap, retained substitution, bounded children, manual equality/recovery | +| TH-7 | T-7, T-8, T-9 | Tasks 15–19: exact certificate/source/caller/run semantics and immutable membership; GitHub remains a trust root | + +## Focused Rust test selection guard + +Before accepting any filtered private-module run, require an actual selected test. Register producer modules/tests before filters such as `documents::tests`, `preflight::tests`, `source::tests`, `materials::tests`, `process::tests`, `boundary::tests`, `preparation::tests`, `javascript::tests`, `qualification::tests`, `recipe::tests`, `inspect::tests`, `composition::tests`, `spdx::tests`, `collection::tests`, `distribution::tests`, `publication::tests` and `package::tests`. For Tasks 17–20 select `trusted-server-cli` and its registered `release::tests::{documents,gh,verification,offline}` filters instead. An intended compile failure for missing behavior can establish the first red state; successful zero selection never can. + +```sh +set -eu +HOST_TARGET=$(rustc -vV | sed -n 's/^host: //p') +test -n "$HOST_TARGET" +RELEASE_TEST_PACKAGE=trusted-server-release +RELEASE_TEST_FILTER=documents::tests +RELEASE_TEST_LIST=$(mktemp) +trap 'rm -f "$RELEASE_TEST_LIST"' EXIT +cargo test -p "$RELEASE_TEST_PACKAGE" --target "$HOST_TARGET" \ + "$RELEASE_TEST_FILTER" -- --list > "$RELEASE_TEST_LIST" +awk '/: test$/ { count++ } END { if (count == 0) { print "Focused release test filter selected zero tests"; exit 1 } print "Selected " count " release tests" }' "$RELEASE_TEST_LIST" +cargo test -p "$RELEASE_TEST_PACKAGE" --target "$HOST_TARGET" "$RELEASE_TEST_FILTER" +``` + +Set `HOST_TARGET=$(rustc -vV | sed -n 's/^host: //p')` before each independent shell session/filter. `$APPROVED_SOURCE_COMMIT` MUST come from independent source approval, never the release index. These Cargo commands build a local developer tool only. Qualified production wrappers MUST select the independently approved source and OS-native toolchain described above. The ordinary `test-release-native` CI matrix on Ubuntu 24.04/macOS 15 directly runs all native helper tests, clippy/all-targets/all-features, documentation and actual nonzero native JS `release_prebuilt` tests/clippy. The prior WASI alias selecting zero native tests supplies no coverage. This source-built CI matrix is not production bootstrap or qualification proof. Future aliases require separate review; adapter/CLI aliases do not cover the helper. The explicit host commands below remain required until aliases are reviewed/wired. Never use default WASI output to bootstrap a native producer. + +## Full implementation verification commands + +Run from the repository root with pinned tools and installed npm dependencies. Linux CI uses its host triple; macOS can use the CLI helper. Record each command independently so a later command cannot mask an earlier failure. + +```sh +cargo fmt --all -- --check +cargo clippy-fastly +cargo clippy-axum +cargo clippy-cloudflare +cargo clippy-cloudflare-wasm +cargo clippy-spin-native +cargo clippy-spin-wasm +cargo clippy-cli +cargo clippy-codegen +cargo test-fastly +cargo test-fastly-reuse +cargo test-axum +cargo test-cloudflare +cargo test-spin +cargo test --manifest-path crates/trusted-server-integration-tests/Cargo.toml --test parity +./scripts/test-cli.sh +cargo test -p trusted-server-core --test template_build_digest +cargo clippy -p trusted-server-core --test template_build_digest -- -D warnings +scripts/lint-wasm-clock.sh +HOST_TARGET=$(rustc -vV | sed -n 's/^host: //p') +cargo test -p trusted-server-release --target "$HOST_TARGET" --all-features +cargo doc -p trusted-server-release --target "$HOST_TARGET" --no-deps --all-features +cargo test -p trusted-server-js --target "$HOST_TARGET" --test release_prebuilt +cargo clippy -p trusted-server-js --target "$HOST_TARGET" --all-targets --all-features -- -D warnings +cargo clippy -p trusted-server-release --target "$HOST_TARGET" --all-targets --all-features -- -D warnings +``` + +Run the remaining commands with the repository-pinned Node/npm selected explicitly (subdirectory shell startup can otherwise pick a different Node): + +```sh +(cd crates/trusted-server-js/lib && node build-all.mjs && TRUSTED_SERVER__RELEASE_JS_TESTS=1 npx vitest run && npm run format) +(cd docs && npm run lint && npm run format && npm run build) +docs/node_modules/.bin/prettier --config docs/.prettierrc --check "*.md" ".claude/**/*.md" ".github/**/*.md" "crates/**/*.md" "scripts/**/*.md" "scripts/**/*.mjs" "tinybird/**/*.md" +RELEASE_HOST_TARGET="$(rustc -vV | awk '/host:/ { print $2 }')" +cargo run -p trusted-server-cli --target "$RELEASE_HOST_TARGET" -- dev lint domains --staged +git diff --check +git diff --cached --check +``` + +Stage the reviewed implementation files before the staged-domain check; each grouped command stops at its first failure. New producer/schema dependencies MUST be installed at their exact reviewed versions first. No bare workspace cargo test/build or global all-features clippy. + +## Traceability and milestone exits + +| Requirements | Tasks / principal negative proof | Acceptance | +| --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- | ---------- | +| R-COHORT, R-PLATFORM, R-VARIANT | 1,3,8,9: exact cohort/profile, incompatible native runtime, unsupported reuse | AC-1, AC-2 | +| R-CONFIG, R-INPUTS, R-ENV | 3–5,7,9: operator inputs, unexpected source files, skips/overrides, source integrity | AC-2 | +| R-JS | 5–7,10: locked A/B, inventory joins, input versus retained bytes | AC-3 | +| R-BUILD, R-REPEAT, R-NATIVE | 8–12: actual confinement, complete wasm equality, valid transfers, honest native outcome | AC-3 | +| R-SBOM | 10,11,13,19: real Rust/native/JS coverage and SPDX root/predicate equality | AC-4 | +| R-FACTS | 4: strict full source, owning target/variant, external tag and unchanged service identity | AC-5 | +| R-SOURCE, R-SIGN, R-PUBLISH | 14–16: approved lightweight main/RC, exact-commit gates, isolated signer, immutable reconciliation | AC-6 | +| R-INDEX, R-EVIDENCE, R-POLICY | 2,13,17,19: schema/semantic joins, subjects and independent policy identity | AC-4, AC-7 | +| R-BOOTSTRAP, R-BOUNDS, R-PUBLICATION | 18–20: trustworthy first use, limits/pipe pressure, exact REST/signed membership | AC-7 | +| R-OFFLINE | 19,20: approved build proof only; offline official deploy rejected | AC-7 | +| R-DEPLOY/R-PACK manual portion, R-MANUAL | 19,21: verified bytes before mutations, explicit manual limits and live submission | AC-9 | +| R-DEPLOY/R-PACK integrated portion, R-CONTEXT, R-EDGEZERO, R-RECOVERY | Linked upstream plan, not first-release closure | AC-8 | +| Operator/CLI/changelog guidance | 20–22: source-header identity, bootstrap and manual-route instructions (D8) | AC-9 | +| R-FOLLOWUP | 1,20–22: explicit unsupported adapters/reconstruction/offline; January supersession | AC-10 | + +A completed task means its described tests pass and reviewed code exists, not merely that its checkbox was checked. Recipe pins/qualification are concrete outputs of Task 8, not assumed values. An unavailable required host blocks the selected contract; it does not authorize weakening it. For optional CLI B only, bounded unavailability is an honest nonblocking result. + +## Planning-only verification and handoff + +For these Markdown plans, validate formatting, links, shell syntax, requirement coverage, task dependencies and independent review. Do not claim that runtime tests verify implementation that has not been written. Continue in the existing main checkout on branch `spec/1235-release-attestation` and PR #1262. Automated review is not maintainer approval. diff --git a/docs/superpowers/plans/2026-10-13-cli-packaging-attestation-integration.md b/docs/superpowers/plans/2026-10-13-cli-packaging-attestation-integration.md new file mode 100644 index 0000000000..23ecf8caf5 --- /dev/null +++ b/docs/superpowers/plans/2026-10-13-cli-packaging-attestation-integration.md @@ -0,0 +1,112 @@ +# CLI package attestation integration Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Make installable CLI packages verifiably contain the canonical attested executables, without creating a second release authority or weakening #1235 evidence. + +**Architecture:** Consume final canonical Build A executables and their qualified evidence; packaging never recompiles or alters those bytes. A separately signed package inventory binds outer package digests to exact executable members. One publication job uploads canonical and selected package evidence before publishing a new immutable tag. + +**Tech Stack:** Existing #1292 shell helpers/nfpm packaging design, the host-only Rust release tool's strict metadata and bounded package/archive modules, SPDX canonical executable evidence, pinned GitHub attest/gh contracts from the primary plan. + +--- + +The [Rust migration addendum](../specs/2026-10-14-release-tooling-rust-migration.md) applies to every new packaging producer helper. Extend private `crates/trusted-server-release/src/package.rs` and `distribution.rs` modules; retain thin POSIX/nfpm adapters, no introduced Python or generic packaging framework. Native independently approved-source bootstrap and all exact executable/acquisition checks remain required. Unit tests stay private alongside modules; fixture data uses the crate's `tests/fixtures/` and public process tests use `tests/commands.rs`. + +## Inputs and prerequisites + +Read the [primary release plan](2026-10-13-attested-release-implementation.md), [#1235 contract](../specs/2026-10-09-1235-release-attestation-design.md), PR #1292's packaging spec/plan and its final reviewed revision. At planning time #1292 is documentation only; do not assume its workflow/helper paths exist. Fetch or stack the owning changes before editing them. This plan supplements that plan rather than copying its independent publish job. + +The governing draft #1235 index v1 covers three raw CLI binaries plus Fastly, no package-inventory reference and no Linux ARM64. The package integration therefore requires a reviewed additive contract and new schema version. Task P1 MUST approve it before P2–P7 implementation. Neither package provenance alone nor SHA256SUMS satisfies the executable's SPDX/provenance graph. + +**Selected initial integration:** preserve the existing three qualified CLI targets. Produce three Linux x86_64 installer formats and two macOS archives (five packages). This deliberately does not complete #863's Linux ARM64 requirement. Task P8 independently extends canonical qualification/SBOM/policy to ARM64 before publishing its three packages. If maintainers require all eight installer assets in the first packaging release, P8 becomes a prerequisite before that release; no half-qualified ARM asset is slipped into the three-target cohort. + +Initial #1235 release can happen first; packaging appears on a later new tag once its approved contract is enabled. Never backfill packages into an immutable release. No implementation step may remove #1262's raw assets to make room for packages. + +### Task P1: Approve the cross-PR contract and schema migration + +**Modify after owning branch is available:** `docs/superpowers/specs/2026-10-12-863-cli-release-packaging-design.md`, `docs/superpowers/plans/2026-10-12-cli-release-packaging.md`, #1235 contract. +**Create:** `docs/superpowers/specs/schemas/release-index-v2.schema.json`, `cli-packages-v1.schema.json`; keep index v1 immutable for historical verification. + +- [ ] Write a concrete spec delta: one release.yml owner; reusable release-build.yml signer; canonical three CLI profiles plus Fastly unchanged; five initial wrappers; ARM enablement separately selected; package code/member evidence is additive. +- [ ] Agree on #1262's GNU/macOS baselines and real Intel startup. Do not treat Ubuntu 22.04/glibc 2.35 or Rosetta alone as satisfying GNU 2.34/Intel qualification. Keep package ecosystem smoke tests as additional evidence. +- [ ] Resolve version policy explicitly: packaging milestone chooses #1292's tag equals owning CLI Cargo version, with documented maintainer bump/changelog procedure. This is a reviewed narrower packaging prerequisite, not an attempt to embed external tag into binaries. Initial un-packaged #1235 tags need not retroactively adopt it. +- [ ] Approve index v2's optional `package_inventory` reference and its attestation references with exact name/length/digests. Enable it only in the reviewed `phase1-fastly-cli-packaged` cohort and updated consumer policy. Approve `ts release verify --package --expected-source-commit ` as the distinct package-selection interface; `--package` and `--asset` are mutually exclusive. Older v1 consumers reject unsupported v2, with an independently trusted verifier upgrade path documented. +- [ ] Define package inventory records: schema/source/cohort/recipe/policy identity, each package name/format/architecture/size/SHA-256, exact executable member path/mode/size/SHA-256 and canonical raw asset name. Refer to raw asset digests, not release-index digest: index → inventory → package/member MUST NOT form a self-hash cycle. Bundle references are in the index, not inside the inventory that the bundles authenticate. +- [ ] Replace #1292's signed/annotated `git tag -s` release instruction with `git tag v ` followed by the explicit tag push. Validate lightweight object type; a tag signature does not replace the approved source/signing policy. +- [ ] Get the delta approved/merged and update plan baseline. Commit `Align CLI packaging with canonical attestation`. + +**Exit:** No implementer chooses their own signer, platform floor, ARM assurance, archive relationship or versioning rule. + +### Task P2: Consume canonical executables without transformations + +**Modify/create according to #1292 ownership:** `scripts/release/package-linux.sh`, `package-macos.sh`, `packaging/nfpm.yaml`, release tool private `package::tests` and `tests/commands.rs`. + +- [ ] Write tests rejecting wrong source/target/role, source recompilation, strip/sign hooks, unverified raw input, altered member and CLI B output substituted for A. +- [ ] Derive `HOST_TARGET=$(rustc -vV | sed -n 's/^host: //p')`; run `cargo test -p trusted-server-release --target "$HOST_TARGET" package::tests` with the primary plan's nonzero-selection guard; expect missing integration behavior. These local source-built commands do not qualify native bootstrap. +- [ ] Accept only Task 12/13 primary-plan validated A output and identity table. All final stripping/signature transformations occur in the canonical recipe before its digest/SBOM, never in packaging. Remove #1292's independent strip step and compiled-cache restoration from the canonical build path. +- [ ] Add packaging configuration for the qualified x86_64 Linux executable and both Apple executables. Preserve member bytes; set documented installation modes. nfpm/tool acquisition has reviewed version/digest verification, not a floating installer. +- [ ] Test canonical file unchanged before/after all package formats. Commit `Package the canonical CLI bytes without modification`. + +### Task P3: Inspect whole package structure and bind members + +**Extend:** release tool `src/package.rs`, `src/distribution.rs` and private registered tests; add adversarial data under `crates/trusted-server-release/tests/fixtures/packages/` and process cases to `tests/commands.rs`. Proposed fixed commands are `trusted-server-release inspect-cli-package` and `make-package-inventory`, only after P1 approval. + +- [ ] Write tests for nested traversal, links/hardlinks, duplicate members, oversized compressed/decompressed content, extra executable members, wrong architecture/mode, substituted maintainer script, unsupported compression and altered ts bytes. +- [ ] Run `cargo test -p trusted-server-release --target "$HOST_TARGET" package::tests` and `distribution::tests` separately with the nonzero-selection guard; expect absent inspector. +- [ ] Implement bounded format-specific inspection: Debian ar/control/data archives, RPM payload/header, Arch tar and macOS tar. Use maintained parsers/pinned utilities where possible. Each adapter validates allowed installation paths, executable ts, license/readme and explicitly approved metadata/scripts; reject unknown code-bearing entries. A raw tar scan is insufficient for .deb/.rpm. +- [ ] Compare extracted executable bytes against canonical retained SHA-256/size, check 0755 installation mode, then hash the final outer package. Generate deterministic inventory only after every selected wrapper passes. Do not compare an archive digest directly to executable digest. +- [ ] Validate closed package schema and source/target/profile joins with the canonical index data. Reject inventory attempts to approve an unknown raw target. Commit `Bind installer packages to canonical CLI members`. + +### Task P4: Smoke-test installation and provenance-aware extraction + +**Modify:** #1292 smoke helpers/workflow jobs; release tool private package-smoke tests and public process fixtures. + +- [ ] Test that failed install/startup/version or member mismatch blocks the selected package cohort; metadata-only architecture checks do not count as execution qualification. +- [ ] Run `cargo test -p trusted-server-release --target "$HOST_TARGET" package_smoke` with the nonzero-selection guard after registering these cases; expect incomplete checks. +- [ ] Install Linux wrappers in reviewed clean Ubuntu/Fedora/Arch environments, run version/help/config-init outside a checkout, and compare installed /usr/bin/ts to canonical digest. Preserve the GNU compatibility qualification independently of package-manager tests. +- [ ] Extract/run macOS archives on actual qualified ARM/Intel hosts. Rosetta may be extra coverage, never the sole Intel gate. Restore executable permissions without altering bytes; verify loader requirements. +- [ ] Record exact container/tool/OS identities and outcomes. Smoke images MUST be pinned and qualified, not `latest`. Commit `Qualify installed CLI package behavior and identity`. + +### Task P5: Sign package evidence inside the existing trusted boundary + +**Modify:** `.github/workflows/release-build.yml`, release tool `src/distribution.rs`, workflow contract tests. + +- [ ] Test package metadata cannot replace executable SPDX/provenance, wrong signer/action/run, modified outer bytes, inventory-member mismatch and unapproved package subject. +- [ ] Run `cargo test -p trusted-server-release --target "$HOST_TARGET" distribution::tests` and `workflow_contract` separately with the nonzero-selection guard; expect package evidence unsupported. +- [ ] Reuse exact same-revision signer and pinned attest 4.2.2; do not keep #1292's direct v2 provenance action in publish. Package provenance names each final package plus authenticated package inventory as exact subjects. Raw executable proofs still name only their approved executable/companions. +- [ ] Add a signed package-inventory proof and index v2 references for inventory/bundles. Root index is assembled after package proofs exist and covers them; no signature/checksum cycle. For the CLI's component composition, consumers use the executable's SPDX proof. Do not advertise an enclosing installer SBOM unless a separate container-level scope is actually generated. +- [ ] Verify actual pinned certificates/provenance and all wrapper/member edges before publication. Commit `Attest CLI installer and member evidence`. + +### Task P6: Extend consumer policy and bootstrap before v2 publication + +**Modify:** CLI `src/release/policy.rs`, `documents.rs`, `evidence.rs`, `release-policy.json`, release verification/CLI tests, `docs/guide/release-verification.md`, packaging install guide. + +- [ ] Add tests for v1 historical acceptance, v2 package selection, unknown package/version, altered inventory/member, verified outer package with unverified executable and the inverse. +- [ ] Run CLI release unit/process tests; expect unsupported v2. +- [ ] Implement the reviewed v2/member policy without letting a downloaded index select arbitrary package kinds or actions. Verify package signature/official membership, authenticate inventory, verify raw executable/SBOM graph and prove exact installed/extracted member equality before recommending installation/execution. +- [ ] Implement the exact package-selection syntax approved in P1; do not overload `--asset` silently to turn arbitrary archives into canonical executable subjects. Use the same internal verification library, with a distinct verified-package result and retained-member identity. +- [ ] Qualify independently trusted source/previous-verifier bootstrap for first v2 use. A CLI that only supports v1 cannot automatically authorize its own unverified upgrade. Commit `Verify packaged CLI members under reviewed v2 policy`. + +### Task P7: Integrate one complete immutable publication + +**Modify:** `.github/workflows/release.yml`, package release helpers, `CONTRIBUTING.md`, `CHANGELOG.md` and tests. + +- [ ] Test incomplete wrappers, mixed source versions, package-only draft, missing raw/SBOM assets, an existing immutable v1 tag and uncertain publish response. +- [ ] Run `cargo test -p trusted-server-release --target "$HOST_TARGET" publication::tests` with the nonzero-selection guard; expect package cohort unsupported. +- [ ] Publish exact canonical plus selected wrapper assets/evidence/checksums through the single owner. No independent #1292 publish job races or finalizes the tag first. Require all selected wrapper gates, complete downloads/joins and post-publication immutable checks. +- [ ] Rehearse the whole v2 release and trusted package install in an authorized disposable repo, then run all AGENTS gates and producer/consumer tests. Only a separately authorized new production tag follows. +- [ ] Commit `Publish canonical and packaged CLI evidence together`. + +### Task P8: Qualify Linux ARM64 before expanding packaging scope + +**Modify through a separate reviewed extension:** cohort, tools, versioned schema, consumer policy, native recipe/composition fixtures and package matrix. + +- [ ] Obtain approval for an aarch64-unknown-linux-gnu canonical executable, provenance-sbom profile and qualified compatibility floors. Until then it is unsupported, not a weaker undocumented extra asset. +- [ ] Build actual ARM64 A with fresh JS/materials, validate native/external components, generate full SPDX and attest it under the same policy. Native B remains a bounded optional measurement unless explicitly promoted. +- [ ] Run startup on actual ARM64 baseline hosts and each claimed package ecosystem. If an Arch ARM64 runtime cannot be qualified, either retain an explicit metadata-only distribution limitation approved in the packaging spec or defer that package; do not advertise fully smoke-tested execution. +- [ ] Approve the `phase1-fastly-cli-arm64-packaged` cohort with five canonical executables and eight wrappers, and update the versioned schema/producer/consumer expected sets before runs; repeat P2–P7 for ARM packages. A failed selected ARM A/SBOM/runtime gate blocks the expanded cohort, with no after-failure fallback. +- [ ] Commit `Qualify and attest the ARM64 CLI distribution`; close #863 only when its approved platform/package acceptance is satisfied. + +## Completion and review + +P1–P7 produce verifiable packages for the initially qualified targets on a new release. P8 completes the original all-four-platform packaging intent after qualification. Packaging does not weaken or retroactively expand existing #1235 releases. Run each new focused test first, then the primary plan's full AGENTS command list and package-manager/actual-host qualification. Review the complete evidence graph, source/tag/version consistency and first-use bootstrap before publication. diff --git a/docs/superpowers/plans/2026-10-13-fastly-prebuilt-edgezero-integration.md b/docs/superpowers/plans/2026-10-13-fastly-prebuilt-edgezero-integration.md new file mode 100644 index 0000000000..bbbc5d41f9 --- /dev/null +++ b/docs/superpowers/plans/2026-10-13-fastly-prebuilt-edgezero-integration.md @@ -0,0 +1,125 @@ +# Fastly prebuilt EdgeZero integration Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add byte-preserving online production/staging deployment of verified Fastly wasm through a typed EdgeZero API with durable recoverable observations. + +**Architecture:** Trusted Server owns official verification, operator authorization and a private retained artifact. EdgeZero owns Fastly packaging/transport, observed context and concrete lifecycle operations. A durable operation sink records intent before every write and response afterward; uncertain provider state is reconciled read-only before any retry. + +**Tech Stack:** Existing Rust/error-stack consumer, reviewed upstream EdgeZero/Fastly CLI 15.1.0 or qualified API transport, restricted atomic local journal, provider-free runner fixtures and authorized disposable Fastly qualification. + +--- + +## Authority and prerequisites + +Read the [primary plan](2026-10-13-attested-release-implementation.md) and [contract R-DEPLOY through R-RECOVERY](../specs/2026-10-09-1235-release-attestation-design.md#r-deploy). This is AC-8, a separate integration milestone, not an initial #1235 closure condition. It depends on the primary verifier/retained artifact, package inspector and public qualification; it may be prepared upstream in parallel but cannot ship an official release path without that verifier. + +Baseline EdgeZero revision is `683202c66948146ec360f490126f1d60f920288b`. Its generic adapter operation returns unit, source deploy can execute manifest shell commands, and Fastly staging builds/autoclones before staging. These are reference mechanisms, not an existing typed prebuilt seam. + +Use a separate upstream EdgeZero checkout/worktree; do not edit Cargo's shared cached checkout. Upstream relative paths below belong to that repo. After review, update Trusted Server's Cargo pin/lock rather than vendor-patching the cache. + +## Proposed API boundaries + +Keep small concrete APIs, with default unsupported behavior for other adapters. Do not add speculative Cloudflare/Spin/Axum payload variants. + +The owning upstream design MUST implement these concrete records (not empty adapter variants): + +| Record / interface | Required fields / behavior | +| -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `PreparedFastlyPackage` | Private retained archive handle/lifetime, checked archive length/SHA-256, canonical module length/SHA-256, deployment-only manifest SHA-256 and inspected package name | +| `PrebuiltFastlyRequest` | Strong service ID, exact base version, production/staging mode, prepared payload and resolved context/sharing policy | +| `WriteIntent` | Stable operation ID, monotonically increasing sequence, phase (`clone`, `upload`, `relink`, `selector-write`, `stage`, `activate`), exact resource/version where known, request identity digest | +| `WriteObservation` | Matching operation/sequence/phase, actual response resource/version, known/unknown result and sanitized evidence references | +| `OperationSink::before_write(&WriteIntent)` | Synchronous concrete fallible result; caller MUST flush durable intent and failure forbids the following mutation | +| `OperationSink::after_response(&WriteObservation)` | Synchronous concrete fallible result; failure stops subsequent writes while preserving already attempted effects | +| `PrebuiltOutcome` | Exact module/package/service identities, optional exact version, last confirmed phase, mutation-attempted flag and per-phase known/unknown results on success and error; no inferred active-version success | + +E1 follows the owning repository's AGENTS.md for concrete error conventions and visibility. A result-only deploy method is insufficient: it cannot let the caller flush a journal before hidden internal link/selector mutations. No publisher signing or universal attestation registry is introduced. + +### Task E1: Approve upstream API and operation-state contract + +**Upstream modify:** `crates/edgezero-adapter/src/registry.rs`, `crates/edgezero-cli/src/lib.rs`. +**Upstream create:** `crates/edgezero-adapter/src/prebuilt.rs`, `crates/edgezero-adapter-fastly/src/prebuilt.rs`. + +- [ ] Write fake adapter tests for unsupported adapter/action, payload/context mismatch, mutation attempts before authorization, unit-success ambiguity and failed durability sink, truncated payload, pre-send failure versus sent/no-response uncertainty and missing exact version. +- [ ] Run upstream targeted prebuilt tests; expect missing API. +- [ ] Define private retained payload identity, exact service/base version, action/mode, logical/physical mappings, staging selector identity and sharing policy. Return structured observed IDs/phase states and error outcomes with known/unknown state, not an active-version lookup. +- [ ] Define explicit phase enum and WriteIntent/WriteObservation with stable operation/sequence IDs; before_write MUST synchronously succeed before every provider/config mutation. Journal persistence errors halt further writes, even after earlier successful writes. +- [ ] Approve exact-source Fastly unsupported defaults and review public API/docs/error types. Commit `Add a typed prebuilt Fastly operation interface`. + +### Task E2: Prepare byte-preserving packages from retained inputs + +**Upstream modify/create:** Fastly `src/prebuilt.rs`, `src/prebuilt/package.rs`, provider-free package fixtures. + +- [ ] Write tests for retained-file/path replacement, changed package/member/manifest, generic archive selection, local-server config leakage, extra executable, traversal and unsafe names. +- [ ] Run package tests; expect no validated prepared-payload constructor. +- [ ] Adopt the primary plan's exact deployment-only manifest and bounded four-member inspection semantics. Rehash retained wasm, manifest and selected archive before transport. Never run source build, compute publish, annotation/strip/sign hooks or arbitrary manifest shell overrides. +- [ ] Keep module/archive digests distinct. Public callers cannot construct prepared identity from arbitrary asserted hashes; constructors validate owned bytes and context. +- [ ] Run byte-retention/adversarial fixtures against pinned real pack output. Commit `Prepare verified Fastly packages without rebuilding`. + +### Task E3: Resolve and validate actual deployment context + +**Upstream create:** Fastly `src/prebuilt/context.rs`; modify existing selector/link helper seams only as required. + +- [ ] Test declared logical IDs vs observed links mismatch, wrong selector/config key, unknown secret-reference metadata, conflicting shared staging selector and disallowed resource-sharing policy. +- [ ] Run context tests; expect absent observed-context validation. +- [ ] Resolve service/base version and fetch read-only provider links/selector state. Validate repository logical IDs, selected physical links/key, runtime selector entries and required secret-reference metadata. Secret values are write-only; do not claim plaintext readback or log/store resolved secrets. +- [ ] Declare which KV/secret/external-effect resources staging shares versus isolates. Config-selector isolation alone does not imply all resource isolation. Validate resulting exact draft again before stage/activation. +- [ ] Commit `Validate observed Fastly deployment bindings` after fake-provider negatives pass. + +### Task E4: Implement exact clone/upload/context/stage or activation + +**Upstream create:** Fastly `src/prebuilt/lifecycle.rs` and fake provider/command tests. + +- [ ] Write sequence tests: verify/package/context before first write; explicit clone returning draft ID; upload to that ID without autoclone; exact draft readback; stage or activate only that ID. Test same-hash skip, partial response and unsupported staged request. +- [ ] Run lifecycle tests; expect absent state machine. +- [ ] Invoke OperationSink before each mutation, including clone, upload, resource relinks, selector writes, stage and activation. Use explicit version update rather than compute deploy's automatic activation or combined update/autoclone. +- [ ] Capture returned IDs from operation responses. Require validated resulting context before activation; staged success reports staged, not activated. Never guess success from exit zero or whichever service revision is currently active. +- [ ] Stop on sink/provider uncertainty and preserve last known observation. Commit `Deploy prepared Fastly packages through exact lifecycle phases`. + +### Task E5: Implement durable journal and scoped serialization + +**Trusted Server create:** CLI `src/release/deployment/mod.rs`, `journal.rs`, `context.rs`; internal tests and `tests/release_deploy.rs`. + +- [ ] Write crash-injection tests before send/after send/before response, failed journal flush, receipt path replacement, process restart, shared-selector races and loss of temporary files. +- [ ] Run CLI deployment tests; expect absent journal integration. +- [ ] Implement persistent restricted storage, stable operation ID, private retained evidence/package/context and atomic state writes. Flush file plus directory at each durable intent/response boundary. A TempDir that disappears on process exit is not recovery storage. +- [ ] Lock by service and relevant shared-resource identity across processes/environments; define stale-lock inspection/recovery without blindly deleting locks. Preserve production previous-version observation before mutation. +- [ ] Record exact known/unknown state, policy/source/module/package hashes and sanitized context identities; no active config, token or resolved secrets. Deployment-only health is not_performed. Commit `Persist deployment intent and recovery observations`. + +### Task E6: Reconcile uncertainty without guessing or replaying mutations + +**Upstream create:** Fastly `src/prebuilt/reconcile.rs`; Trusted Server internal journal reconciliation tests. + +- [ ] Test known draft inspection, lost first-clone response, multiple indistinguishable candidate versions, changed selector ownership, unknown upload/activation and receipt failure after success. +- [ ] Run reconciliation tests; expect missing read-only recovery. +- [ ] Query exact known resources/versions where IDs exist; attach observations to the saved operation. A lost clone ID without provider idempotency cannot always be recovered. Keep it unknown; do not adopt a candidate by timestamp/package resemblance or mutate a guess. +- [ ] Require explicit operator-approved reconciliation before another write. Do not automatically rollback after uncertain state, bypass serialization, or replay create/clone on restart. Health and rollback remain separate operations consuming known exact identities. +- [ ] Review any public recovery command separately before adding it; internal support and runbook are required here. Commit `Reconcile uncertain Fastly operations without replay`. + +### Task E7: Review upstream revision and integrate release arguments + +**Trusted Server modify:** root `Cargo.toml`, `Cargo.lock`, CLI `src/run.rs`, `src/commands/mod.rs`. +**Create:** CLI `src/commands/deploy.rs`, `src/release/deployment/transport.rs`. + +- [ ] Land reviewed upstream tests/docs, pin its exact revision and rerun source deploy regression tests. Do not modify the shared Cargo cache or float a branch pin. +- [ ] Write command tests for source deployment preserved, release/passthrough/package/source conflicts, unsupported offline/adapter/variant, failed verification before journal/provider writes and unavailable prebuilt capability. +- [ ] Add wrapper flattening existing DeployArgs plus reviewed release/source/receipt inputs. Source mode delegates as before. Release mode selects a compatible retained VerifiedArtifact, validates local action/context and invokes only the typed prepared path. No fallback to a source build. +- [ ] Keep producer artifact capability and consumer implemented transport intersection: initially production only. Enable fastly-staging only after E8 qualification and reviewed cohort/policy change; old releases do not gain it by downloaded metadata. +- [ ] Run CLI source/release regressions and all AGENTS gates. Commit `Integrate verified release deployment with EdgeZero`. + +### Task E8: Qualify production/staging, recovery and operator guidance + +**Modify:** CLI/release guide, changelog and evidence records; upstream full validation. + +- [ ] Execute fake-provider coverage for all writes, crash boundaries, context mismatches, locks and unknown results; verify no provider writes occur on failed official evidence. +- [ ] Before enabling integrated production transport, execute end-to-end `ts deploy --release` production qualification on a separately authorized disposable Fastly service using an independently verified production-capable release. Assert canonical module bytes, explicit clone/upload to the exact returned draft, context validation before activation, the exact activated version, durable per-write observations and receipt/restart behavior. The primary manual compute-deploy rehearsal is a different transport and cannot satisfy this gate. +- [ ] First qualify direct upstream prepared-byte staging on a separately authorized disposable Fastly service, using source-built fixtures and no claim of an official staging-capable release. Compare actual module/context and shared/isolated bindings, plus separate health/rollback commands. The initial production-only CLI/release MUST continue to reject staging before writes. +- [ ] After that transport proof, approve the cohort and embedded policy capability change, then publish a separately authorized new staging-enabled release through the primary pipeline. Use the independently trusted verifier to qualify end-to-end `ts deploy --release --staging` against that new release. Test that historical production-only releases still reject staging; no downloaded metadata or test policy override enables it in the production CLI. +- [ ] Qualify durable recovery across process restart and uncertain responses without exposing secrets. An irreducibly unknown clone remains a documented manual resolution case, not a passed exactly-once guarantee. +- [ ] Record the already reviewed/qualified staging capability and historical producer/consumer compatibility explicitly. Document receipt limits and the actual loaded snapshot boundary for #161; unsigned operator observations are not publisher authority or remote execution proof. +- [ ] Run upstream tests and all Trusted Server AGENTS gates, independent staff review, then hand off for maintainer deployment approval. Commit `Qualify integrated Fastly artifact deployment and recovery`. + +## Exit criteria + +AC-8 is met only when reviewed upstream code is pinned, production/staging preserve canonical bytes, actual context is validated before finalization, durability precedes every write, exact/unknown outcomes survive failure and live disposable qualification is recorded. No other adapter, publisher claim signer, remote execution attestation or generic deploy framework is implied. diff --git a/docs/superpowers/plans/binary-attestations-phase1-compatibility.md b/docs/superpowers/plans/binary-attestations-phase1-compatibility.md new file mode 100644 index 0000000000..7cd8d2e67f --- /dev/null +++ b/docs/superpowers/plans/binary-attestations-phase1-compatibility.md @@ -0,0 +1,305 @@ +# Phase 1 compatibility audit and approved alignment + +**Audit baseline:** reviewed #1262 at `122b78664b1235c717e9208fd200f9a9b0c335df` versus #1435 at `4dd217125c6e38055e4bcb2cba32c0b95e739914`. The table records the audit of those commits. The approved status below supersedes earlier implementation suggestions in the audit; work in progress is not a claim of completed release qualification. The published SBOM format is **SPDX 2.3**. Historical rehearsal `.5` passed at `937214a632f9387601dba94f996de78b2e6ee424`; the rehearsal history below records earlier outcomes. + +**Conclusion:** the original Phase 1 signer path and CycloneDX public format would have required a later breaking policy replacement. Fix those now. Phase 1 is the first implementation slice of the existing contract: retain its public names, signer/caller distinction, SPDX semantics and original policy files. Introduce no competing release-index schema or weakened consumer policy. + +Frozen source references below mean: + +- [Spec](https://github.com/IABTechLab/trusted-server/blob/122b78664b1235c717e9208fd200f9a9b0c335df/docs/superpowers/specs/2026-10-09-1235-release-attestation-design.md), especially R-COHORT/R-PLATFORM/R-SOURCE/R-SIGN/R-PUBLISH/R-INDEX/R-EVIDENCE. +- [Plan Tasks 15–16](https://github.com/IABTechLab/trusted-server/blob/122b78664b1235c717e9208fd200f9a9b0c335df/docs/superpowers/plans/2026-10-13-attested-release-implementation.md#task-15-orchestrate-independent-builds-and-isolated-signing). +- [Cohort](https://github.com/IABTechLab/trusted-server/blob/122b78664b1235c717e9208fd200f9a9b0c335df/scripts/release/release-cohort.json), [tools](https://github.com/IABTechLab/trusted-server/blob/122b78664b1235c717e9208fd200f9a9b0c335df/scripts/release/release-tools.json), [consumer policy](https://github.com/IABTechLab/trusted-server/blob/122b78664b1235c717e9208fd200f9a9b0c335df/crates/trusted-server-cli/release-policy.json). +- [Index v1 schema](https://github.com/IABTechLab/trusted-server/blob/122b78664b1235c717e9208fd200f9a9b0c335df/docs/superpowers/specs/schemas/release-index-v1.schema.json), [distribution names](https://github.com/IABTechLab/trusted-server/blob/122b78664b1235c717e9208fd200f9a9b0c335df/crates/trusted-server-release/src/distribution.rs#L55), [consumer companion rules](https://github.com/IABTechLab/trusted-server/blob/122b78664b1235c717e9208fd200f9a9b0c335df/crates/trusted-server-cli/src/release/documents.rs#L236), [SPDX joins](https://github.com/IABTechLab/trusted-server/blob/122b78664b1235c717e9208fd200f9a9b0c335df/crates/trusted-server-cli/src/release/evidence/joins.rs#L188). + +### Phase 1 review notes — rehearsal .9 passed + +Phase 1 is **part of #1235**, pending D7 maintainer settings. These notes supersede +historical phase applicability, not the retained destination requirements. +Goals, Non-goals, R-COHORT and R-BOUNDS refer to the original four-artifact design; +this slice explicitly includes seven binaries / 29 files, including Axum and +Linux ARM64. Its original four public names stay unchanged; no v1 index is emitted. +The Node producer helpers and inline publisher are interim exceptions to the +retained Rust-producer constraint, replaced by slices 2.5 (recipes/inspection), +2.6 (composition/SPDX) and 2.8 (publication). The frozen Status and tool versions +below describe #1262, not current Phase 1 implementation status. + +Current tool pins: Rust 1.98.1, Node 24.20.0, Fastly CLI 16.0.0, Viceroy 0.21.0; +verification uses GitHub CLI 2.102.0. The reusable release-build.yml is the signer. Rehearsals use GitHub CLI 2.102.0, Fastly CLI 16.0.0 and Viceroy 0.21.0; the carried destination spec/plan still mention GitHub CLI 2.97.0 and Fastly CLI 15.1.0. Those carried versions are unchanged. +[Rehearsal `.9`](https://github.com/IABTechLab/trusted-server/pull/1435#issuecomment-6364827112) passed at exact implementation SHA `149754d357901ec874eab6140e92bce3d8613085`. Subsequent review-head updates are **docs-only since rehearsed `149754d35`**. Rehearsal `.8` remains historical evidence for its own implementation. Rehearsal `.6` at `39a769c3e` failed after immutable publication despite successful independent verification; `.7` at `7a05940e2` was cancelled before signing/publication because GitHub could not assign a macOS ARM runner. The compatibility audit retains those failed outcomes. + +Slice 2.3 immutable build facts are reused from introducing commit +`bfb697404551a28217af8906024506095bcd9559` and revision +`6d0efda7210983021ab3c9efe8a0a3190e9eb81b`, as retained at the frozen source. +Core's resolver and new CLI/Fastly owner build scripts validate +`TRUSTED_SERVER__RELEASE_BUILD=1` and the full lowercase source SHA. +Cargo supplies the owning target/package; features supply the variant. Tags and +run IDs stay external. CLI now labels package/source/target/variant; Fastly queues +the same immutable facts through existing startup diagnostics in reuse mode only; single mode retains the source response header without per-request build-fact logs. The source audit +permits exactly this release control, GIT_VERSION and Cargo-owned BUILD_TARGET, +not deployment values. CLI source-resource cleanup remains deferred. + +Axum binding is runtime-only: `EDGEZERO__ADAPTER__HOST` defaults to loopback; +containers can set `0.0.0.0`, with runtime `EDGEZERO__ADAPTER__PORT` or the compatible `PORT` override. Both values are validated, and conflicting values fail startup. The smoke uses +bridge networking and reaches the process through a published host port. +TLS termination, graceful shutdown and request/resource limits remain follow-up. + +Publication requires `RELEASE_PUBLICATION_ENABLED=true` only after maintainers +confirm immutable releases and the v\* tag ruleset. There is no settings token or +prepublication settings API call, following the reviewed signing boundary; +postpublication immutable=true readback fails loudly. Hyphenated tags are +prereleases and never Latest. Stable tags are normal releases with `make_latest: legacy`; GitHub selects Latest by version/date, so backports do not force Latest. Eligibility of +rc-branch tags is an unresolved maintainer decision; Phase 1 does not implement +the destination branch-ancestry/ruleset validator or authorize rc tags. + +## Approved scope after audit + +This document records the alignment decisions. Carried requirements remain +unchanged; the phasing sections describe this slice and its rehearsal outcome. The frozen source remains the destination contract. The pre-correction +Phase 1 audit baseline is `4dd217125c6e38055e4bcb2cba32c0b95e739914`; the +following corrections are implemented in this slice, with verification and +rehearsal tracked in the PR description at its current commit. Signing and +immutable publication rehearsal `.9` passed at `149754d357901ec874eab6140e92bce3d8613085`; see the +[full verification transcript](https://github.com/IABTechLab/trusted-server/pull/1435#issuecomment-6364827112). + +The current correction aligns the local input-free `release-build.yml` signer +with the `release.yml` caller/publisher, real SPDX 2.3 documents, fourteen +named bundles, seven binary names and `SHA256SUMS`: 29 assets. Each binary has +provenance and an SPDX predicate attestation; each signed SPDX predicate matches +its published companion. No missing evidence +subjects, index, inventory, recipe or rebuild results are fabricated. Exact +artifact selection and signing-approved digest checks replace wildcard merging. +The publisher lists all releases including drafts, refuses a matching tag, +retains the created release ID for mutations, performs no automatic mutation +retries and verifies immutable membership after publication. + +The tools and consumer policy files remain unchanged. The cohort adds the +three descriptive Axum entries below; all three policies still have **no Phase 1 +readers**. Checked cohort selection, a supplemental tool policy, environment +normalization/remapping, stricter tag/source validation and full platform qualification remain additive later work. Post-publication asset re-download and byte comparison against signed `SHA256SUMS` shipped and passed in rehearsal `.9`. This corrective batch adds immutable build facts and a verified macOS 13.0 deployment target; UBI GNU glibc 2.34 checks and +startup smokes remain. Copying a +destination policy alone never establishes its qualification requirements. Native reproduction is a later measurement, not a +Phase 1 release gate. Full Task 16 resume/reconciliation remains deferred. + +The [operator guide](/guide/release-verification) describes the corrected +verification procedure and qualification limits. Its commands pin the reusable +signer and exact tag certificate in separate mutually exclusive flag checks. +Future companions and stronger verification remain additive; existing immutable +releases and approved tags are never overwritten or reinterpreted as complete +qualified evidence. + +## Stable public contracts: audit and adoption + +| Contract point | Frozen #1262 value | #1435 value at audit | Alignment / adoption | +| --------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Release caller | `release.yml`; local reusable call at the same source revision (R-SIGN, Task 15, consumer policy identity) | `release.yml` owns all jobs | Keep caller/publication in `release.yml`; move build orchestration and signing into local input-free `release-build.yml`. | +| Signing workflow identity | SAN/builder `https://github.com/IABTechLab/trusted-server/.github/workflows/release-build.yml@refs/tags/` | Direct `release.yml` signer and guide pins | Correct now; never broaden frozen policy to accept the wrong caller SAN. Fork rehearsal substitutes only the sandbox repository identity. | +| Caller identity / parameters | `buildConfigURI` names `release.yml`; external workflow `{ref, repository, path}`; no arbitrary commands/inputs (consumer `evidence.rs:128–140,213–240`) | No caller/signer distinction | Input-free `workflow_call`; preserve caller/source/run binding. | +| Workflow job IDs | No prescribed spelling; topology/identity is normative | `build`, `sign`, `release` | Keep compatible job IDs; file ownership and permissions are the contract. | +| Provenance predicate / build type | SLSA `https://slsa.dev/provenance/v1`; `https://actions.github.io/buildtypes/workflow/v1` | SLSA default from pinned action | Preserve; inspect actual rehearsal statements rather than supplying custom provenance. | +| Signer permissions | Build read-only; signer contents-read/OIDC/attestations-write; publisher contents-write only; no artifact-metadata write | Already split correctly | Preserve across reusable boundary; caller grants only needed maxima and called jobs narrow privileges. | +| Run/source binding | Same approved source, caller/signer revision and repository run/attempt (R-EVIDENCE) | Same run, no custom evidence joins | Preserve invocation shape now; full graph joins deferred, not substituted by metadata claims. | +| Exact four raw executable assets | `trusted-server-fastly.wasm`, `ts-aarch64-apple-darwin`, `ts-x86_64-apple-darwin`, `ts-x86_64-unknown-linux-gnu` | Same | Preserve exact names and unarchived executable subjects. | +| Cohort identity / selection | `phase1-fastly-cli`, schema 1, exact package/bin/target/default-feature/variant/role/capability combinations | Names/packages/targets duplicated in YAML/shell; no cohort metadata | Carry original cohort unchanged with no reader. Checked declaration-based selection is additive later work, never an arbitrary command interface. | +| SPDX document / predicate | SPDX 2.3; `https://spdx.dev/Document/v2.3` | CycloneDX 1.5 / `https://cyclonedx.org/bom` | User selected SPDX: generate real SPDX 2.3 and restore the source predicate before publication. | +| SBOM companion name | `.spdx.json` (distribution/consumer hardwired suffix) | `.cdx.json` | Align now; never rename CycloneDX bytes to SPDX. | +| SBOM root/binary join | `SPDXRef-DOCUMENT`; exactly one described root package and one `SHA256` checksum equal to executable bytes | CycloneDX metadata hash | Validate real converted SPDX against the existing root/hash contract. Root ID spelling itself is not hardwired. | +| SBOM predicate/document equality | Executable subject; predicate structurally equals distributed parsed SPDX; file bytes independently hashed | Executable subject is correct; no signed-predicate/file comparison | Preserve subject semantics, reject malformed JSON before signing (duplicate-key rejection is not claimed), and verify the shipped SPDX against the attested predicate. | +| Provenance bundle name | `.provenance.sigstore.json` | Not shipped | Save action bundle and ship under the existing name now. | +| SBOM bundle name | `.sbom.sigstore.json` | Not shipped | Save and ship now; do not introduce a competing suffix. | +| Provenance subject set | Executable, SPDX, build-evidence, build-recipe, rebuild-check (Task 15/R-EVIDENCE) | Executable only | Sign executable + real SPDX in this slice. Add only the real evidence/recipe/rebuild subjects when their owning slices land; do not fabricate companions. | +| Remaining per-asset names | `.build-evidence.json`, `.build-recipe.json`, `.rebuild-check.json` | Absent | Reserve exact existing names; defer contents until the reviewed producers own them. | +| Release-wide names | `release-index.json`, `release-index.provenance.sigstore.json`, `tsjs-inventory.json`, `SHA256SUMS` | Only `SHA256SUMS` | Preserve names; index/inventory remain additive later assets. No alternative Phase 1 index. | +| Release layout | Four binaries + six companions each + four release-wide assets = 32 | Four binaries + four CDX + checksums = 9 | First corrected slice: 4 binaries + 4 SPDX + 8 saved bundles + checksums = 17. Later 12 evidence/recipe/rebuild and 3 index/inventory assets complete the same 32-asset layout. | +| Checksums | `SHA256SUMS` covers every other published asset, never itself | Correct smaller-set rule | Preserve rule, expanding the exact set as real companions are added. | +| Index structural contract | Unchanged closed v1 schema; `{name,size,sha256}` file records and six fixed companion keys | Schema retained, no index emitted | Keep schema bytes unchanged; later emit the existing index, not a replacement schema. | +| Index source/policy joins | Source/recipe/cohort commits equal; producer policy version/hash; bounded exact names/digests | No index or policy identity claim | Defer these graph joins. No invented policy hash or assertion that the full source consumer accepts an index-less release. | +| Consumer policy / actions | Official repository/IDs, readonly verify, fixed bounds and identities | Custom verifier absent | Carry source policy unchanged as destination contract; manual `gh` verification remains supported for this first slice. | +| Candidate qualification markers | Candidate sentinel and `release_qualification:false` are explicitly rejected for official consumption | No candidate records published | Preserve this separation; never repurpose candidate documents as official evidence. | +| Tag syntax / object | Strict schema SemVer, including prerelease/build metadata; lightweight official tags (R-SOURCE) | Any `v*`; guide discusses peeled SHA; first canceled rehearsal tag was annotated | Strict schema/tag-object validation is additive later work. Use a separately approved fresh tag for rehearsal; never reuse `.1`. | +| Eligible source | Exactly `main`, actual remote ancestry/governance plus same-commit validation (R-SOURCE/Task 14) | Manual source approval; no trusted Task 14 execution | Preserve policy `main` unchanged with no reader. Remote ancestry/governance and required-job validation are deferred; maintainer source approval remains required. | + +### Build/policy contracts and explicit scope limits + +| Contract point | Frozen #1262 value | #1435 value at audit | Alignment / adoption | +| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Policy file paths | `scripts/release/release-cohort.json`, `scripts/release/release-tools.json`, `crates/trusted-server-cli/release-policy.json` (`policy.rs:23–27`) | All absent | Carry all three unchanged with no Phase 1 readers. Later checked adoption must not execute source-supplied commands. | +| Policy qualification | Tools, environments and recipes remain `pending`, acquisition/digest fields null | Ordinary hosted build | Preserve all pending/null state. A Phase 1 checksum is not a claim of authenticated Phase 2 qualification. | +| Existing tool versions | Rust 1.98.1, Node 24.20.0, npm 11.19.0, gh 2.97.0; attest 4.2.2 at `1e69f48acb82d1966a394da916b4c1698aa569d6` | Rust/Node match; npm unchecked; ambient gh; attest matches | Preserve current tracked Rust/Node/action pins. Additional policy readers and exact gh/npm enforcement remain additive later work; original tool policy is unchanged. | +| New SBOM tool acquisition | No cargo-cyclonedx/converter entries; strict producer rejects unknown tool names | Cargo generator version/hash hardcoded | Supplemental tool-policy adoption is deferred. Keep existing Phase 1 acquisition checks and carry the original strict tools records unchanged. | +| Package/bin/target/features | Exact cohort entries, default features, no extras | Package/target agree; bin implicit | Preserve current fixed selections. A checked cohort reader and explicit-bin reconciliation are additive later work. | +| Profile and Cargo flags | Release, debug 1, strip none; `--frozen`, structured messages, fixed jobs (`recipe/plan.rs:175–197`) | Release/debug agree; `--locked`, online build, implicit bin/jobs | Keep online locked acquisition/build as the current Phase 1 boundary. Structured output/jobs, explicit bin and source normalization are additive later work; frozen confined execution follows its reviewed owner. | +| Normalized environment | C locale, UTC, source-commit epoch, immutable source facts, controlled AWS-LC/SQLite selection (`preflight/child.rs:82–115`) | Full SHA source fact only | Environment normalization and checked commit epoch are additive later work; no Phase 1 reproducibility claim. | +| Rust/C path remapping | Stable `/trusted-server`, `/cargo-vendor`, `/cargo-home`, `/rust-tools`, `/cargo-target` roots (`recipe/remap.rs`) | None | All additional remapping follows later owned inputs; it does not change names or verification identities. | +| Fresh/private inputs | Private prepared state and target; denied network; authenticated paths | Fresh target; hosted HOME/CARGO_HOME/PATH and network | Keep fresh target and scrubbed environment; prepared/offline/sandbox/tool closure remains the reviewed later slice. | +| JS preparation | Checked prebuilt inventory, shared twice-built modules, no Vite env/config loading | Fresh npm per artifact; private Cargo output; no shared comparison | Keep fresh npm/private Cargo outputs. Additional environment controls, authenticated inventory, two builds and shared contribution evidence remain later work; never invent equality claims. | +| macOS baseline | Deployment target **13.0**, hosted `macos-15` / `macos-15-intel` | Same runners, deployment target **15.0** | This corrective batch targets 13.0 and checks Mach-O CPU/load-command minimum OS. Actual older-host runtime qualification remains later work. | +| Linux baseline | GNU builder maximum **glibc 2.34**, UBI9/Debian12/Ubuntu22 compatibility evidence | Ordinary Ubuntu24 build; 2.39 environment, older compatibility unestablished | Implemented: UBI 9.6 GNU builds, final-byte GLIBC ceiling 2.34, and digest-pinned Debian 12/Ubuntu 22.04/UBI 9 startup checks. Full compiler/native composition qualification remains deferred. | +| Runtime/startup/file inspection | Owning runtime/startup acceptance and exact target classification | Build-only + config canaries | Additional binary type/architecture checks and full runtime qualification remain additive later work with the source inspection owner. | +| CLI immutable build facts | Source-controlled release/source/target/variant facts in CLI output (Task 4) | Existing package-version CLI output | Port the reviewed slice 2.3 immutable facts now, preserving the labelled source-identity schema. | +| SBOM precision | Complete Cargo/native/JS composition and retained-byte joins | Conservative Cargo target/npm build-tree inventory; native/std/runtime gaps | Keep gaps explicit in SPDX and docs. Enrich the same SPDX companion later; do not label the smaller inventory complete. | +| Assurance profiles | Fastly `provenance-sbom-repeatable`; native `provenance-sbom` with full evidence | No repeatability/full evidence | Preserve destination profiles in cohort; do not emit an index/profile claim until its gates hold. Native reproduction remains a measurement; Fastly reproducibility is a later gate. | + +### Publication contract and nonbreaking recovery plan + +| Contract point | Frozen #1262 value | #1435 value at audit | Alignment / adoption | +| ------------------------------- | ---------------------------------------------------------------------------- | --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Publication owner / permissions | `release.yml`, contents-write only (Task 16) | Matches | Preserve; no signer/OIDC/settings credential in publisher. | +| Artifact transfer | Validated current-run outputs, exact bytes/sets (Tasks 12/15/16) | Wildcard `release-*` merge | Fix now: exact named downloads in separate directories, signing-owned digest manifests, reject missing/extra/changed files before creation. | +| Draft lookup/recovery | Published refused; matching draft resumable only after source/assets match | Tag lookup + refuse existing; no full draft listing | List all releases including drafts and refuse every matching tag in Phase 1. Capture created ID for mutations and identity-bound verification. Later matching-draft resume is an additive recovery capability. | +| Prepublication validation | Upload, re-download and validate intended set | Upload then publish | Upload responses already validate size and SHA-256 digest before publication. Re-downloading bytes before publication remains destination work; post-publication asset re-download and byte comparison against signed `SHA256SUMS` are implemented and proven in rehearsal `.9`. Exact signing-approved local assets and mutations are bound to the created release ID. | +| Checks after publish | Immutable=true; signed release/membership and exact intended source/bytes | Immutable check and normal gh verification | Preserve and bind checks to created release ID. `gh release verify` accepts tags, so bracket it with created-ID/tag correspondence checks rather than invent an ID attestation endpoint. | +| Mutable/lost/unknown outcomes | Controlled cleanup for confirmed mutable; read-only reconciliation otherwise | Loud failure, manual recovery | Planned narrower Phase 1 behavior: leave failed/unknown state for explicit inspection, no blind retry/deletion. Later owner adds safe reconciliation without changing immutable assets or user verification commands. | +| Concurrency | Tag scoped; never cancel publishing run | Same rule also applies to PR builds | Preserve tags; cancel superseded PR builds only. | +| Public deployment route | Verified raw Fastly bytes; prepared EdgeZero context/recovery later | Manual clean package/member comparison | Preserve manual verification/deployment while later adding prepared-payload APIs; #1235 does not wait for boundaries 5–6. | + +**Nonbreaking adoption rule:** existing `gh` checks remain valid when later releases add real companions, index and richer evidence. The full custom verifier is introduced for index-bearing releases; it must not reinterpret the first slice as having complete graph/reproducibility/qualification evidence. No fake index or placeholder successful companions will be published. The unavoidable missing-evidence/recovery/platform-qualification limits are explicit; signer path, cohort, SPDX predicate/names and reserved companion names are aligned before the next publication attempt. + +### Approved seven-binary Axum extension + +This implementation extends the frozen cohort from four +to **seven binaries / 29 release files**, retaining all four original names. +Rehearsal `.4` at `922c2211c` proves only the original four-binary/17-file +implementation. Rehearsal `.9` passed for the seven-binary implementation at +`149754d357901ec874eab6140e92bce3d8613085`; the `.4` evidence is not reused for the expanded cohort. + +The exact cohort diff is three appended records, all default features, empty +extra features, `variant: default`, `assurance_profile: provenance-sbom`, +`role: server`, `bin: trusted-server-axum`, package +`trusted-server-adapter-axum`, and descriptive capability `axum-production`: + +| Added name / target | Compatibility record | +| ----------------------------------------------------------------------------- | ------------------------------------ | +| `trusted-server-axum-x86_64-unknown-linux-gnu` / `x86_64-unknown-linux-gnu` | `x86_64`, `linux-gnu`, `glibc-2.34` | +| `trusted-server-axum-x86_64-unknown-linux-musl` / `x86_64-unknown-linux-musl` | `x86_64`, `linux-musl`, `static` | +| `trusted-server-axum-aarch64-unknown-linux-gnu` / `aarch64-unknown-linux-gnu` | `aarch64`, `linux-gnu`, `glibc-2.34` | + +This adds production Axum distributions by explicit scope decision. `static` +means no dynamic libc/loader requirement, not qualification of every Linux +kernel. The cohort’s `eligible_branches: ["main"]` is a destination value, not enforced by Phase 1; rc-branch eligibility remains a maintainer decision. Phase 1 ships seven artifacts. The historical cohort identifier `phase1-fastly-cli` is a destination-only label for the original contract; it is not the current file count or a qualification claim. The Fastly `provenance-sbom-repeatable` profile is destination-only: Phase 1 makes no repeatability claim. Its original four entries remain unchanged. **No Phase 1 reader** enforces these records or +their destination assurance profiles/capability labels. `release-tools.json` and +consumer `release-policy.json` remain byte-for-byte unchanged. The workflow's +explicit seven selections own the Phase 1 builds; no supplemental tool file or +policy reader is introduced. + +Every binary gets `.spdx.json`, `.provenance.sigstore.json` and +`.sbom.sigstore.json`; only actual actions/attest outputs are retained. +`SHA256SUMS` covers the other **28** files and never itself. Exact-name downloads, +signing receipts and release membership checks cover the entire 29-file set. +Signer identity stays at reusable `release-build.yml` with the exact tag ref. +Existing users' four asset names and verification commands continue to work. + +GNU compilation runs in digest-pinned UBI 9.6 with pinned Rust installed through +rustup, explicit target installation and `--locked`; final ELF requirements above +glibc 2.34 fail. Node is the existing pinned distribution mounted read-only. +The builder's system packages are acquired through DNF and are not a fully +qualified or pinned native tool closure. Musl uses Ubuntu's exact `1.2.4-2` +`musl`, `musl-dev` and `musl-tools` packages, the explicit `musl-gcc` linker and +GCC 13 driver, whose actual version is logged. Target-specific C/linker/static +flags leave host SBOM-tool builds unchanged. No allocator changes are included; allocator/load qualification remains later work. + +Builds scan the final bytes for config canaries and check locked inputs are +unchanged. Before upload they check wasm magic/version, each CLI's labelled package/source/target/variant output, GNU symbol ceilings and static-musl loader +requirements. GNU x86 binaries start on Debian 12-slim, Ubuntu 22.04 and UBI 9.6 +minimal; ARM Axum starts on Debian 12-slim on the ARM runner; musl starts on +pinned Alpine and a scratch-like root filesystem with a read-only runtime CA +bundle. The locked adapter initializes a TLS client before dispatching requests, +so an entirely empty trust store fails even a health request. Every Axum smoke mounts +the runner's system certificates at `/ca.pem` and sets `SSL_CERT_FILE`; operators +must supply runtime trust data independently. No dynamic libraries are added. +Acquired builder/runtime image IDs +are logged. Axum has no help/version mode; its bridge-networked, read-only, nonroot +container receives only a fictional runtime envelope/secrets and must return +`200 ok` from its configured health router. This does not establish publisher +proxy/load qualification or complete native composition. + +The carried **v1 index schema remains unchanged**: its closed four-entry graph +cannot represent this cohort. Phase 1 publishes no index or deferred companions. +Phase 2 must review a seven-entry schema/policy amendment including musl/ARM +compatibility and `axum-production` capability forms before reopening its custom +consumer. The [slice-map annotations](binary-attestations-slice-map.json) mark +producer/consumer `FIXED_ARTIFACTS`, `cohort_order`, descriptor slots, bounds, +qualification enums, candidate workflows and fixtures as **update for Axum**. +The full frozen graph would have 53 files for seven artifacts; Phase 1's 29 is +not a replacement for those deferred bounds. Richer companions and consumer +support remain additive later work; existing manual GitHub verification stays +valid. + +## Rehearsal history + +- `v0.0.0-rehearsal.1` at `4dd217125`: Fastly build passed; native builds + were canceled after review. Signing and publication executed no steps, so + the run produced no attestations or release. The original annotated tag remains + unused; later rehearsal tags are lightweight and never reused. +- `v0.0.0-rehearsal.2` at `41896cb07`: all builds and signing passed and + an immutable 17-asset release was published. The final API read failed because + of a stale pooled connection after synchronous verification. Independent + verification passed; see the [verification transcript](https://github.com/IABTechLab/trusted-server/pull/1435#issuecomment-6341078344). +- `v0.0.0-rehearsal.3` at `ca902b6fe`: all builds, signing, publication + and the final API read passed. Independent verification with GitHub CLI 2.102.0 + passed release verification, all 17 asset checks, both predicates and both + signer identity checks for each binary, checksums and SPDX predicate equality; + see the [full verification transcript](https://github.com/IABTechLab/trusted-server/pull/1435#issuecomment-6343374330). + +- `v0.0.0-rehearsal.4` at `922c2211c0cc7941bfc8ffa9f775ef8ac0971232`: + all four builds, signing, immutable publication and post-publish checks passed. + Independent GitHub CLI 2.102.0 verification passed all 17 asset checks, + checksums, both predicates and signer checks, and SPDX equality. Offline + verification also passed with OS-enforced network denial; pinned Viceroy 0.21.0 + served a configured page from the verified wasm with the exact source header. + See the [online transcript](https://github.com/IABTechLab/trusted-server/pull/1435#issuecomment-6346483024) + and [offline/local runtime proof](https://github.com/IABTechLab/trusted-server/pull/1435#issuecomment-6346428668). + Bounded read-only retries were present, but no transient retry was observed. + +- `v0.0.0-rehearsal.5` at `937214a632f9387601dba94f996de78b2e6ee424`: + all seven builds/signing jobs, immutable publication and post-publish checks + passed. Independent GitHub CLI 2.102.0 verification passed all 29 asset checks, + 28 checksum entries, both predicates and signer checks for all seven binaries, + and seven SPDX predicate comparisons. Offline verification passed under + OS-enforced network denial; Viceroy 0.21.0 served a configured page from the + verified wasm with the exact source header. GNU runtime image checks, native + ARM startup and static-musl Alpine/scratch-like checks passed with explicit + runtime CA data. See the [online transcript](https://github.com/IABTechLab/trusted-server/pull/1435#issuecomment-6357357438), + [offline proof](https://github.com/IABTechLab/trusted-server/pull/1435#issuecomment-6357351810) and + [build/local runtime proof](https://github.com/IABTechLab/trusted-server/pull/1435#issuecomment-6357352104). + +- `v0.0.0-rehearsal.6` at `39a769c3e3d6476395fa7a215bd0a5e74a760f3e`: failed after immutable prerelease publication at the release-attestation lookup. Independent verification passed all 29 assets, online/offline predicates and signer pins, SPDX equality and the manual Fastly route. [Failure and independent proof](https://github.com/IABTechLab/trusted-server/pull/1435#issuecomment-6360012578). + +- `v0.0.0-rehearsal.7` at `7a05940e2f7b10c459bdd2d3949f265b755ce42c`: cancelled after the macOS ARM job could not acquire a hosted runner. The remaining build was cancelled; signing/publication executed no steps and no release was created. Its lightweight tag remains unchanged. [Failure record](https://github.com/IABTechLab/trusted-server/pull/1435#issuecomment-6361386927). + +- `v0.0.0-rehearsal.8` at `7a05940e2f7b10c459bdd2d3949f265b755ce42c`: all seven builds/signing jobs, immutable prerelease publication and complete independent online/offline/runtime verification passed. All 29 assets, 28 checksum entries, both predicates and signer forms, online store lookup without bundles, SPDX equality and the pinned Fastly pack/validate/inspect/cmp route passed. [Full transcript](https://github.com/IABTechLab/trusted-server/pull/1435#issuecomment-6362236955). + +- `v0.0.0-rehearsal.9` at `149754d357901ec874eab6140e92bce3d8613085`: all seven builds/signing jobs, immutable prerelease publication and complete independent verification passed. The publisher found the release attestation available on its first check (no live availability wait), then re-downloaded and byte-compared all 29 assets. All bundle/online-store/offline predicates and signer pins, SPDX comparisons, checksums, macOS floor and CLI source checks passed. Axum smokes proved 0.0.0.0:8080 from outside each container and rejected primary/legacy port zero. Pinned Fastly pack/validate/inspect/cmp and Viceroy runtime checks passed, with visible no-Build-facts check/result. [Full transcript](https://github.com/IABTechLab/trusted-server/pull/1435#issuecomment-6364827112). + +The latest rehearsed implementation is `149754d357901ec874eab6140e92bce3d8613085` from `.9`. +This review-head update is docs-only since rehearsed `149754d35`; workflows, scripts, tests and runtime code retain the rehearsed implementation. + +The immutable `.2`, `.3`, `.4`, `.5`, `.6`, `.8` and `.9` releases remain intact; no drafts remain. The `.7` tag has no release. No upstream tags, releases or repository settings were changed. + +### Phase 1 review boundary notes + +The reusable caller declares the maximum signing permissions required by its +called workflow. GitHub does not allow a read-only caller to invoke a nested +workflow that declares greater permissions, even for conditionally skipped jobs. +Actual build/check jobs narrow permissions to contents-read; signing remains +tag-only and requires the publication enablement variable. Splitting a new build-only workflow is deferred to preserve this slice. + +The configuration scanner covers literal/raw/concatenated macros and literal +build-script deployment prefixes. It is a best-effort source regression audit, +not Rust expansion or dataflow analysis: `include!`, raw byte strings (`br"…"` / +`rb"…"`), computed names, procedural macros and third-party build scripts are not +completely analyzed. Planted canary scans and +clean inherited environments provide additional checks, not hermeticity. + +### Rehearsal .6 outcome + +Rehearsal `.6` at `39a769c3e3d6476395fa7a215bd0a5e74a760f3e` failed after immutable publication at the release-attestation lookup. [Independent verification](https://github.com/IABTechLab/trusted-server/pull/1435#issuecomment-6360012578) passed for all seven binaries / 29 assets, online and offline checks, SPDX equality and the manual Fastly pack/validate/inspect/cmp route; this does not turn the failed workflow green. Fresh `.8` subsequently passed at `7a05940e2f7b10c459bdd2d3949f265b755ce42c`; see the [full transcript](https://github.com/IABTechLab/trusted-server/pull/1435#issuecomment-6362236955). + +The failed publisher check ran at 07:19:53.744 UTC; the independently verified release attestation has a trusted timestamp of 07:19:58 UTC. The pinned CLI hides underlying lookup errors, so the original HTTP status is unknown. The immutable prerelease and its 29 assets remain unchanged. Before cryptographic release verification, the availability check makes at most five release-attestation GETs, with 2s/5s/10s/20s backoff (37s total waiting). Only this endpoint treats HTTP 404 as no record yet; a valid HTTP 200 with no current-tag record may also wait. Each GET times out after at most 4s, and a separate 60s deadline bounds availability checking, including other-tag verification, within the shared three-minute scheduling window. Every other non-200 status fails immediately. Malformed records, duplicate current-tag records, identity/source/asset mismatches and definitive cryptographic failures stop immediately. Other-tag records are ignored only after their exact bundles verify; those read-only verification calls may retry recognized transient failures within the same bounds. Current-tag cryptographic verification is single-shot. Logs show every wait or availability on the first check. No mutation is retried. + +### Corrective batch publication checks + +Post-publish downloads use fresh temporary directories and bounded transient read retries. The complete 29-file inventory and every byte, including SHA256SUMS, must match the cryptographically verified release/handoff. Missing, extra or changed files fail immediately. The release-body verification commands are tested against the operator guide’s canonical block. Axum uses the pinned EdgeZero resolver with every warning fatal; ports must be 1–65535, and primary `EDGEZERO__ADAPTER__PORT` and legacy `PORT` must agree. Container smokes use 0.0.0.0:8080 with host-side reachability. + +### Follow-ups + +- Axum invalid-port diagnostics: `PORT=0` is correctly refused, but the forwarded EdgeZero warning names `EDGEZERO__ADAPTER__PORT="0"` and says “falling back.” Reword the error to name the actual supplied variable and state that startup is refused. This changes the binary and requires a fresh rehearsal; it is not part of this docs-only update. diff --git a/docs/superpowers/plans/binary-attestations-slice-map.json b/docs/superpowers/plans/binary-attestations-slice-map.json new file mode 100644 index 0000000000..84f58a1e8e --- /dev/null +++ b/docs/superpowers/plans/binary-attestations-slice-map.json @@ -0,0 +1,681 @@ +{ + "base": "f9281917a7260b723f5f22175911c6d2147ef650", + "source": "122b78664b1235c717e9208fd200f9a9b0c335df", + "total": 434, + "notes": [ + "File ownership is a planning map, not safe cherry-pick boundaries: main.rs, Cargo manifests/lock, shared fixtures and workflow YAML require staged hunks.", + "Phase 1 linked evidence closure includes source-modified legacy spec, migration addendum, two integration plans, 13 dated evidence reports and JS composition narrative; baseline axum-dev.md need not port.", + "Commit citations identify introducing or major shaping commits, not complete exact-replay commit sets.", + "Phase 1 interim producers scripts/release/phase1-build.sh and phase1.mjs are replaced by slices 2.5 (recipes/inspection) and 2.6 (composition/SPDX); release.yml inline publisher is replaced by slice 2.8. Slice 2.3 immutable build facts are partly adopted now; CLI source-resource cleanup remains deferred." + ], + "groups": { + "phase-1-contract-and-linked-evidence": { + "commits": [ + "91c190710a821fad73835a5f32c3628ffff20636", + "24a8a7180a32a30e0438d9f5b77697c058c6e9c3", + "484d2c0845be35f006cd8ecafb54dd1fc41e9a9b", + "58d2e7a8b6f81ef4e6b7546321ddc2e51846e415", + "6d0efda7210983021ab3c9efe8a0a3190e9eb81b" + ], + "files": [ + "docs/superpowers/plans/2026-10-13-attested-release-implementation.md", + "docs/superpowers/plans/2026-10-13-cli-packaging-attestation-integration.md", + "docs/superpowers/plans/2026-10-13-fastly-prebuilt-edgezero-integration.md", + "docs/superpowers/specs/2026-01-15-attestation-design.md", + "docs/superpowers/specs/2026-10-09-1235-release-attestation-design.md", + "docs/superpowers/specs/2026-10-12-1235-release-attestation-feasibility.md", + "docs/superpowers/specs/2026-10-14-release-tooling-rust-migration.md", + "docs/superpowers/specs/schemas/release-index-v1.schema.json", + "scripts/release/js-composition.md", + "scripts/release/qualification/2026-10-15-consumer-real-fixtures.md", + "scripts/release/qualification/2026-10-15-js-two-builds.md", + "scripts/release/qualification/2026-10-16-confined-cargo-candidate.md", + "scripts/release/qualification/2026-10-16-fastly-canonical-reproduction-equality.md", + "scripts/release/qualification/2026-10-16-fastly-runtime-gate.md", + "scripts/release/qualification/2026-10-16-linux-cli-candidate.md", + "scripts/release/qualification/2026-10-16-producer-normalized-manifests.md", + "scripts/release/qualification/2026-10-19-apple-sdk-inputs.md", + "scripts/release/qualification/2026-10-20-apple-cli-candidates.md", + "scripts/release/qualification/2026-10-20-linux-cli-gnu-follow-up.md", + "scripts/release/qualification/2026-10-21-candidate-release-index.md", + "scripts/release/qualification/2026-10-21-four-asset-candidate-join.md", + "scripts/release/qualification/2026-10-21-native-reproduction-schedule.md" + ], + "phase": "1", + "state": "Reviewed documentation and historical evidence only" + }, + "phase-2a-strict-documents-policy-preflight-source": { + "commits": [ + "1cfca1357d0eee7158a4e6049444fef46807c564", + "90a87c35ac73c1f1a137740c48df5af41cc7df90", + "917620d2e383471e804d19205f235412ebede83d", + "8a68537344d6c9e1dbd7c8dea3731ea971346376", + "67aaa3fc3d8ba21011472be60e500b884f590742" + ], + "files": [ + "Cargo.lock", + "Cargo.toml", + "crates/trusted-server-release/Cargo.toml", + "crates/trusted-server-release/src/documents.rs", + "crates/trusted-server-release/src/documents/tests.rs", + "crates/trusted-server-release/src/error.rs", + "crates/trusted-server-release/src/git_objects.rs", + "crates/trusted-server-release/src/main.rs", + "crates/trusted-server-release/src/policy.rs", + "crates/trusted-server-release/src/policy/tests.rs", + "crates/trusted-server-release/src/policy/tools.rs", + "crates/trusted-server-release/src/preflight.rs", + "crates/trusted-server-release/src/preflight/child.rs", + "crates/trusted-server-release/src/preflight/tests.rs", + "crates/trusted-server-release/src/source.rs", + "crates/trusted-server-release/src/source/closure.rs", + "crates/trusted-server-release/src/source/config.rs", + "crates/trusted-server-release/src/source/git.rs", + "crates/trusted-server-release/src/source/materialize.rs", + "crates/trusted-server-release/src/source/tests.rs", + "crates/trusted-server-release/src/source/tests/closure.rs", + "crates/trusted-server-release/src/source/tests/layout.rs", + "crates/trusted-server-release/src/source/tests/objects.rs", + "crates/trusted-server-release/src/source/tests/process.rs", + "crates/trusted-server-release/src/source/tests/worktree.rs", + "crates/trusted-server-release/src/source/tree.rs", + "crates/trusted-server-release/src/source/worktree.rs", + "crates/trusted-server-release/src/tool_tree.rs", + "crates/trusted-server-release/src/tool_tree/tests.rs", + "crates/trusted-server-release/tests/commands.rs", + "crates/trusted-server-release/tests/fixtures/sha-mbles-1.bin", + "scripts/release/bootstrap.sh", + "scripts/release/release-cohort.json", + "scripts/release/release-tools.json" + ], + "phase": "2.1", + "state": "Closed; no triggers or production selection" + }, + "phase-2b-process-and-native-sandbox": { + "commits": [ + "f407fe446fcb4daa431bae2ee1834e1ec16664fc", + "39fbf7cf2f6c7c3621e192d1897920250e5f9dab", + "60fafa7175f26685edd88c8336e958e8e709b000", + "d4408d2e015f44bd5e9c0641a9e9f1acc966907f" + ], + "files": [ + "crates/trusted-server-release/src/boundary.rs", + "crates/trusted-server-release/src/boundary/cleanup_and_system_root_tests.rs", + "crates/trusted-server-release/src/boundary/command.rs", + "crates/trusted-server-release/src/boundary/command/selector_tests.rs", + "crates/trusted-server-release/src/boundary/daemon.rs", + "crates/trusted-server-release/src/boundary/docker.rs", + "crates/trusted-server-release/src/boundary/docker/host_path_tests.rs", + "crates/trusted-server-release/src/boundary/entropy.rs", + "crates/trusted-server-release/src/boundary/host_inputs.rs", + "crates/trusted-server-release/src/boundary/macos.rs", + "crates/trusted-server-release/src/boundary/probe.rs", + "crates/trusted-server-release/src/boundary/proof.rs", + "crates/trusted-server-release/src/boundary/roots.rs", + "crates/trusted-server-release/src/boundary/seccomp.rs", + "crates/trusted-server-release/src/boundary/selector_placement_tests.rs", + "crates/trusted-server-release/src/boundary/sibling_tests.rs", + "crates/trusted-server-release/src/boundary/survivors.rs", + "crates/trusted-server-release/src/boundary/tests.rs", + "crates/trusted-server-release/src/boundary/tests/container.rs", + "crates/trusted-server-release/src/boundary/tests/linux.rs", + "crates/trusted-server-release/src/boundary/tests/macos.rs", + "crates/trusted-server-release/src/boundary/tests/modes.rs", + "crates/trusted-server-release/src/boundary/tests/unit.rs", + "crates/trusted-server-release/src/lifecycle_guard.rs", + "crates/trusted-server-release/src/process.rs", + "crates/trusted-server-release/src/process/environment.rs", + "crates/trusted-server-release/src/process/environment/selector_tests.rs", + "crates/trusted-server-release/src/process/pipes.rs", + "crates/trusted-server-release/src/process/redact.rs", + "crates/trusted-server-release/src/process/supervise.rs", + "crates/trusted-server-release/src/process/tests.rs", + "crates/trusted-server-release/src/process/tests/fixture.rs", + "crates/trusted-server-release/src/process/tests/outcomes.rs", + "crates/trusted-server-release/src/process/tests/redaction.rs", + "crates/trusted-server-release/src/process/tests/supervision.rs", + "crates/trusted-server-release/src/process/tests/variables.rs", + "crates/trusted-server-release/tests/boundary.rs", + "crates/trusted-server-release/tests/fixtures/cargo-denial/Cargo.lock", + "crates/trusted-server-release/tests/fixtures/cargo-denial/Cargo.toml", + "crates/trusted-server-release/tests/fixtures/cargo-denial/crates/build-direct/Cargo.toml", + "crates/trusted-server-release/tests/fixtures/cargo-denial/crates/build-direct/build.rs", + "crates/trusted-server-release/tests/fixtures/cargo-denial/crates/build-direct/src/main.rs", + "crates/trusted-server-release/tests/fixtures/cargo-denial/crates/build-grandchild/Cargo.toml", + "crates/trusted-server-release/tests/fixtures/cargo-denial/crates/build-grandchild/build.rs", + "crates/trusted-server-release/tests/fixtures/cargo-denial/crates/build-grandchild/src/main.rs", + "crates/trusted-server-release/tests/fixtures/cargo-denial/crates/control/Cargo.toml", + "crates/trusted-server-release/tests/fixtures/cargo-denial/crates/control/build.rs", + "crates/trusted-server-release/tests/fixtures/cargo-denial/crates/control/src/control.rs", + "crates/trusted-server-release/tests/fixtures/cargo-denial/crates/control/src/main.rs", + "crates/trusted-server-release/tests/fixtures/cargo-denial/crates/macro-attempt/Cargo.toml", + "crates/trusted-server-release/tests/fixtures/cargo-denial/crates/macro-attempt/src/lib.rs", + "crates/trusted-server-release/tests/fixtures/cargo-denial/crates/macro-attempt/src/main.rs", + "scripts/release/qualification/SECCOMP-LICENSE", + "scripts/release/qualification/seccomp-default.json" + ], + "phase": "2.2", + "state": "Closed; no triggers or production selection" + }, + "phase-2c-immutable-build-facts": { + "commits": [ + "bfb697404551a28217af8906024506095bcd9559", + "6a07ccc03278af1a3e07b30e25a1cd46efb2f57b" + ], + "files": [ + "crates/trusted-server-adapter-fastly/build.rs", + "crates/trusted-server-adapter-fastly/src/main.rs", + "crates/trusted-server-cli/build.rs", + "crates/trusted-server-cli/src/prebid_bundle.rs", + "crates/trusted-server-cli/tests/release_build_facts.rs", + "crates/trusted-server-core/build.rs", + "crates/trusted-server-core/build_support/git_version.rs", + "crates/trusted-server-core/src/build_info.rs", + "crates/trusted-server-core/src/constants.rs", + "crates/trusted-server-core/src/lib.rs", + "crates/trusted-server-core/tests/git_version_resolve.rs", + "crates/trusted-server-integration-tests/browser/helpers/infra.ts", + "crates/trusted-server-integration-tests/tests/common/runtime.rs", + "crates/trusted-server-integration-tests/tests/frameworks/mod.rs", + "crates/trusted-server-integration-tests/tests/parity.rs", + "docs/superpowers/specs/2026-09-25-git-version-header-design.md" + ], + "phase": "2.3", + "state": "Closed; no triggers or production selection" + }, + "phase-2d-js-inventory-composition-materials-two-builds": { + "commits": [ + "e1606fa3da0af4deb4d75b26a11d6677704d747c", + "eeb68f9188b4609bd3b8f7890a6fe51cbc26a57d", + "2edd46ce63b5684a5a0fce976a60bb9794e1c6cf", + "bf3057d60f99a92693c0836305129ba68962afd5", + "0c67be66ee57584c9107f3cb6ea796c77d8a20cb" + ], + "files": [ + "crates/trusted-server-js/Cargo.toml", + "crates/trusted-server-js/README.md", + "crates/trusted-server-js/build.rs", + "crates/trusted-server-js/build/bundle_set.rs", + "crates/trusted-server-js/build/inventory.rs", + "crates/trusted-server-js/build/prebuilt.rs", + "crates/trusted-server-js/lib/.gitignore", + "crates/trusted-server-js/lib/.prettierignore", + "crates/trusted-server-js/lib/build-all.mjs", + "crates/trusted-server-js/lib/build-module-options.mjs", + "crates/trusted-server-js/lib/eslint.config.js", + "crates/trusted-server-js/lib/package.json", + "crates/trusted-server-js/lib/release-composition.mjs", + "crates/trusted-server-js/lib/test/observe-npm.test.mjs", + "crates/trusted-server-js/lib/test/release-composition.test.mjs", + "crates/trusted-server-js/lib/vitest.config.ts", + "crates/trusted-server-js/tests/release_prebuilt.rs", + "crates/trusted-server-release/src/javascript.rs", + "crates/trusted-server-release/src/javascript/contributions.rs", + "crates/trusted-server-release/src/javascript/contributions/tests.rs", + "crates/trusted-server-release/src/javascript/tests.rs", + "crates/trusted-server-release/src/javascript/tests/adopt.rs", + "crates/trusted-server-release/src/materials.rs", + "crates/trusted-server-release/src/materials/archive.rs", + "crates/trusted-server-release/src/materials/cargo_vendor.rs", + "crates/trusted-server-release/src/materials/cargo_vendor/differential.rs", + "crates/trusted-server-release/src/materials/cargo_vendor/git_tree.rs", + "crates/trusted-server-release/src/materials/cargo_vendor/index.rs", + "crates/trusted-server-release/src/materials/cargo_vendor/normalize.rs", + "crates/trusted-server-release/src/materials/cargo_vendor/source.rs", + "crates/trusted-server-release/src/materials/cargo_vendor/tests.rs", + "crates/trusted-server-release/src/materials/cargo_vendor/tests/git.rs", + "crates/trusted-server-release/src/materials/cargo_vendor/tests/normalize.rs", + "crates/trusted-server-release/src/materials/cargo_vendor/tests/registry.rs", + "crates/trusted-server-release/src/materials/cargo_vendor/tests/source.rs", + "crates/trusted-server-release/src/materials/cargo_vendor/tests/support.rs", + "crates/trusted-server-release/src/materials/cargo_vendor/writer.rs", + "crates/trusted-server-release/src/materials/extract.rs", + "crates/trusted-server-release/src/materials/extract/compatibility.rs", + "crates/trusted-server-release/src/materials/extract/tests.rs", + "crates/trusted-server-release/src/materials/extract/tests/cargo_package.rs", + "crates/trusted-server-release/src/materials/extract/tests/npm_compatibility.rs", + "crates/trusted-server-release/src/materials/git.rs", + "crates/trusted-server-release/src/materials/git/input.rs", + "crates/trusted-server-release/src/materials/git/tests.rs", + "crates/trusted-server-release/src/materials/locked.rs", + "crates/trusted-server-release/src/materials/manifest.rs", + "crates/trusted-server-release/src/materials/npm_bundle.rs", + "crates/trusted-server-release/src/materials/tests.rs", + "crates/trusted-server-release/src/preparation.rs", + "crates/trusted-server-release/src/preparation/tests.rs", + "crates/trusted-server-release/src/preparation/tests/full.rs", + "scripts/release/build-js.sh", + "scripts/release/collect-js.mjs", + "scripts/release/observe-npm.mjs", + "scripts/release/tests/js-fixture.Dockerfile", + "scripts/release/tests/prepare-js-fixture.mjs" + ], + "phase": "2.4", + "state": "Closed; no triggers or production selection" + }, + "phase-2e-recipes-inspection-manual-package": { + "commits": [ + "a7ae687547409540ed748fc89024b2c9dd84a5b0", + "03fa8e5a622fc7cf86d81195ccdd2f40df50ccfc", + "d472aaa50fb3d532ca997c6822a8c0b8907386de", + "4cc5700e834c3238f99a81b3a567edb634fecfe1", + "10167c6017b725d0a4d5ad0a0c8e6a96fd4aa807", + "c78185b8d699f56d843a01c8812890f38e4a55f6", + "58fa27d3e8b45987fe51d36a80a7d6db74b3e072", + "9999c39584d5b2731bb83a6dae995aad59cc9e38" + ], + "files": [ + "crates/trusted-server-adapter-cloudflare/build.sh", + "crates/trusted-server-release/src/inspect.rs", + "crates/trusted-server-release/src/inspect/elf.rs", + "crates/trusted-server-release/src/inspect/elf/loader.rs", + "crates/trusted-server-release/src/inspect/macho.rs", + "crates/trusted-server-release/src/inspect/macho/signature.rs", + "crates/trusted-server-release/src/inspect/tests.rs", + "crates/trusted-server-release/src/inspect/tests/builders.rs", + "crates/trusted-server-release/src/inspect/tests/builders/macho_data.rs", + "crates/trusted-server-release/src/inspect/tests/elf.rs", + "crates/trusted-server-release/src/inspect/tests/macho.rs", + "crates/trusted-server-release/src/inspect/tests/wasm.rs", + "crates/trusted-server-release/src/inspect/wasm.rs", + "crates/trusted-server-release/src/package.rs", + "crates/trusted-server-release/src/package/tests.rs", + "crates/trusted-server-release/src/recipe.rs", + "crates/trusted-server-release/src/recipe/ambient.rs", + "crates/trusted-server-release/src/recipe/apple.rs", + "crates/trusted-server-release/src/recipe/apple/sdk.rs", + "crates/trusted-server-release/src/recipe/apple/toolchain.rs", + "crates/trusted-server-release/src/recipe/apple/toolchain/tests.rs", + "crates/trusted-server-release/src/recipe/artifact.rs", + "crates/trusted-server-release/src/recipe/artifact/tests.rs", + "crates/trusted-server-release/src/recipe/cargo_home.rs", + "crates/trusted-server-release/src/recipe/cargo_home/tests.rs", + "crates/trusted-server-release/src/recipe/cargo_output.rs", + "crates/trusted-server-release/src/recipe/cargo_output/tests.rs", + "crates/trusted-server-release/src/recipe/compare.rs", + "crates/trusted-server-release/src/recipe/completed.rs", + "crates/trusted-server-release/src/recipe/execute.rs", + "crates/trusted-server-release/src/recipe/fastly_tools.rs", + "crates/trusted-server-release/src/recipe/final_paths.rs", + "crates/trusted-server-release/src/recipe/fresh.rs", + "crates/trusted-server-release/src/recipe/gnu.rs", + "crates/trusted-server-release/src/recipe/gnu/image.rs", + "crates/trusted-server-release/src/recipe/gnu/tests.rs", + "crates/trusted-server-release/src/recipe/identity.rs", + "crates/trusted-server-release/src/recipe/identity/tests.rs", + "crates/trusted-server-release/src/recipe/native.rs", + "crates/trusted-server-release/src/recipe/paths.rs", + "crates/trusted-server-release/src/recipe/paths/fake.rs", + "crates/trusted-server-release/src/recipe/plan.rs", + "crates/trusted-server-release/src/recipe/remap.rs", + "crates/trusted-server-release/src/recipe/runtime.rs", + "crates/trusted-server-release/src/recipe/runtime/config.rs", + "crates/trusted-server-release/src/recipe/runtime/exchange.rs", + "crates/trusted-server-release/src/recipe/runtime/export.rs", + "crates/trusted-server-release/src/recipe/runtime/export/tests.rs", + "crates/trusted-server-release/src/recipe/runtime/logs.rs", + "crates/trusted-server-release/src/recipe/runtime/viceroy.rs", + "crates/trusted-server-release/src/recipe/sources.rs", + "crates/trusted-server-release/src/recipe/startup.rs", + "crates/trusted-server-release/src/recipe/startup/accepted.rs", + "crates/trusted-server-release/src/recipe/startup/accepted/tests.rs", + "crates/trusted-server-release/src/recipe/startup/lifecycle.rs", + "crates/trusted-server-release/src/recipe/startup/native.rs", + "crates/trusted-server-release/src/recipe/tests.rs", + "crates/trusted-server-release/src/recipe/tests/ambient.rs", + "crates/trusted-server-release/src/recipe/tests/bind.rs", + "crates/trusted-server-release/src/recipe/tests/candidate.rs", + "crates/trusted-server-release/src/recipe/tests/candidate_entry.rs", + "crates/trusted-server-release/src/recipe/tests/candidate_index.rs", + "crates/trusted-server-release/src/recipe/tests/cli.rs", + "crates/trusted-server-release/src/recipe/tests/cli_candidate.rs", + "crates/trusted-server-release/src/recipe/tests/compare.rs", + "crates/trusted-server-release/src/recipe/tests/complete.rs", + "crates/trusted-server-release/src/recipe/tests/complete_darwin.rs", + "crates/trusted-server-release/src/recipe/tests/darwin.rs", + "crates/trusted-server-release/src/recipe/tests/darwin_toolchain.rs", + "crates/trusted-server-release/src/recipe/tests/fixture.rs", + "crates/trusted-server-release/src/recipe/tests/full.rs", + "crates/trusted-server-release/src/recipe/tests/full_macos.rs", + "crates/trusted-server-release/src/recipe/tests/macos_candidate.rs", + "crates/trusted-server-release/src/recipe/tests/macos_cli.rs", + "crates/trusted-server-release/src/recipe/tests/native.rs", + "crates/trusted-server-release/src/recipe/tests/plan.rs", + "crates/trusted-server-release/src/recipe/tests/real_fixture.rs", + "crates/trusted-server-release/src/recipe/tests/remap.rs", + "crates/trusted-server-release/src/recipe/tests/runtime.rs", + "crates/trusted-server-release/src/recipe/tests/sources.rs", + "crates/trusted-server-release/src/recipe/tests/spdx_candidate.rs", + "crates/trusted-server-release/src/recipe/tests/tools.rs", + "crates/trusted-server-release/src/recipe/tools.rs", + "scripts/release/build.sh", + "scripts/release/tests/cargo-fixture.Dockerfile", + "scripts/release/tests/cli-builder.Dockerfile", + "scripts/release/tests/prepare-cargo-fixture.mjs", + "scripts/release/tests/prepare-cli-fixture.mjs" + ], + "phase": "2.5", + "state": "Closed; no triggers or production selection" + }, + "phase-2f-composition-spdx": { + "commits": [ + "4176b81f2056f8ff639da48875bb4f9876a82582", + "6bc3dcf678c87ee3d3dc48090b34c32c81b72988", + "775696b28a45e4349ffed970c6d31452219cf0e1", + "1719cf15fd58fcf029f6b4ae5003215802bef405", + "416b43f43c5d7e69c08bca32753240abded8523c", + "5a98c18ada25863a896b93f379125e51497e83f8" + ], + "files": [ + "crates/trusted-server-release/schemas/SPDX-LICENSE", + "crates/trusted-server-release/schemas/spdx-2.3.schema.json", + "crates/trusted-server-release/src/composition.rs", + "crates/trusted-server-release/src/composition/capture.rs", + "crates/trusted-server-release/src/composition/external.rs", + "crates/trusted-server-release/src/composition/external/tests.rs", + "crates/trusted-server-release/src/composition/graph.rs", + "crates/trusted-server-release/src/composition/graph/tests.rs", + "crates/trusted-server-release/src/composition/javascript.rs", + "crates/trusted-server-release/src/composition/javascript/tests.rs", + "crates/trusted-server-release/src/composition/native.rs", + "crates/trusted-server-release/src/composition/native/tests.rs", + "crates/trusted-server-release/src/composition/origin.rs", + "crates/trusted-server-release/src/composition/origin/tests.rs", + "crates/trusted-server-release/src/composition/platform.rs", + "crates/trusted-server-release/src/composition/platform/tests.rs", + "crates/trusted-server-release/src/composition/tests.rs", + "crates/trusted-server-release/src/spdx.rs", + "crates/trusted-server-release/src/spdx/candidate.rs", + "crates/trusted-server-release/src/spdx/candidate/tests.rs", + "crates/trusted-server-release/src/spdx/cargo.rs", + "crates/trusted-server-release/src/spdx/cargo/tests.rs", + "crates/trusted-server-release/src/spdx/javascript.rs", + "crates/trusted-server-release/src/spdx/javascript/tests.rs", + "crates/trusted-server-release/src/spdx/native.rs", + "crates/trusted-server-release/src/spdx/native/tests.rs", + "crates/trusted-server-release/src/spdx/platform.rs", + "crates/trusted-server-release/src/spdx/platform/tests.rs", + "crates/trusted-server-release/src/spdx/tests.rs", + "crates/trusted-server-release/tests/fixtures/composition/README.md", + "crates/trusted-server-release/tests/fixtures/composition/cargo-1.95-cross-host-metadata.json", + "crates/trusted-server-release/tests/fixtures/composition/cargo-1.95-cross-messages.jsonl", + "crates/trusted-server-release/tests/fixtures/composition/cargo-1.95-cross-target-filtered-metadata.json", + "crates/trusted-server-release/tests/fixtures/composition/cargo-1.95-cross-tree.txt", + "crates/trusted-server-release/tests/fixtures/composition/cargo-1.95-messages.jsonl", + "crates/trusted-server-release/tests/fixtures/composition/cargo-1.95-metadata.json" + ], + "phase": "2.6", + "state": "Closed; no triggers or production selection" + }, + "phase-2g-collection-workflow-jobs-candidate-index": { + "commits": [ + "6c065adf29b5be142d5c448c430259ab28987a55", + "0f43d4a81f9ea1339a254a4020d302f2eb7012ce", + "6ffebbd0872d9a8d52d0525dcb2ee55959a710fc", + "060172fb62856ea2b94ead6c196a21bb5e75ac1d", + "99a8c5803e22a717afd03a86b367d816bd1302f9" + ], + "files": [ + "crates/trusted-server-release/src/collection.rs", + "crates/trusted-server-release/src/collection/archive.rs", + "crates/trusted-server-release/src/collection/producer.rs", + "crates/trusted-server-release/src/collection/producer/strict_record.rs", + "crates/trusted-server-release/src/collection/producer/strict_record/tests.rs", + "crates/trusted-server-release/src/collection/records.rs", + "crates/trusted-server-release/src/collection/records/receipt.rs", + "crates/trusted-server-release/src/collection/tests.rs", + "crates/trusted-server-release/src/collection/transfer.rs", + "crates/trusted-server-release/src/collection/transfer/ordinary.rs", + "crates/trusted-server-release/src/collection/transfer/tests.rs", + "crates/trusted-server-release/src/collection/workflow_jobs.rs", + "crates/trusted-server-release/src/collection/workflow_jobs/tests.rs", + "crates/trusted-server-release/src/collection/workflow_jobs/tests/support.rs", + "crates/trusted-server-release/src/collection/workflow_jobs/tests/transfer.rs", + "crates/trusted-server-release/src/distribution.rs", + "crates/trusted-server-release/src/distribution/entry.rs", + "crates/trusted-server-release/src/distribution/joins.rs", + "crates/trusted-server-release/src/distribution/ordinary.rs", + "crates/trusted-server-release/src/distribution/summary.rs", + "crates/trusted-server-release/src/distribution/tests.rs", + "crates/trusted-server-release/src/distribution/tests/golden.rs", + "crates/trusted-server-release/src/distribution/tests/summary.rs", + "crates/trusted-server-release/src/distribution/tests/transfer.rs", + "crates/trusted-server-release/src/distribution/transfer.rs", + "crates/trusted-server-release/tests/fixtures/collection/README.md", + "crates/trusted-server-release/tests/fixtures/collection/build-record.json", + "crates/trusted-server-release/tests/fixtures/collection/composition.json", + "crates/trusted-server-release/tests/fixtures/index/candidate/candidate-release-index.json", + "crates/trusted-server-release/tests/fixtures/index/candidate/trusted-server-fastly.wasm.build-evidence.json", + "crates/trusted-server-release/tests/fixtures/index/candidate/trusted-server-fastly.wasm.build-recipe.json", + "crates/trusted-server-release/tests/fixtures/index/candidate/trusted-server-fastly.wasm.rebuild-check.json", + "crates/trusted-server-release/tests/fixtures/index/candidate/trusted-server-fastly.wasm.spdx.json", + "crates/trusted-server-release/tests/fixtures/index/candidate/ts-aarch64-apple-darwin.build-evidence.json", + "crates/trusted-server-release/tests/fixtures/index/candidate/ts-aarch64-apple-darwin.build-recipe.json", + "crates/trusted-server-release/tests/fixtures/index/candidate/ts-aarch64-apple-darwin.rebuild-check.json", + "crates/trusted-server-release/tests/fixtures/index/candidate/ts-aarch64-apple-darwin.spdx.json", + "crates/trusted-server-release/tests/fixtures/index/candidate/ts-x86_64-apple-darwin.build-evidence.json", + "crates/trusted-server-release/tests/fixtures/index/candidate/ts-x86_64-apple-darwin.build-recipe.json", + "crates/trusted-server-release/tests/fixtures/index/candidate/ts-x86_64-apple-darwin.rebuild-check.json", + "crates/trusted-server-release/tests/fixtures/index/candidate/ts-x86_64-apple-darwin.spdx.json", + "crates/trusted-server-release/tests/fixtures/index/candidate/ts-x86_64-unknown-linux-gnu.build-evidence.json", + "crates/trusted-server-release/tests/fixtures/index/candidate/ts-x86_64-unknown-linux-gnu.build-recipe.json", + "crates/trusted-server-release/tests/fixtures/index/candidate/ts-x86_64-unknown-linux-gnu.rebuild-check.json", + "crates/trusted-server-release/tests/fixtures/index/candidate/ts-x86_64-unknown-linux-gnu.spdx.json", + "crates/trusted-server-release/tests/fixtures/index/candidate/tsjs-inventory.json" + ], + "phase": "2.7", + "state": "Closed; no triggers or production selection" + }, + "phase-2h-publication-owner": { + "commits": ["9bd8be2c20a2e188fbdddef7a838b63545279e82"], + "files": [ + "crates/trusted-server-release/src/publication.rs", + "crates/trusted-server-release/src/publication/api.rs", + "crates/trusted-server-release/src/publication/incident.rs", + "crates/trusted-server-release/src/publication/model.rs", + "crates/trusted-server-release/src/publication/owner.rs", + "crates/trusted-server-release/src/publication/role.rs", + "crates/trusted-server-release/src/publication/tests.rs", + "crates/trusted-server-release/src/publication/tests/drafts.rs", + "crates/trusted-server-release/src/publication/tests/fake.rs", + "crates/trusted-server-release/src/publication/tests/final_state.rs", + "crates/trusted-server-release/src/publication/tests/inputs.rs", + "crates/trusted-server-release/src/publication/tests/reconciliation.rs" + ], + "phase": "2.8", + "state": "Closed; no triggers or production selection" + }, + "phase-2i-candidate-ci-same-commit-validation": { + "commits": [ + "2b642b5091298b9278f4e64a2342fd57d23d1aae", + "3e3e30996714f3691319e4cb6ca96d4fbb3240e3", + "b4c8e0c0fded27f02930edc6de990f17a63f5820", + "4483e8256dc52c89e615f1388daa873324a871f5", + "073704cc7f65793767b129a5a330447e02926c7b" + ], + "files": [ + ".github/actions/setup-integration-test-env/action.yml", + ".github/workflows/release-validation.mjs", + ".github/workflows/release-validation.test.mjs", + ".github/workflows/release-validation.yml", + ".github/workflows/test.yml", + "crates/trusted-server-release/src/workflow_contract.rs", + "crates/trusted-server-release/src/workflow_contract/tests.rs", + "scripts/release/qualification/approvals.json", + "scripts/release/qualification/hosted-workflow.proposal.yml", + "scripts/release/qualification/results.schema.json", + "scripts/release/tests/candidate-changes.sh", + "scripts/release/tests/candidate-changes.test.mjs" + ], + "phase": "2.9", + "state": "Closed; no triggers or production selection" + }, + "phase-2j-consumer-cli": { + "commits": [ + "7450b37805131d307d402df089d7a585104dd543", + "da3c9ce6682915aef2ae48d02d8d343181e05702", + "4eebe62138606636e095aa975b7dd37a4fa899ca", + "ac07047ed2b4f1bbe0e1f2e4aac9f845e70b7c7c", + "d774ee47c274257fa0a088280f7df37ed530020c" + ], + "files": [ + "crates/trusted-server-cli/Cargo.toml", + "crates/trusted-server-cli/README.md", + "crates/trusted-server-cli/release-policy.json", + "crates/trusted-server-cli/src/commands/dev/lint/domains.rs", + "crates/trusted-server-cli/src/commands/mod.rs", + "crates/trusted-server-cli/src/commands/release.rs", + "crates/trusted-server-cli/src/lib.rs", + "crates/trusted-server-cli/src/release/artifact.rs", + "crates/trusted-server-cli/src/release/bounds.rs", + "crates/trusted-server-cli/src/release/documents.rs", + "crates/trusted-server-cli/src/release/environment.rs", + "crates/trusted-server-cli/src/release/error.rs", + "crates/trusted-server-cli/src/release/evidence.rs", + "crates/trusted-server-cli/src/release/evidence/candidate_fixture_tests.rs", + "crates/trusted-server-cli/src/release/evidence/joins.rs", + "crates/trusted-server-cli/src/release/evidence/upstream_format_tests.rs", + "crates/trusted-server-cli/src/release/gh.rs", + "crates/trusted-server-cli/src/release/gh/input.rs", + "crates/trusted-server-cli/src/release/github.rs", + "crates/trusted-server-cli/src/release/mod.rs", + "crates/trusted-server-cli/src/release/offline.rs", + "crates/trusted-server-cli/src/release/online.rs", + "crates/trusted-server-cli/src/release/policy.rs", + "crates/trusted-server-cli/src/release/process.rs", + "crates/trusted-server-cli/src/release/source_policy.rs", + "crates/trusted-server-cli/src/release/tests/documents/mod.rs", + "crates/trusted-server-cli/src/release/tests/documents/producer.rs", + "crates/trusted-server-cli/src/release/tests/gh/argv.rs", + "crates/trusted-server-cli/src/release/tests/gh/environment.rs", + "crates/trusted-server-cli/src/release/tests/gh/lifecycle.rs", + "crates/trusted-server-cli/src/release/tests/gh/mod.rs", + "crates/trusted-server-cli/src/release/tests/mod.rs", + "crates/trusted-server-cli/src/release/tests/offline.rs", + "crates/trusted-server-cli/src/release/tests/online.rs", + "crates/trusted-server-cli/src/release/tests/verification/fixture.rs", + "crates/trusted-server-cli/src/release/tests/verification/mod.rs", + "crates/trusted-server-cli/src/release/tests/verification/publication.rs", + "crates/trusted-server-cli/src/run.rs", + "crates/trusted-server-cli/tests/fixtures/release/release-index.json", + "crates/trusted-server-cli/tests/fixtures/release/upstream-formats.json", + "crates/trusted-server-cli/tests/release_cli.rs" + ], + "phase": "2.10", + "state": "Closed; no triggers or production selection" + }, + "phase-2k-docs-and-format-misc": { + "commits": [ + "8510fde38b494ad7c637f0981772709a7a9b0ad5", + "6a07ccc03278af1a3e07b30e25a1cd46efb2f57b", + "58fa27d3e8b45987fe51d36a80a7d6db74b3e072" + ], + "files": [ + ".github/workflows/format.yml", + "CHANGELOG.md", + "crates/trusted-server-adapter-spin/src/app.rs", + "crates/trusted-server-adapter-spin/src/middleware.rs", + "crates/trusted-server-adapter-spin/tests/routes.rs", + "crates/trusted-server-release/tests/fixtures/README.md", + "docs/.vitepress/config.mts", + "docs/guide/cli.md", + "docs/guide/edgezero.md", + "docs/guide/error-reference.md", + "docs/guide/getting-started.md", + "docs/guide/release-verification.md", + "scripts/docs-proposal/hunks.mjs", + "scripts/docs-proposal/hunks.test.mjs", + "scripts/release/qualification/README.md", + "scripts/release/tests/fixtures/README.md" + ], + "phase": "2.11", + "state": "Closed; no triggers or production selection" + } + }, + "phase1_code_reuse": [ + { + "path": "crates/trusted-server-cli/src/commands/dev/lint/domains.rs", + "source_commit": "6d0efda7210983021ab3c9efe8a0a3190e9eb81b", + "subset": "SLSA and SPDX exact reference-host entries. Other source scanner changes and tests stay in the consumer slice." + }, + { + "source_path": "crates/trusted-server-release/src/recipe/native.rs", + "source_commit": "10167c6017b725d0a4d5ad0a0c8e6a96fd4aa807", + "destination": "scripts/release/phase1.mjs", + "subset": "Translate the small planted-operator-value byte scanner; no producer crate or qualification machinery is imported." + } + ], + "axum_amendment": { + "status": "Update for Axum before deferred slices reopen; frozen v1 contracts stay unchanged", + "executables": 7, + "phase1_release_files": 29, + "requires_reviewed_schema_and_policy_amendment": true, + "full_graph_note": "The frozen full graph has 4 release-wide files plus 7 per executable: 32 for 4 executables, or 53 for 7. Phase 1 publishes only 4 per executable plus SHA256SUMS: 29.", + "paths": { + "crates/trusted-server-release/src/policy.rs": "Update for Axum: ReleaseTarget/ALL and producer FIXED_ARTIFACTS encode four identities; extend targets, server product and exact seven-entry cohort.", + "crates/trusted-server-cli/src/release/policy.rs": "Update for Axum: Consumer FIXED_ARTIFACTS/validate_artifacts encode four identities; review seven-entry policy and new capability forms.", + "crates/trusted-server-cli/src/release/bounds.rs": "Update for Axum: COHORT_EXECUTABLES is four; review all derived bounds.", + "crates/trusted-server-cli/release-policy.json": "Update for Axum: Four records and cohort_executables=4; leave carried bytes unchanged until reviewed consumer-policy amendment.", + "scripts/release/release-cohort.json": "Update for Axum: Phase 1 deliberately adds three Axum records; original four remain unchanged and no Phase 1 reader is added.", + "scripts/release/release-tools.json": "Update for Axum: Existing recipe/tool records cover four artifacts; keep carried bytes unchanged and review musl/ARM/Axum tool identities later.", + "scripts/release/qualification/approvals.json": "Update for Axum: Qualification keys cover the original four products and environments.", + "scripts/release/qualification/results.schema.json": "Update for Axum: Asset/environment/tool/platform enumerations require Axum, musl and ARM review.", + "docs/superpowers/specs/schemas/release-index-v1.schema.json": "Update for Axum: Frozen min/max4, identity-presence clauses and target/capability/compatibility enums require reviewed amendment; do not edit this carried schema.", + "crates/trusted-server-release/src/distribution.rs": "Update for Axum: cohort_order and index validation enforce exact FIXED_ARTIFACTS membership.", + "crates/trusted-server-release/src/distribution/ordinary.rs": "Update for Axum: DESCRIPTOR_SLOTS[4] and four-job collection require three additional entries.", + "crates/trusted-server-release/src/publication/model.rs": "Update for Axum: INDEX_ARTIFACTS=4 and derived 32-asset full graph; seven-artifact full graph would have 53, not Phase 1 count 29.", + ".github/workflows/test.yml": "Update for Axum: Selection/candidate-index dependencies, descriptor variables and four-job fixture invocation need seven producers.", + "crates/trusted-server-release/src/policy/tests.rs": "Update for Axum: Exact four-name assertions and every-native-is-CLI assumptions.", + "crates/trusted-server-release/src/collection/tests.rs": "Update for Axum: Exact four-artifact expectation.", + "crates/trusted-server-release/src/distribution/tests.rs": "Update for Axum: Four-entry schema joins and duplicate-entry assertions.", + "crates/trusted-server-release/src/distribution/tests/transfer.rs": "Update for Axum: Fewer-than-four rejection cases.", + "crates/trusted-server-release/src/distribution/tests/golden.rs": "Update for Axum: Four-artifact embedded candidate fixture set.", + "crates/trusted-server-release/src/recipe/tests/candidate_entry.rs": "Update for Axum: Explicit four-artifact rebuild outcome list.", + "crates/trusted-server-release/src/recipe/tests/candidate_index.rs": "Update for Axum: Four-artifact candidate-index assembly fixtures.", + "crates/trusted-server-release/src/publication/tests/inputs.rs": "Update for Axum: 32-asset acceptance/33-asset rejection reflect the full frozen graph.", + "crates/trusted-server-cli/src/release/tests/documents/mod.rs": "Update for Axum: Explicit artifacts.len()==4 assertion.", + "crates/trusted-server-cli/src/release/evidence/candidate_fixture_tests.rs": "Update for Axum: Embedded four-artifact fixture set.", + "crates/trusted-server-cli/tests/fixtures/release/release-index.json": "Update for Axum: Exact original four identities.", + "crates/trusted-server-release/tests/fixtures/index/candidate/candidate-release-index.json": "Update for Axum: Exact original four identities.", + "crates/trusted-server-release/src/inspect.rs": "Update for Axum: Review exhaustive ReleaseTarget matches, native-CLI assumptions, Axum product/runtime inspection and composition.", + "crates/trusted-server-release/src/inspect/elf.rs": "Update for Axum: Review exhaustive ReleaseTarget matches, native-CLI assumptions, Axum product/runtime inspection and composition.", + "crates/trusted-server-release/src/recipe/execute.rs": "Update for Axum: Review exhaustive ReleaseTarget matches, native-CLI assumptions, Axum product/runtime inspection and composition.", + "crates/trusted-server-release/src/recipe/plan.rs": "Update for Axum: Review exhaustive ReleaseTarget matches, native-CLI assumptions, Axum product/runtime inspection and composition.", + "crates/trusted-server-release/src/recipe/startup.rs": "Update for Axum: Review exhaustive ReleaseTarget matches, native-CLI assumptions, Axum product/runtime inspection and composition.", + "crates/trusted-server-release/src/recipe/startup/accepted.rs": "Update for Axum: Review exhaustive ReleaseTarget matches, native-CLI assumptions, Axum product/runtime inspection and composition.", + "crates/trusted-server-release/src/recipe/startup/native.rs": "Update for Axum: Review exhaustive ReleaseTarget matches, native-CLI assumptions, Axum product/runtime inspection and composition.", + "crates/trusted-server-release/src/composition/external.rs": "Update for Axum: Review exhaustive ReleaseTarget matches, native-CLI assumptions, Axum product/runtime inspection and composition.", + "crates/trusted-server-release/src/composition/native.rs": "Update for Axum: Review exhaustive ReleaseTarget matches, native-CLI assumptions, Axum product/runtime inspection and composition.", + "crates/trusted-server-release/src/composition/platform.rs": "Update for Axum: Review exhaustive ReleaseTarget matches, native-CLI assumptions, Axum product/runtime inspection and composition.", + "crates/trusted-server-release/src/spdx/platform.rs": "Update for Axum: Review exhaustive ReleaseTarget matches, native-CLI assumptions, Axum product/runtime inspection and composition.", + "crates/trusted-server-release/tests/fixtures/index/candidate/trusted-server-fastly.wasm.build-evidence.json": "Update for Axum: Candidate companions and digest joins expand alongside the seven-artifact candidate index.", + "crates/trusted-server-release/tests/fixtures/index/candidate/trusted-server-fastly.wasm.build-recipe.json": "Update for Axum: Candidate companions and digest joins expand alongside the seven-artifact candidate index.", + "crates/trusted-server-release/tests/fixtures/index/candidate/trusted-server-fastly.wasm.rebuild-check.json": "Update for Axum: Candidate companions and digest joins expand alongside the seven-artifact candidate index.", + "crates/trusted-server-release/tests/fixtures/index/candidate/trusted-server-fastly.wasm.spdx.json": "Update for Axum: Candidate companions and digest joins expand alongside the seven-artifact candidate index.", + "crates/trusted-server-release/tests/fixtures/index/candidate/ts-aarch64-apple-darwin.build-evidence.json": "Update for Axum: Candidate companions and digest joins expand alongside the seven-artifact candidate index.", + "crates/trusted-server-release/tests/fixtures/index/candidate/ts-aarch64-apple-darwin.build-recipe.json": "Update for Axum: Candidate companions and digest joins expand alongside the seven-artifact candidate index.", + "crates/trusted-server-release/tests/fixtures/index/candidate/ts-aarch64-apple-darwin.rebuild-check.json": "Update for Axum: Candidate companions and digest joins expand alongside the seven-artifact candidate index.", + "crates/trusted-server-release/tests/fixtures/index/candidate/ts-aarch64-apple-darwin.spdx.json": "Update for Axum: Candidate companions and digest joins expand alongside the seven-artifact candidate index.", + "crates/trusted-server-release/tests/fixtures/index/candidate/ts-x86_64-apple-darwin.build-evidence.json": "Update for Axum: Candidate companions and digest joins expand alongside the seven-artifact candidate index.", + "crates/trusted-server-release/tests/fixtures/index/candidate/ts-x86_64-apple-darwin.build-recipe.json": "Update for Axum: Candidate companions and digest joins expand alongside the seven-artifact candidate index.", + "crates/trusted-server-release/tests/fixtures/index/candidate/ts-x86_64-apple-darwin.rebuild-check.json": "Update for Axum: Candidate companions and digest joins expand alongside the seven-artifact candidate index.", + "crates/trusted-server-release/tests/fixtures/index/candidate/ts-x86_64-apple-darwin.spdx.json": "Update for Axum: Candidate companions and digest joins expand alongside the seven-artifact candidate index.", + "crates/trusted-server-release/tests/fixtures/index/candidate/ts-x86_64-unknown-linux-gnu.build-evidence.json": "Update for Axum: Candidate companions and digest joins expand alongside the seven-artifact candidate index.", + "crates/trusted-server-release/tests/fixtures/index/candidate/ts-x86_64-unknown-linux-gnu.build-recipe.json": "Update for Axum: Candidate companions and digest joins expand alongside the seven-artifact candidate index.", + "crates/trusted-server-release/tests/fixtures/index/candidate/ts-x86_64-unknown-linux-gnu.rebuild-check.json": "Update for Axum: Candidate companions and digest joins expand alongside the seven-artifact candidate index.", + "crates/trusted-server-release/tests/fixtures/index/candidate/ts-x86_64-unknown-linux-gnu.spdx.json": "Update for Axum: Candidate companions and digest joins expand alongside the seven-artifact candidate index.", + "crates/trusted-server-release/tests/fixtures/index/candidate/tsjs-inventory.json": "Update for Axum: Candidate companions and digest joins expand alongside the seven-artifact candidate index." + } + }, + "phase1_interim_producers": { + "scripts/release/phase1-build.sh": { + "replaced_by": ["2.5"] + }, + "scripts/release/phase1.mjs": { + "replaced_by": ["2.5", "2.6"] + }, + ".github/workflows/release.yml inline publisher": { + "replaced_by": ["2.8"] + } + } +} diff --git a/docs/superpowers/specs/2026-01-15-attestation-design.md b/docs/superpowers/specs/2026-01-15-attestation-design.md index 87b135bdbc..bec21f80b0 100644 --- a/docs/superpowers/specs/2026-01-15-attestation-design.md +++ b/docs/superpowers/specs/2026-01-15-attestation-design.md @@ -1,6 +1,6 @@ # Runtime Integration Attestation Architecture -> **Status:** Proposal +> **Status:** Historical proposal. Binary-attestation sections are superseded in direction by the [October release artifact specification](2026-10-09-1235-release-attestation-design.md), which becomes the implementation contract upon approval. Runtime/configuration proposals remain reference material for #161. > **Author:** Architecture Team > **Date:** January 2026 @@ -563,7 +563,7 @@ Sigstore uses DSSE envelopes carrying in-toto Statements. The runtime attestatio "attested_by": { "identity": "iab-tech-lab", "oidc_issuer": "https://token.actions.githubusercontent.com", - "certificate_subject": "https://github.com/IABTechLab/trusted-server/.github/workflows/release.yml@refs/tags/v1.5.0" + "certificate_subject": "https://github.com/IABTechLab/trusted-server/.github/workflows/release-build.yml@refs/tags/v1.5.0" } }, "integrations": [ @@ -1164,6 +1164,12 @@ This section provides detailed code examples and implementation patterns for the ### GitHub Artifact Attestations (CI/CD) +This binary workflow example is historical and superseded by the [October +release contract](2026-10-09-1235-release-attestation-design.md#r-sign). Its +current certificate SAN names `release-build.yml`; the caller `release.yml` +does not satisfy that signer identity. Runtime/configuration and publisher +proposals below remain historical #161 material. + Use [GitHub Artifact Attestations](https://docs.github.com/actions/security-for-github-actions/using-artifact-attestations/using-artifact-attestations-to-establish-provenance-for-builds) to create SLSA Build Level 2/3 provenance for the WASM binary. ```yaml diff --git a/docs/superpowers/specs/2026-10-09-1235-release-attestation-design.md b/docs/superpowers/specs/2026-10-09-1235-release-attestation-design.md new file mode 100644 index 0000000000..c517710c63 --- /dev/null +++ b/docs/superpowers/specs/2026-10-09-1235-release-attestation-design.md @@ -0,0 +1,763 @@ +# Release artifact attestation and verified consumption + +## Phasing — 2026-10-22 + +This is the phased rework of [#1262](https://github.com/IABTechLab/trusted-server/pull/1262), frozen at `122b78664b1235c717e9208fd200f9a9b0c335df`. The reviewed contract below is retained as the destination design. This section controls phase applicability; historical statements about first-release gates and PR ownership below refer to the original combined proposal. No new spec approval cycle is introduced by reducing scope. + +**Phase 1 contributes to #1235 (D7 settings pending):** tag-triggered builds of Fastly wasm, three native CLI binaries and three production Axum binaries; SLSA provenance and a shipped, attested SBOM for each binary; draft/upload/publish of an immutable release with `SHA256SUMS`; GitHub CLI verification pinned to the reusable `release-build.yml` signer and predicate; and CI enforcement of configuration independence. Operators deploy the verified release bytes. Reproducibility is not a Phase 1 claim or gate. The source-version and fresh JS mechanisms already on main are reused. + +The phase boundaries follow [aram356's round-1 architecture review](https://github.com/IABTechLab/trusted-server/pull/1262#pullrequestreview-5484754857): + +| Reviewer boundary | Phase ownership | +| ---------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 1. Controlled build and composition evidence | Phase 1: standard pinned, locked builds and conservative Cargo/npm SBOMs. Phase 2.1–2.7 and 2.9: reviewed controlled recipes, qualification, composition and candidate evidence from #1262. | +| 2. Signing and immutable publication | Phase 1: actions/attest and an immutable GitHub Release. Phase 2.8: the reviewed publication owner. | +| 3. Trusted Server verifier library, trusted local policy and source approval | Phase 1: users invoke GitHub's gh attestation verify. Phase 2.10: the reviewed custom consumer and policy from #1262. | +| 4. Opaque VerifiedArtifact with retained bytes | Phase 2.10: the reviewed consumer retains verified bytes; no custom verified value is required for Phase 1. | +| 5. EdgeZero prepared payload and resolved context | Later phase, not started; does not block #1235. | +| 6. Observed operation state and recovery record | Later phase, not started; does not block #1235. | + +Native reproduction is deferred to Phase 2+, where it is measured, not a release gate. Fastly-first byte-for-byte reproducibility is a later phase; Phase 1 makes no reproducibility claim. One artifact's reproduction therefore cannot hold up the first attested release. + +Phase 1 publishes SPDX 2.3 for a conservative selected-Cargo-target and installed-npm-build-tree inventory, with the final binary hash. CycloneDX is only an intermediate generator format. Complete composition qualification below remains Phase 2+; Phase 1 does not claim exact retained-code or native-source closure. + +### Phase 1 input audit and operator evidence + +The Phase 1 audit against main at `f9281917a7260b723f5f22175911c6d2147ef650` +supplements the historical input table below without changing its destination +requirements. Source references: [core build inputs](https://github.com/IABTechLab/trusted-server/blob/f9281917a7260b723f5f22175911c6d2147ef650/crates/trusted-server-core/build.rs), +[JS private output](https://github.com/IABTechLab/trusted-server/blob/f9281917a7260b723f5f22175911c6d2147ef650/crates/trusted-server-js/build.rs), +[JS module options](https://github.com/IABTechLab/trusted-server/blob/f9281917a7260b723f5f22175911c6d2147ef650/crates/trusted-server-js/lib/build-all.mjs), +[Axum dependency features](https://github.com/IABTechLab/trusted-server/blob/f9281917a7260b723f5f22175911c6d2147ef650/crates/trusted-server-adapter-axum/Cargo.toml), +and [CLI version output](https://github.com/IABTechLab/trusted-server/blob/f9281917a7260b723f5f22175911c6d2147ef650/crates/trusted-server-cli/src/run.rs). + +- Rust is pinned to **1.98.1**. The tracked `Cargo.toml`, `Cargo.lock`, + `.cargo/config.toml`, tool pins, source tree and release profile remain inputs. +- Workspace build scripts include core and JS plus the new CLI and Fastly owner `build.rs` files. + Core also reads `build_support/git_version.rs`, its source tree, the root + manifest/lockfile and tracked `edgezero.toml`. The latter supplies the logical + `trusted_server_config` store ID, not a physical deployment binding. +- JS inputs include `build/bundle_set.rs`, its declared source/manifests/lockfile, + `lib/build-all.mjs` and its indirect `lib/build-module-options.mjs` import. + Fresh npm installation precedes compilation. Cargo embeds modules through + generated `OUT_DIR/tsjs_modules.rs` from private `OUT_DIR/tsjs-dist`; + `crates/trusted-server-js/dist/` is not the release input. Phase 1 rejects the development + `TSJS_SKIP_BUILD`, `TSJS_PREBUILT_DIR` and `TSJS_TEST` overrides. +- Production includes also cover the GPT bootstrap, CLI consent stub, CLI ad-template collector and + fictional example-config scaffolds. The CLI and locked EdgeZero dependency + contain developer checkout fallbacks via `CARGO_MANIFEST_DIR`; removing these + remains a later recipe requirement. Tracked OpenRTB generated source is an + ordinary Rust input, not ambient protobuf generation during this build. +- Cargo supplies package/target facts and `OUT_DIR`; core's Git resolver reads + source refs unless overridden. Phase 1 already supplies the validated full + source SHA through `TRUSTED_SERVER__GIT_VERSION`. Locked EdgeZero CLI at + `85d933b8044dec29fe9e4685cae194eaf0290911` additionally generates its + linked-adapter registry from its own manifest and feature selection. +- Axum has no own build script or compile-time `env!`/`include_*` inputs. + Its normal release dependency enables core defaults only; `test-utils` is + dev-only. Shared logical IDs and JS inputs above remain compiled inputs; + application configuration and secrets are runtime inputs. This audit does + not claim the deferred complete native compiler/library closure. + +For the R-OFFLINE decision below, Phase 1 uses the guide's +[offline bundle verification](../../guide/release-verification.md#verify-bundles-offline) +with independently obtained roots and source pins. Offline success is build +proof, not current official membership or deployment authorization. + +For #161, release Fastly `x-ts-version` is the full 40-character source commit; +`ts --version` labels package version, source commit, target and variant. The executable +digest is verified externally; exposing it at runtime remains with #161. +The [verification guide](../../guide/release-verification.md) records these facts +and the maintainer settings gates. Local Viceroy execution proves a local route, +not submission to a Fastly service. + +**Phase 2+ retains all remaining requirements:** two-build reproducibility, confined recipes, platform qualification, authenticated tool/image acquisition, complete composition/SPDX coverage, candidate collection/index, the custom consumer, publication owner, Task 14 same-commit validation, and Cloudflare/Spin distributions plus full custom-verifier qualification of Axum. These slices remain closed to production selection and signing until separately qualified and authorized. + +**Phase 1 Axum extension:** the original four asset names remain unchanged; +three named Axum variants extend this slice to seven binaries and 29 files. +GNU builds enforce a maximum glibc requirement of 2.34; musl is fully static. +The cohort records this explicit extension, but tools/policy and the frozen v1 +index schema remain unchanged. Phase 1 emits no index. The later index/consumer +slices need a reviewed seven-artifact amendment, including new target/capability +forms; see the [compatibility audit](../plans/binary-attestations-phase1-compatibility.md) +and slice-map annotations. + +### Phase 1 review notes + +Phase 1 is **part of #1235**, pending D7 maintainer settings. These notes supersede +historical phase applicability, not the retained destination requirements. +Goals, Non-goals, R-COHORT and R-BOUNDS refer to the original four-artifact design; +this slice explicitly includes seven binaries / 29 files, including Axum and +Linux ARM64. Its original four public names stay unchanged; no v1 index is emitted. +The Node producer helpers and inline publisher are interim exceptions to the +retained Rust-producer constraint, replaced by slices 2.5 (recipes/inspection), +2.6 (composition/SPDX) and 2.8 (publication). The frozen Status and tool versions +below describe #1262, not current Phase 1 implementation status. + +Current tool pins: Rust 1.98.1, Node 24.20.0, Fastly CLI 16.0.0, Viceroy 0.21.0; +verification uses GitHub CLI 2.102.0. The reusable release-build.yml is the signer. +Slice 2.3 immutable build facts are reused from introducing commit +`bfb697404551a28217af8906024506095bcd9559` and revision +`6d0efda7210983021ab3c9efe8a0a3190e9eb81b`, as retained at the frozen source. +Core's resolver and new CLI/Fastly owner build scripts validate +`TRUSTED_SERVER__RELEASE_BUILD=1` and the full lowercase source SHA. +Cargo supplies the owning target/package; features supply the variant. Tags and +run IDs stay external. CLI now labels package/source/target/variant; Fastly queues +the same immutable facts through existing startup diagnostics in reuse mode only; single mode retains the source response header without per-request build-fact logs. The source audit +permits exactly this release control, GIT_VERSION and Cargo-owned BUILD_TARGET, +not deployment values. CLI source-resource cleanup remains deferred. + +Axum binding is runtime-only: `EDGEZERO__ADAPTER__HOST` defaults to loopback; +containers can set `0.0.0.0`, with runtime `EDGEZERO__ADAPTER__PORT` or the compatible `PORT` override. Both values are validated, and conflicting values fail startup. The smoke uses +bridge networking and reaches the process through a published host port. +TLS termination, graceful shutdown and request/resource limits remain follow-up. + +Publication requires `RELEASE_PUBLICATION_ENABLED=true` only after maintainers +confirm immutable releases and the v\* tag ruleset. There is no settings token or +prepublication settings API call, following the reviewed signing boundary; +postpublication immutable=true readback fails loudly. Hyphenated tags are +prereleases and never Latest. Stable tags are normal releases with `make_latest: legacy`, allowing GitHub to choose Latest by version/date rather than forcing backports to Latest. Eligibility of +rc-branch tags is an unresolved maintainer decision; Phase 1 does not implement +the destination branch-ancestry/ruleset validator or authorize rc tags. + +The phase labels on requirement headings below apply only to the reduced Phase 1 subset described above; all additional clauses and requirements marked Phase 2+ remain deferred. The original assurance profiles, custom verifier, retained evidence graph and reproduction gates do not block the first Phase 1 release. The [implementation plan](../plans/2026-10-13-attested-release-implementation.md#phasing--2026-10-22) contains the acceptance mapping and exhaustive slice inventory. + +**Issue:** [#1235 — Publish attested release binaries with build provenance and SBOMs](https://github.com/IABTechLab/trusted-server/issues/1235) + +**Date:** 2026-10-09. **Review revision:** 2026-10-12. + +**Status:** Draft for maintainer review. As directed on 2026-10-13, this specification, its implementation plans and implementation share PR #1262 on `spec/1235-release-attestation`; no separate implementation PR is opened. Contract/code approval is required before merge and all selected qualification gates remain required before official release enablement. Requirements below describe proposed behavior, not an existing release pipeline. The [feasibility audit](2026-10-12-1235-release-attestation-feasibility.md) records measured prototypes, missing implementations and unresolved hosted/provider qualifications; document review alone is not feasibility proof. + +**Producer implementation constraint (2026-10-14):** Introduced release producer tooling MUST use the small domain-specific host-only `crates/trusted-server-release` Rust 2024/1.95.0 workspace member, excluded from default members and independent of core/CLI/JS crates. No introduced Python tooling is permitted. Existing Node JS collection and thin POSIX wrappers remain; native approved-source Rust bootstrap is independently authenticated and supplies no official binary proof for itself. `ts` remains the consumer, and the [migration addendum](2026-10-14-release-tooling-rust-migration.md) preserves every qualification, acquisition and evidence gate. The producer is not a fifth canonical asset or a generic attestation/publisher abstraction. + +**Implementation baseline:** `44c5b5620bfb1f82e5b959ab5ba8ec69c7091e52` includes #1213 and #1214, both merged on 2026-10-12. Release implementation extends their source-version and isolated JS/prebuilt mechanisms. The feasibility audit used the earlier `7f6056bf83eb96aa4167efca64d43280299fba73` snapshot with then-open PR overlays; those historical probes are not current release qualification. + +**Related work:** [#1213](https://github.com/IABTechLab/trusted-server/pull/1213), [#1214](https://github.com/IABTechLab/trusted-server/pull/1214), [#161](https://github.com/IABTechLab/trusted-server/issues/161), [#1179](https://github.com/IABTechLab/trusted-server/pull/1179), [#799](https://github.com/IABTechLab/trusted-server/pull/799), [#1045](https://github.com/IABTechLab/trusted-server/pull/1045), [#1084](https://github.com/IABTechLab/trusted-server/pull/1084), and [#167](https://github.com/IABTechLab/trusted-server/pull/167). + +## Problem + +Publishers and vendors need evidence that distributed binaries were built from reviewed source, and operators need a route that submits the verified bytes. Ordinary CI publishes no attested distribution. Source deployment delegates to build-capable EdgeZero/Fastly commands. Fastly build annotation can also change Cargo's wasm output. + +The previous draft coupled four-target reconstruction and all-adapter enablement to the first release. This contract instead selects assurance requirements per artifact before execution. Required evidence remains mandatory; an optional measurement cannot quietly become a claim that it passed. Build provenance, publication, local authorization and remote execution are separate claims. + +## Goals + +1. Publish Fastly wasm and three Linux/macOS operator CLI executables with build provenance, artifact-specific SPDX SBOMs, bundles and immutable release membership. +2. Enforce configuration independence and fresh locked inputs, including one shared, twice-built JS module set. +3. Require complete-byte Fastly A/B equality; report native CLI comparisons without making native reproducibility a first-release prerequisite. +4. Document exact verification and a byte-preserving Fastly production route first, then integrate production/staging through a new EdgeZero API. +5. Provide stable build facts and deployment observations for #161 without claiming publisher authorization or remote execution proof. + +## Non-goals + +Operator reconstruction, retained-input archives, automated offline deployment, Cloudflare/Spin/Axum distributions, standalone tsjs assets, external rebuild witnesses and SLSA Build Level 3 require follow-up specifications. Runtime/configuration attestation, publisher enrollment, variable permission-policy distribution, request signing and vendor endorsements remain with their owners. Windows, Linux ARM64, containers, Apple notarization and self-update are excluded from this distribution. + +Offline build-evidence verification is documented here, but cannot establish fresh official release membership and cannot authorize `ts deploy --release`. The external Prebid bundle remains outside the embedded artifact's SBOM. CLI commands that need source resources retain documented prerequisites. + +## Contract vocabulary and ownership + +MUST, MUST NOT, SHOULD and MAY have the meanings in BCP 14, only when capitalized. Each numbered requirement is normative. Rationale, rollout ordering, informational records and deferred investigations do not add implicit acceptance gates. + +Canonical **Build A** is the selected output proposed for publication. **Build B** is a fresh comparison build: complete-byte equality is mandatory for Fastly and the shared JS set, while native CLI B reports a bounded measurement. + +“Retained” specifies its object: an owned byte copy held by the verifier; JS module bytes observed in the final executable; archived reconstruction inputs (deferred); or durable deployment recovery state. These claims are distinct. + +The **declared insensitive envelope** lists recipe inputs qualified to vary without changing output bytes. A **terminal outcome** records `matched`, `mismatched`, `failed` or `unavailable` before the deadline. An **action-scoped verified value** holds verified bytes and approval for one supported operation and context; it grants no unrelated deployment authority. Requirement headings below provide stable R-ID links; T-1 through T-13 identify the test families in the traceability table. + +| Owner | Responsibility | +| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Release tooling | Controlled builds, composition evidence, required comparison gates, signing and immutable publication | +| Trusted Server verifier | Fixed release policy; distinct checks of build evidence, official publication and consumer/action authorization; construction of an opaque `VerifiedArtifact` retaining the bytes | +| EdgeZero | Prepared payload integrity, resolved provider context, concrete lifecycle operations and durable known/unknown outcomes; no IAB release-policy decisions | +| Consumer | Trusted verifier bootstrap, approved source/tag/action/context and credentials | +| #161 | Publisher authority, loaded configuration claims, signing/freshness and runtime transport | + +```mermaid +flowchart TD + A[Controlled build and composition evidence] --> B[Signing and immutable publication] + B --> C[Trusted Server verifier] + P[Trusted local policy and source approval] --> C + C --> D[Opaque VerifiedArtifact with retained bytes] + D --> E[EdgeZero prepared payload and resolved context] + E --> F[Observed operation state and recovery record] +``` + +## 1. Threat model and claim limits + +| Threat | Capability | Defending checks | Residual trust or limitation | +| ---------------------------------------- | -------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | +| TH-1 Asset/mirror tampering | Replace executable, index or companions | R-INDEX, R-EVIDENCE, R-PUBLICATION: signed digests, exact subjects and release membership; negative proof families T-8, T-9 | Authentic malicious source is still malicious | +| TH-2 Malicious evidence transfer | Substitute roots, policy, approved commit or offline pins | R-POLICY, R-BOOTSTRAP, R-OFFLINE: obtain trust inputs independently of supplied evidence; negative proof families T-8, T-9, T-10 | A compromised trusted transfer channel defeats its pins | +| TH-3 Compromised dependency/build script | Generate altered code or fetch undeclared inputs | R-INPUTS, R-JS, R-SBOM, R-BUILD: locked materials, controlled environment, observed/declarative coverage, no build credentials; negative proof families T-2, T-3, T-4, T-5 | Locks authenticate origin, not safety; same malicious input can reproduce | +| TH-4 Rogue tag pusher | Tag unapproved source or bypass checks | R-SOURCE, R-PUBLISH: branch/source authorization, same-commit validation, restricted tag authority; negative proof families T-7 | Authorized maintainers and bypass actors remain trusted | +| TH-5 Compromised runner image/compiler | Produce altered bytes and consistent A/B results | R-BUILD, R-SIGN: checked environment identities, hosted runners, separated signing permissions; negative proof families T-4, T-7 | A/B is project-controlled repeatability, not independent builder trust | +| TH-6 Compromised operator host | Replace gh/ts, retained files or runtime; lie about deployment | R-BOOTSTRAP, R-DEPLOY, R-RECOVERY: trusted bootstrap, retained handles, pre-upload equality, recorded observations; negative proof families T-9, T-11, T-12 | Local checks cannot establish honest remote execution against this host | +| TH-7 GitHub compromise | Misstate workflow identity, source or immutable membership | R-POLICY, R-EVIDENCE, R-PUBLICATION: constrained identities and Sigstore evidence; negative proof families T-7, T-8, T-9 | GitHub/OIDC, applicable roots and release authority are trust roots; this design does not defeat their compromise | + +Build provenance establishes that the constrained workflow produced a digest. Immutable release evidence establishes publication membership. A/B records measured equality under declared conditions. None proves absence of vulnerabilities, truthful configuration claims, publisher/domain authority or that submitted bytes execute at every edge location. SLSA Build Level 2 is an implementation target; a local reusable workflow alone does not establish Level 3. + +## 2. Selected artifacts and assurance profiles + +### R-COHORT + +**Phase:** 1 for the reduced acceptance subset in Phasing; remaining clauses are Phase 2+. + +`scripts/release/release-cohort.json` MUST select the complete artifact set and required assurance profile before jobs execute. The first cohort is `phase1-fastly-cli`. Every selected artifact MUST have successful canonical Build A, build provenance, SBOM, composition validation, shared JS input equality, artifact-specific retained-JS coverage and immutable publication. A failure of any selected requirement MUST block publication; the workflow MUST NOT shrink the set or lower a profile after failure. + +| Asset | Package / target | Required profile | Compatibility qualification | +| ----------------------------- | ------------------------------------------------- | ---------------------------- | ------------------------------------------------------------------------------------------ | +| `trusted-server-fastly.wasm` | `trusted-server-adapter-fastly` / `wasm32-wasip1` | `provenance-sbom-repeatable` | Fastly production defaults; variant `default` | +| `ts-x86_64-unknown-linux-gnu` | `trusted-server-cli` / `x86_64-unknown-linux-gnu` | `provenance-sbom` | glibc floor 2.34; clean UBI 9 (RHEL 9 family), Ubuntu 22.04 and Debian 12 startup fixtures | +| `ts-aarch64-apple-darwin` | `trusted-server-cli` / `aarch64-apple-darwin` | `provenance-sbom` | macOS loader deployment target 13.0; initial release qualification on hosted macOS 15 only | +| `ts-x86_64-apple-darwin` | `trusted-server-cli` / `x86_64-apple-darwin` | `provenance-sbom` | Same loader floor and macOS 15 qualification; actual Intel startup fixture required | + +### R-PLATFORM + +**Phase:** 1 for the reduced acceptance subset in Phasing; remaining clauses are Phase 2+. + +Recipes MUST use explicit packages/targets and recorded effective features/release profile. A pinned Linux builder with glibc no newer than 2.34 MUST establish the GNU compatibility floor; scheduling it on Ubuntu 24.04 does not establish a 2.39 floor. Native link inspection MUST validate the actual ELF GLIBC symbol ceiling and reject undeclared libraries and runner-only absolute paths. + +GNU recipe completion MUST derive its forbidden host and execution paths from the bound checkout, retained JS, materials, Rust tools, whole Cargo state and target roots, plus the host-only output root. The whole Cargo state includes `HOME`, `TMPDIR` and `CARGO_HOME`. Completion MUST inspect and scan the same bounded, nofollow-read bytes, verify their recorded size and SHA-256, reject those paths and `/boundary/`, and require a fixed remapped prefix before recording success. A standalone ELF inspection records structural facts; it does not establish this build-path property. + +The GNU build boundary MUST expose ordinary writable storage only through its checked roots: `/tmp` MUST be an empty read-only tmpfs, private IPC MUST omit `/dev/shm`, and selected images MUST declare no implicit writable volumes. Actual controls MUST demonstrate denied writes outside the roots and successful bound-`TMPDIR` write/read/removal, followed by full JS and Cargo compatibility runs. Generic `/tmp/` runtime lookup strings, such as the locked browser dependency's default search location, MUST NOT alone be classified as runner identity. This Linux refinement does not relax macOS inspection or qualify its filesystem boundary. + +macOS MUST use a recorded matching-host recipe initially; a reviewed ARM-to-Intel cross recipe MAY replace it only after full CLI build and Intel execution qualification. `MACOSX_DEPLOYMENT_TARGET=13.0` MUST be explicit. It is a loader/ABI floor, not a claim that macOS 13/14 execution is qualified. + +The initial required startup matrix is macOS 15 on ARM64 and Intel. macOS 13 hosted runners have retired and macOS 14 is retiring; neither is a first-release test gate. Broader runtime support requires separately available execution hosts and evidence. Schema `compatibility.minimum_version` records the loader/libc floor; `build-evidence.compatibility_tests` records the actually qualified OS, architecture and outcome. Intel execution remains mandatory on an Intel host. `macos-15-intel` availability MUST NOT be assumed indefinitely: qualify a replacement execution host or review removal of the target before that runner becomes unavailable. Failure to establish these baselines blocks this cohort before its first tag; it does not justify advertising untested compatibility. A static musl distribution is an alternative requiring a reviewed cohort/target change. + +### R-VARIANT + +**Phase:** 2+. + +The first Fastly artifact MUST retain the existing default, single-request production variant, with `reusable-sandbox` disabled. This is a deliberate conservative shipping choice pending deployed reuse qualification, not a claim that reuse is publisher configuration. There is no Fastly `test` feature. A second attested `reusable-sandbox` variant, or promotion to the default after the four runtime bounds and reuse lifecycle pass qualification, requires a reviewed cohort/policy change. Operators requesting reuse MUST receive an explicit unsupported-variant diagnostic rather than be told an unattested custom build is official. + +Native CLI A/B outcome is reported as `matched`, `mismatched`, `failed` or `unavailable`. Its measurement job MUST reach a bounded terminal outcome, but equality is not required by `provenance-sbom`. A later change can promote an individual target after qualification. No profile in this contract claims separately bootstrapped operator reconstruction. + +## 3. Configuration independence and complete build inputs + +### R-CONFIG + +**Phase:** 1 for the reduced acceptance subset in Phasing; remaining clauses are Phase 2+. + +Operator configuration MUST NOT be compiled into release binaries. Publisher domains, account/service IDs, physical bindings, active settings, secrets, selected integrations, configuration revisions and deployment-variable permission policy MUST enter at runtime. All artifacts MUST use the tracked repository `edgezero.toml` unchanged. Logical store identifiers, product defaults and the fictional CLI scaffold are source-defined product inputs shared by all operators. The scaffold MUST NOT be an active-config fallback. The permission work in #1045/#1084 MUST resolve any conflicting embedded variable-policy design before release enablement; this contract does not choose its distribution protocol. + +### R-INPUTS + +**Phase:** 1 for the reduced acceptance subset in Phasing; remaining clauses are Phase 2+. + +Build evidence MUST record every input class below, with selected identities/digests and roles. Introducing an input requires updating the inventory in the same implementation PR. Generic SLSA provenance is not assumed to enumerate these transitively. + +| Input | Scope and control | +| ---------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| Git source snapshot | Exact peeled tag commit and tracked bytes of workspace/crate manifests, Rust/TS/JS source, embedded assets, build scripts, and release scripts | +| Cargo resolution | Root `Cargo.lock`, registry checksums, exact git revisions, resolved package/features, target and host dependency roles | +| npm resolution | JS `package.json` and `package-lock.json`, pinned Node and bundled npm version, installed package versions and integrity metadata | +| Toolchain configuration | `rust-toolchain.toml`, `.tool-versions`, `.cargo/config.toml`, Rust compiler/LLVM/sysroot identity, Node/npm identity | +| Build recipe | Package and binary target, target triple, explicit feature policy, release profile including current `debug = 1`, linker/native compiler settings and any byte transformations | +| Repository EdgeZero manifest | Entire tracked `edgezero.toml`, default logical configuration ID derived by core, and the manifest digest | +| Core template build digest | All non-editor regular files under `core/src`, core `build.rs`/`Cargo.toml`, and root `Cargo.toml`/`Cargo.lock`/`edgezero.toml`; normalized relative paths and original bytes | +| Embedded/generated JS | Fresh core and discovered integration IIFEs, exact module IDs/digests, and the generated Rust module table copied into `OUT_DIR` | +| Other embedded assets | Core GPT bootstrap, CLI browser collector/consent scripts, and the repository-root `trusted-server.example.toml` scaffold embedded by the CLI | +| OpenRTB bindings | Checked-in generated bindings and their tracked sources; normal releases do not regenerate protobuf or invoke an ambient `protoc` | +| Dependency-generated code | Build-script and proc-macro outputs, including EdgeZero CLI adapter registrations derived from its pinned manifest and enabled features | +| Release build facts | Validated source commit supplied after environment sanitization; target/package version from Cargo; variant derived by the owning adapter/product; release tag stays external | +| Native build inputs | Runner image version, SDK/Xcode where applicable, linker, compiler/assembler, build tools, vendored native library metadata, and linked external libraries | +| Paths and environment | Checkout/build paths, `CARGO_MANIFEST_DIR`, `OUT_DIR`, sanitized compiler/native-tool settings, explicit Vite mode, and permitted release environment values | + +`TEMPLATE_BUILD_DIGEST` includes complete raw manifest bytes and ordinary non-editor core source files, including untracked files. It is a template/cache compatibility input, not an executable digest or provenance. Fresh checkouts MUST contain only expected source/materials; generated outputs MUST remain outside source-only directories. Before/after checks MUST detect tracked-input changes and unexpected source-directory files. + +### R-ENV + +**Phase:** 2+. + +Release preflight MUST audit inherited variables before clearing them and reject `TSJS_SKIP_BUILD` with any value. It MUST NOT evade this audit by starting under an empty environment. Hosted/setup exports such as `RUSTFLAGS` or `VCPKG_INSTALLATION_ROOT` MUST be rejected unless their exact values and tool/path identities are explicitly reviewed and qualified. + +After auditing, the recipe MUST remove inherited operator `TRUSTED_SERVER__*` and `EDGEZERO__*`, sanitize the child environment, and reinject validated `TRUSTED_SERVER__RELEASE_BUILD=1` and the approved `TRUSTED_SERVER__GIT_VERSION`. Cargo supplies target/package facts and the owning product derives the typed variant (R-FACTS). Compiler/Cargo profile/linker flags, wrappers, Cargo home/configuration layers, native build selectors, `NODE_OPTIONS`, npm/Vite overrides and environment-file loading MUST be selected or rejected by the recipe. No ambient compiler wrapper is allowed. Release Vite mode MUST be explicit. Build jobs MUST have no provider/signing credentials. Wall-clock times, run IDs, publisher identity and final checksum MUST NOT be embedded. `SOURCE_DATE_EPOCH` MUST derive from the approved commit's committer timestamp. + +The CLI's compiled developer checkout fallback (`CARGO_MANIFEST_DIR`) MUST be disabled for official releases; explicit/current-checkout discovery can remain for documented source-dependent commands. Rust remapping does not rewrite arbitrary `env!` strings. CLI help, version and `config init` MUST run outside a checkout. + +## 4. Shared JS build and artifact repeatability + +### R-JS + +**Phase:** 1 for the reduced acceptance subset in Phasing; remaining clauses are Phase 2+. + +Release work MUST extend #1214's private output, expected-module validation, checked test failures and `TSJS_PREBUILT_DIR`; it MUST NOT add a parallel `TSJS_RELEASE_BUILD` fallback mode. One release-wide JS job MUST independently run two fresh locked installations/builds with pinned Node/npm. Both MUST reject any skip flag, missing npm, failed `npm ci`, test failure, missing/empty/extra modules or changed source. Both actual A/B Vitest processes MUST unconditionally receive `TRUSTED_SERVER__RELEASE_JS_TESTS=1` and run all registered tests and type checks; a developer opt-out MUST NOT suppress them. Ordinary development skips only the two slow Git collector fixtures by default; ordinary CI opts into the full suite. Each build owns its installation/output directory and includes required build dependencies. + +The two JS runs MUST compare final bytes for core plus every sorted tracked integration entry point. Only on equality MAY the job publish its Build A bundles and `tsjs-inventory.json`. That version-1 inventory MUST record sorted module IDs, filename, size, SHA-256, contributing source/package identities and tool/lifecycle identities. Each Rust build MUST consume these current-run bundles through absolute `TSJS_PREBUILT_DIR` and `TRUSTED_SERVER__TSJS_PREBUILT_INVENTORY` paths and a checked expected inventory, validate the exact set/digests of bytes copied into `OUT_DIR`, and run no npm. + +The first cohort MUST declare only `fastly-production` for Fastly until the integrated staging milestone is qualified. Adding `fastly-staging` requires a reviewed cohort and consumer-policy update. An artifact capability MUST intersect with an implemented local transport; downloaded metadata cannot enable an unavailable operation. + +The collector MUST verify identical prepared JS input inventories across all builds that compile the JS crate. Each artifact MUST separately record which module bytes remain in its final file. Claimed retained modules MUST match the shared inventory; a CLI executable MAY retain none after linking. Copies into `OUT_DIR` are build inputs and MUST NOT alone be called final embedded code, nor force unused JS into the CLI to satisfy an equality check. + +The shared JS set is a declared input to Rust builds. Sharing it means the Rust A/B pair does not independently test npm/Vite; the separate JS A/B gate supplies that evidence. Installed package inventory and permitted lifecycle outputs MUST be recorded separately from lockfile archive integrity. A platform esbuild binary removed from the prepared installation MUST cause the network-denied bundle phase to fail instead of downloading a replacement. + +### R-BUILD + +**Phase:** 2+. + +Container-backed builds and final-artifact startup MUST acknowledge a full immutable container ID before a separate attached start. Cleanup MUST stop, remove and confirm absence by that ID after execution failures as well as success. Failed or ambiguous creation MUST remain fatal and MUST NOT authorize a start; a name-based absence observation cannot resolve an in-flight creation request. The bounded timeout regression MUST exercise the same lifecycle as real execution. + +`scripts/release/build.sh` and `scripts/release/release-tools.json` at the approved source commit MUST define the executable recipes and checked tool/environment identities. A downloaded descriptor is informational and MUST NOT supply commands. Rust builds MUST use `--locked` (or `--frozen` after verified source preparation), a fresh target directory and checked dependency material. Compiled Cargo/JS outputs MUST NOT be restored from earlier runs. Verified download caches MAY be reused only after archive checksum/revision validation and fresh extraction. The compile/bundle phases MUST enforce network denial with a qualified OS/container boundary, including build-script/lifecycle downloads; a positive unconfined socket control and denied confined socket probe MUST run for each selected recipe. Cargo/npm offline flags, proxy settings or lockfiles alone do not enforce process network denial. Historical local macOS `sandbox-exec` prototypes demonstrated socket denial; replacement Rust boundaries and the full hosted native recipe still require qualification; preparation is a separate network-enabled phase. Required native/system SDK identities and code transformations MUST be checked before compilation. + +### R-REPEAT + +**Phase:** 2+. + +Fastly Build A and Build B MUST run in separate fresh hosted jobs with independent source/dependency materialization and Cargo outputs. Each MUST include every final byte transformation from the recipe. Build B MUST differ from A in each qualified insensitive input: checkout directory, Unix user where supported, umask, `TZ` and locale, start time and Cargo job count. `release-tools.json` MUST list the selected A and B values. The rebuild record MUST mark each dimension `varied`, `normalized` when the recipe pins it internally, or `not-varied` when the runner cannot change it. Only `varied` dimensions count as independence evidence. + +The collector MUST select outputs by the artifact ID and transfer digest returned by each expected job in the current run, verify artifact REST metadata's current workflow run/repository/source SHA, reject reused A/B IDs, validate source/recipe records, and byte-compare complete final wasm. Producing job/attempt association MUST come from the expected upload-step outputs and trusted workflow dependency topology; the artifact API does not expose an independent job/attempt-origin field. Transfer digest is the Actions archive digest, separately checked from final executable SHA-256; transfer mismatch warnings MUST be promoted to failure. It MUST NOT use artifact names as the sole binding. Missing output, inconsistent checked inputs or unequal bytes MUST block the entire selected release. No comparison-time stripping, normalization or excluded byte regions is allowed. + +Native CLI comparison MUST use the same fresh-job/result-binding discipline, but a recorded nonmatching or unavailable B result does not block its profile. Build A and its composition/smoke checks remain mandatory. A signed measurement stating failure MUST NOT be relabeled success. + +### R-NATIVE + +**Phase:** 2+. + +Native records MUST capture compiler, linker, archiver, SDK/sysroot, native source/libraries and relevant options. Linux SHOULD use a content-derived build ID. A future native repeatability-profile qualification MUST cover canonical rustup paths, OSO debug paths/mtimes, a tested content-derived UUID policy and any required ad-hoc signature. Random/disabled UUIDs or removing signatures only for comparison are not acceptable shortcuts. Developer ID signing, notarization and stapling remain outside scope. These controls support native measurements; they do not imply operator reconstruction is supported. + +## 5. Artifact-specific SPDX composition + +### R-SBOM + +**Phase:** 1 for the reduced acceptance subset in Phasing; remaining clauses are Phase 2+. + +The repository-owned Rust producer command `trusted-server-release generate-sbom`, implemented in `crates/trusted-server-release/src/spdx.rs`, MUST emit SPDX 2.3 JSON for each canonical executable from selected Cargo JSON build messages, an unfiltered Cargo metadata identity catalog and explicit-target Cargo tree evidence, the JS lockfile and Rollup contribution inventory, plus declared-and-validated native inputs. Target-filtered metadata can omit dependencies compiled for the host during a cross-build; catalog membership alone MUST NOT establish contribution. Syft and a `RUSTC_WRAPPER` are not required. This settles the generator; its fixture proof precedes release enablement. + +Coverage MUST distinguish observed Rust units, embedded JS contributors, validated native components, external runtime requirements and build materials/tools. With explicit `--target`, Cargo output layout, target kind, package ID and features MUST distinguish host build scripts/proc macros from target units, including a package built in both contexts. Metadata/tree edges MUST be reconciled with actual compiled membership; ambiguous source/unit joins MUST fail. Cargo metadata features are package-level unions, so effective per-unit features MUST come from compiler messages and explicit-target output context. This is a conservative component inventory, not a promise to recover an exact compiler unit dependency DAG from package metadata. Uncompiled dev-only packages MUST NOT be claimed as runtime contributors. Compiled code later eliminated by linking MAY remain in the conservative inventory; exact final function reachability is not claimed. + +The Rollup hook MUST record package/source contributions and virtual/plugin-generated origins. Final bundle hashes MUST be computed after Vite/esbuild transformations. Type-only imports and modules eliminated by Rollup MUST NOT create runtime components. Later minification can remove more code; contribution evidence MUST state this conservative limit. The embedded Prebid shim is covered; a type-only reference to externally distributed `prebid.js` MUST NOT claim that bundle is embedded. Build-only npm dependencies and native bundler tools remain build material even when called dev dependencies. + +Native records MUST bind origin, version/revision, source or linked-library digest, selection policy and validation evidence. The current CLI graph includes AWS-LC, ring and bundled SQLite. The release recipe MUST set `AWS_LC_SYS_USE_SYSTEM=0` and reject conflicting OpenSSL/pkg-config discovery overrides, or explicitly select and digest a reviewed system library. Target-suffixed AWS-LC selectors MUST also be rejected or explicitly selected; a target-specific value can override generic `AWS_LC_SYS_USE_SYSTEM=0`. The recipe MUST set `LIBSQLITE3_SYS_USE_PKG_CONFIG=0` and reject unreviewed SQLCipher/loadable-extension selection; a nonzero pkg-config selector can bypass SQLite's bundled feature. It MUST validate SQLite bundled selection and capture actual native outputs/link directives; a Rust invocation graph alone cannot discover these components. Dynamically linked platform libraries MUST be reported as external requirements, not embedded bytes. Rust sysroot identity MUST also be recorded. + +SPDX root checksum MUST equal the final executable SHA-256. Package IDs MUST include source/revision or installation location where name/version is ambiguous. SPDX relationships MUST distinguish artifact contributors from build tools/materials. Modules compiled as inputs but absent from the final executable MUST remain build material rather than be claimed as embedded JS. Raw substring presence and known source/link behavior supply a conservative retained-byte check for the current uncompressed strings; they do not establish function reachability. Cargo manifests/license files and npm metadata supply licenses; unknown licenses MUST be `NOASSERTION`. Schema, unique IDs, endpoints, coverage and the attestation predicate-size limit MUST be validated; an oversized or incomplete SBOM MUST block publication. Each published SBOM MUST equal the parsed document carried by its artifact's SBOM attestation. + +## 6. Build facts and #161 handoff + +### R-FACTS + +**Phase:** 1 for the reduced acceptance subset in Phasing; remaining clauses are Phase 2+. + +One immutable build-fact mechanism MUST extend merged #1213. Absence of `TRUSTED_SERVER__RELEASE_BUILD` selects development behavior; any present value other than exactly `1` MUST fail. The trusted recipe MUST set `TRUSTED_SERVER__RELEASE_BUILD=1` after inherited-environment audit and sanitation. + +Release Rust builds MUST receive absolute `TSJS_PREBUILT_DIR` and `TRUSTED_SERVER__TSJS_PREBUILT_INVENTORY` paths, validate the inventory and copied bytes, expose its raw digest as `TRUSTED_SERVER__TSJS_INVENTORY_SHA256`, and MUST NOT run npm. Rust Cargo builds MUST reject any presence of `TSJS_SKIP_BUILD`, including empty, `0` and `1`; Node development build/Vitest entry points preserve its development behavior when release control is absent. + +The recipe MUST supply `TRUSTED_SERVER__GIT_VERSION` as the approved full source commit after sanitation, and release builds MUST reject missing/malformed values and prohibit ambient `.git` tag/branch fallback. Development fallback from #1213 MAY remain outside release execution and MUST be recorded as a development input. + +A typed `BuildInfo` MUST derive target from Cargo `TARGET` through `TRUSTED_SERVER__BUILD_TARGET`, package version from `CARGO_PKG_VERSION` and variant/features in the owning adapter/product. Core cannot infer a Fastly feature that is not forwarded to it. Caller-supplied target/variant strings MUST NOT override these derived facts. `TEMPLATE_BUILD_DIGEST` remains separately named. + +The release tag/version MUST stay external in the release index, not be compiled into bytes. Two tags on one source commit can therefore refer to identical bytes. `ts --version` MUST label the fields as `ts package= source= target= variant=default`; it MUST NOT imply the workspace's current `0.1.0` is the external release version. `X-TS-Version` is the approved full source commit in release mode. Development retains #1213's override/tag/branch/short-commit display behavior. A development checked inventory validates its own commit syntax and hashes but MUST NOT compare that source to the development display label. Fastly emits owning package/target/variant facts through its existing startup diagnostics; no additional response header or attestation authority is introduced. Fastly service version uses `X-TS-Fastly-Version`. Neither header is a binary checksum or remote execution proof. + +For the selected default Fastly artifact, settings load per request. Only a future qualified reusable variant retains settings for its sandbox lifetime. #161 MUST distinguish artifact identity, observed service/version and the configuration snapshot actually used; fetching the latest store after a request does not establish that snapshot. Build facts contain no secrets or active configuration. Deployment request-signing keys/JWKS remain unrelated to release signing. + +## 7. Source eligibility, workflow and immutable publication + +### R-SOURCE + +**Phase:** 1 for the reduced acceptance subset in Phasing; remaining clauses are Phase 2+. + +`.github/workflows/release.yml` MUST accept protected `v*` tag pushes and strict stable/prerelease SemVer, including schema-supported build metadata. Official tags MUST be lightweight, so the verified tag target is the source commit; annotated tags MUST be rejected, not confused with their peeled commits. The current reviewed `eligible_branches` is exactly `["main"]`. Eligibility MUST use the actual remote branch tip and commit ancestry, with governance validation of active branch/tag rulesets and bypass actors. Local remote-ref lookalikes MUST NOT supply that authority. + +A future exact `rc/YYYYMM` branch requires maintainer review of its tracked cohort entry and governance inclusion before use, plus actual remote-tip ancestry and ruleset/bypass validation. A regex match or a candidate checkout that lists its own branch MUST NOT approve itself. The tag ruleset and branch policy MUST constrain creation/update/deletion and bypass actors. + +Eligibility MUST run trusted release validation at the exact tag commit for every required AGENTS.md gate. `test.yml`/`format.yml` currently lack push coverage for RC branches, so successful push-to-main checks MUST NOT be the only path. Implementation MUST expose reviewed `workflow_call` validation jobs or equivalent explicit same-commit release jobs. It MUST enumerate required jobs and reject skipped/cancelled/PR-only/fork results. A tag immediately after merge MUST wait for these release validation jobs with a checked deadline (initially 90 minutes), then fail with a rerun diagnostic on timeout. It MUST NOT publish using an unrelated passing check name. + +### R-SIGN + +**Phase:** 1 for the reduced acceptance subset in Phasing; remaining clauses are Phase 2+. + +The local `.github/workflows/release-build.yml` at the caller's source revision MUST own build orchestration, collector validation and file attestations. Build/lifecycle execution MUST occur without OIDC/attestation writes. Signing MUST consume only validated current-run job outputs and MUST NOT compile untrusted source. The authenticated build boundary is the complete workflow invocation, not just the collector. No caller-supplied command, script, arbitrary path/repository or provenance predicate is accepted. All actions MUST be SHA-pinned; tools MUST have exact version/digest pins in `scripts/release/release-tools.json`. + +| Job | Permissions | +| ----------------------------------------- | --------------------------------------------------------------------------------------------------- | +| Eligibility/validation/build/compare | `contents: read`; `actions: read` / `checks: read` only for actual validation/artifact lookup needs | +| Validated artifact and index signing | `contents: read`, `id-token: write`, `attestations: write`; `actions: read` if collecting outputs | +| Draft upload/final publish/reconciliation | `contents: write`; no OIDC, provider credential or repository settings token | + +File attestations MUST NOT request `artifact-metadata: write`; no container storage record is being created. Each canonical artifact MUST get two pinned `actions/attest` invocations: build provenance with the executable and metadata subjects, then SPDX mode with the executable subject and published SBOM as predicate. `sbom-path` does not also create separate build provenance. The collector MUST verify all bundles before index signing. + +### R-PUBLISH + +**Phase:** 1 for the reduced acceptance subset in Phasing; remaining clauses are Phase 2+. + +Maintainers MUST enable immutable releases and active tag rules before first publication and record approval of release authority. No `RELEASE_SETTINGS_READ_TOKEN` is introduced. A tag-scoped concurrency group MUST serialize publication without cancelling a publishing run. The publication job MUST create/resume a matching draft, upload the complete asset set, re-download and verify it, publish, then read `immutable == true` using `GITHUB_TOKEN` and verify the GitHub release attestation. It MUST NOT report success before that final check. + +If a release is confirmed mutable, the job MUST mark publication failed, remove that still-mutable release if authorized by the workflow's release-write role, and raise a release incident with the result. An indeterminate response MUST trigger reconciliation, not blind deletion. This is detection/recovery, not a guarantee that a transient mutable publication was never visible; consumers require immutable signed membership and therefore reject it. Published immutable releases MUST NOT be overwritten or retagged. Defects require an advisory and new version. + +A rerun MUST refuse an already published tag. A partial draft MAY resume only when source/tag and all existing asset digests match the intended set. Conflicting bytes require explicit recovery. A failed optional CLI comparison is reportable; a failed required profile leaves no published release. + +## 8. Distribution schema and evidence graph + +### R-INDEX + +**Phase:** 2+. + +The normative [release-index v1 JSON Schema](schemas/release-index-v1.schema.json) MUST be evaluated with the JSON Schema Draft 2020-12 dialect and defines closed kinds, roles, targets, variants, profiles and capabilities. The governing schema raw bytes MUST remain unchanged absent an explicit reviewed schema amendment. Its retained `$comment` pins Draft 2020-12; Rust `KnownSchema` explicitly selects that dialect with offline evaluation, and committed positive/negative tests exercise the actual governing schema. + +Producer and consumer MUST additionally require `recipe_source_commit` and `cohort_source_commit` to equal the independently approved `source_commit`, and enforce exact cohort membership, unique safe asset names, expected package/target/variant/profile combinations and equality of each size/digest with actual bytes. These repeated v1 source/recipe/cohort equalities intentionally expose the joins; a different revision requires a future reviewed amendment. Structural v1 compatibility may accept `fastly-staging`, but current producer/consumer policy and cohort MUST semantically reject it until reviewed enablement. Tasks 3, 13 and 17 MUST test that rejection. SemVer metadata and schema fields MUST NOT be narrowed without review. + +Unknown schema versions, duplicate JSON keys, duplicate/conflicting identities, traversal names, unknown capabilities and mismatched JS inventories MUST fail. No executable asset is an archive in this cohort. + +Release-wide assets are `release-index.json`, `release-index.provenance.sigstore.json`, `tsjs-inventory.json` and `SHA256SUMS`. Each executable has `.spdx.json`, `.provenance.sigstore.json`, `.sbom.sigstore.json`, `.build-evidence.json`, `.build-recipe.json` and `.rebuild-check.json`. The index MUST name and digest companions and the JS inventory. It MUST NOT include its own digest/bundle; those identities are external. `SHA256SUMS` MUST cover every other published asset, never itself, and is only a convenience until authenticated by release membership. No `release-inputs.tar.zst` is promised here. + +| Document | Version-1 field contract / status | +| ----------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `release-cohort.json` | Producer policy: `schema_version` integer 1; `cohort` fixed string; `eligible_branches` unique exact branch strings; `artifacts` exact schema artifact combinations and required profiles. Source-controlled, never selected by downloaded metadata | +| `release-tools.json` | Producer policy: `schema_version` 1; `tools` sorted records of name/version/SHA-256/acquisition identity; `environments` checked image/SDK identities; `recipes` script paths/digests and effective configuration | +| `tsjs-inventory.json` | `schema_version` 1; `source_commit` 40 lowercase hex; `modules` sorted unique records with `id`, `filename`, `size` nonnegative integer, `sha256` 64 lowercase hex and contribution records; tool/material records | +| `.build-evidence.json` | `schema_version` 1; `source_commit`, `target`, `variant`, `artifact_sha256`; `js_inventory_sha256`; `prepared_modules` sorted input records and `compatibility_tests` records of actual OS/architecture/environment/outcome; `embedded_modules` sorted unique records of `id`, `filename`, nonnegative integer `size` and 64-character lowercase hex `sha256`, observed in the final executable and matched to the shared inventory; absence after linking is explicitly recorded, while `OUT_DIR` copies are prepared input evidence; `inputs`, `rust_units`, `js_contributors`, `native_components`, `external_requirements`, `tools` arrays containing identity/digest/role evidence from R-INPUTS/R-SBOM | +| `.build-recipe.json` | Informational: version 1; recipe source commit/script/tool identities and selected effective settings. MUST NOT be executed as a supplied program | +| `.rebuild-check.json` | Informational to consumers: version 1; profile, recipe/source/JS digest; A/B run/job/artifact IDs and transfer digests; environments, final size/SHA-256 or bounded failure status; outcome enum `matched`, `mismatched`, `failed`, `unavailable` | +| Operation record/receipt | Private lifecycle observation under R-RECOVERY, not a public claim schema. #161 owns any future authenticated receipt use; no normative `verified-publication.json` | + +These JSON files MUST use UTF-8, reject duplicate keys/nonfinite numbers and use safe relative asset names. Digests bind exact distributed file bytes; verifiers MUST NOT reserialize before hashing. Arrays with set semantics MUST have deterministic order. Producer tooling and the Trusted Server wrapper MUST implement strict parsed SBOM-document equality (object key order ignored, array order significant, duplicate keys rejected); it is not substituted for file-byte digest checks. gh verifies a predicate and returns it; it supplies no SBOM-file comparison operation. Duplicate keys MUST be rejected before signing as well as before consumer comparison. Informational descriptors/measurements remain authenticated companions, but consumers MUST NOT execute them or infer independent trust from their contents. + +### R-EVIDENCE + +**Phase:** 2+. + +Every successful verification MUST establish all equalities below: + +| Edge | Required equality/check | +| ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Local policy → index | Repository, exact tag/source commit, cohort, package/kind/role/target/variant/profile/capability equal the approved local contract; recipe/cohort revisions equal approved source, and producer policy identity matches the locally computed current-policy identity or the trusted historical-policy allowlist | +| Index provenance → index | Exact index subject SHA-256; approved certificate/source/builder/caller constraints | +| Index → executable/companions/JS inventory | Exact names, lengths and SHA-256; no unlisted code or conflicting identity | +| Artifact provenance → executable/metadata | Expected executable, SBOM, build evidence, recipe and rebuild-check all present as exact subjects; no extra unapproved subjects | +| SBOM attestation → executable/document | Executable subject SHA-256; SPDX predicate equals published parsed SPDX; SPDX root checksum equals executable | +| Build evidence → JS inventory | Prepared input digest/table equals shared inventory; each claimed final retained module matches that inventory and per-artifact final-byte evidence | +| GitHub release evidence → distribution | Verified release statement binds repository/tag/target and consumed signed subject names/digests; signed index sizes equal actual bytes and REST asset sizes (release subjects have no size field) | +| Consumer → action | Approved source, local operation and service/environment/context before constructing the action-scoped verified value | + +The recipe and rebuild records are authenticated for audit, but consumers do not rerun a project-controlled comparison or treat its JSON outcome as an independent authorization authority. The reviewed producer workflow enforces the profile; consumers enforce its identity and selected profile. + +## 9. Fixed verifier policy, bootstrap and online commands + +### R-POLICY + +**Phase:** 2+. + +`crates/trusted-server-cli/release-policy.json` MUST be a tracked, embedded policy with `policy_version` integer and an external SHA-256 of its exact bytes. Only a reviewed CLI release MAY update it. Index `policy_version`/`policy_sha256` identify the producer policy tracked at `source_commit`, not a downloadable replacement for the consumer policy. The consumer MUST match that pair to independently trusted source-policy bytes, then accept either its own policy version and externally computed exact-byte digest, or a differing producer identity in a separately embedded reviewed historical allowlist. The current policy MUST NOT contain its own SHA-256 or be required to list itself; this avoids a self-hash cycle. Its own acceptance constraints always remain in force. New CLI policies MUST explicitly retain or retire historical identities; the index cannot pick its verifier policy by declaring a hash. Downloaded indexes, operator configuration, roots or arbitrary forwarded gh flags MUST NOT redefine it. The policy MUST fix GitHub host/repository numeric IDs, certificate identities/issuer, source/builder/caller semantics, supported artifact/profile/action combinations and resource bounds. Automation MUST also approve a source commit/tag; an index cannot approve its own source. + +Task 17 MUST obtain producer-policy bytes from an independently approved source snapshot or the fixed official GitHub Contents API endpoint `https://api.github.com/repos/IABTechLab/trusted-server/contents/crates/trusted-server-cli/release-policy.json?ref=`, using `Accept: application/vnd.github.raw+json`. The ref MUST come from independent full-commit approval, never the index. Fetching MUST use an explicit read-only credential, the 1 MiB policy bound, and the overall verification deadline. Arbitrary hosts/paths and redirects that change that origin/path or forward credentials MUST be rejected. The producer hash/version MUST be computed from those approved-source bytes before applying the consumer's own compiled constraints. Offline checks require an independently transferred approved source-policy snapshot and establish build proof only, never official deployment. [GitHub Contents API](https://docs.github.com/en/rest/repos/contents#get-repository-content) documents raw media and explicit commit refs. + +The initial verified tool contract is GitHub CLI **2.97.0** and `actions/attest` **4.2.2** (SHA `1e69f48acb82d1966a394da916b4c1698aa569d6`). Implementation MUST record platform gh binary digests in `release-tools.json` and qualify this exact version. Other versions MUST fail with a compatibility diagnostic until a reviewed policy/tools update qualifies them. This is an exact supported version, not an unspecified minimum. Checking the version string is not proof that an executable is trustworthy. + +| Authenticated expectation | Required value | +| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Repository / owner IDs | `950860906` / `82903538`, with `IABTechLab/trusted-server` on `github.com` | +| Certificate SAN / provenance builder ID | `https://github.com/IABTechLab/trusted-server/.github/workflows/release-build.yml@refs/tags/` | +| Certificate issuer | `https://token.actions.githubusercontent.com` | +| Source ref / source digest | Exact `refs/tags/` / independently approved 40-character commit | +| Build signer digest | Same source commit for this same-revision local reusable workflow | +| Provenance predicate / build type | `https://slsa.dev/provenance/v1` / `https://actions.github.io/buildtypes/workflow/v1` | +| Caller / external parameters | Exactly `workflow: {ref: "refs/tags/", repository: "https://github.com/IABTechLab/trusted-server", path: ".github/workflows/release.yml"}`; no arbitrary inputs/commands | +| Source resolved dependency | `git+https://github.com/IABTechLab/trusted-server@refs/tags/` with `gitCommit` equal to approved commit | +| Event / runner | `push` / `github-hosted` | +| Invocation | Repository Actions run/attempt identity consistent across collected evidence; no cross-run substitution | + +Certificate-backed fields MUST be checked using the pinned gh verifier. Build-type/caller/external-parameter/source-material/subject semantics MUST additionally be checked in successfully verified provenance, not merely inferred from certificate success. Unexpected required structures or missing constraints MUST fail. Policy fixtures MUST use actual output from the pinned action/toolkit; upgrading it requires explicit fixture review. A future separately protected Level 3 builder changes the signer identity: a reviewed policy update MUST define which CLI versions and releases accept it, document the migration and retain verification of supported historical identities. There is no distinct builder/source revision in today's local workflow layout. + +For both provenance and SPDX proofs, post-success gh JSON certificate checks MUST require `sourceRepositoryIdentifier`, `sourceRepositoryOwnerIdentifier`, `buildConfigURI`, `buildConfigDigest`, `buildTrigger` and `runInvocationURI` to match the numeric repository IDs, exact caller `release.yml@refs/tags/`, approved commit, `push` event and allowed shared run/attempt above. These fields are flattened under `verificationResult.signature.certificate` in the pinned verifier. Provenance `runDetails.metadata.invocationId` MUST equal the certificate run URI. SPDX has no SLSA workflow fields; it uses these certificate constraints plus subject/document checks, not an invented provenance predicate. + +### R-BOOTSTRAP + +**Phase:** 2+. + +Consumers MUST obtain gh through a trusted installation channel, use it plus their approved policy to verify the downloaded `ts` executable before first execution, then let verified `ts` drive gh. The exact qualified gh 2.97.0 executable MUST be privately selected through an authenticated installation path, resolved once and retained as the invocation target. Ambient `brew upgrade gh` is diagnostic only: an unsupported resulting version MUST fail, and version text alone MUST NOT authenticate it. A future reviewed list of qualified versions is a policy/tools amendment, not current broadening. + +A consumer compiled with policy v1 MUST reject an unsupported producer policy v2. A newer consumer MAY accept an explicitly reviewed historical v1 identity pair, but MUST enforce its own current bounds and constraints throughout verification. A source-built CLI can run checks but has no official binary/policy assurance until its bytes independently verify. A compromised operator PATH/host remains outside the guarantee. + +Online operations require GitHub authentication even for public artifacts. The integrated verifier MUST accept only an explicitly supplied `GH_TOKEN` or, as fallback, `GITHUB_TOKEN`, for a read-only public-repository request. It MUST pass the selected credential only as `GH_TOKEN` to the child, clear `GH_HOST`, enterprise tokens, inherited `GH_CONFIG_DIR` and other gh overrides, and set isolated private `GH_CONFIG_DIR` and `XDG_STATE_HOME` directories, a reviewed nonempty `HOME` and disabled prompts. Configuration isolation alone does not isolate gh telemetry/device state; an omitted home/state path can write relative to the current directory. Stored `gh auth login` credentials are not silently inherited; the diagnostic MUST explain the explicit-token requirement. Provider credentials MUST NOT reach gh. Normal direct gh commands can use their own trusted login configuration, but integrated behavior follows this narrower contract. The child environment MUST otherwise use reviewed OS/runtime variables and no dynamic-loader injection settings. + +### R-BOUNDS + +**Phase:** 2+. + +Each gh child MUST have a 120-second wall-clock deadline, at most 128 MiB of JSON stdout and 1 MiB of stderr; an integrated verification attempt MUST have a 15-minute overall deadline. Before parsing/invoking gh, index/policy/inventory files MUST be at most 1 MiB, each bundle/build companion at most 32 MiB, SPDX SBOM at most the pinned action's 16 MiB limit, and trusted-root snapshot at most 1 MiB, and each executable at most 512 MiB. The integrated verifier MUST verify one retained, membership-checked bundle per invocation and accept exactly one matching attestation result; JSON output includes both bundle and parsed predicate, so its bound is larger than the input bound. Metadata nesting MUST be limited to 64 levels and the cohort to four executables with their declared companions. Timeout, cancellation, oversize input/output, parser ambiguity, failed exit or incomplete JSON MUST fail, terminate/reap children and construct no `VerifiedArtifact`. Bounds can change only through reviewed policy. Diagnostic output MUST exclude credentials and secret context. + +### R-PUBLICATION + +**Phase:** 2+. + +Online verification MUST combine pinned gh signature verification with explicit REST and verified-statement policy checks; successful gh exit statuses alone are insufficient. `release verify-asset` matches the file digest even when its local name differs, does not check raw tag type or the API immutable flag, and does not authenticate an asset-size field. The wrapper MUST compare signed subject names/digests, independently approved source/type, REST immutable state and index/actual/API lengths. Online verification MUST verify the GitHub immutable release, confirm its lightweight tag object is a commit, match the signed release target to the approved commit, authenticate index provenance before using its fields, verify R-EVIDENCE, and verify immutable membership for every consumed asset. Successful verification of one predicate MUST NOT stand in for all predicates. Only then, after local action/context authorization, MAY the library construct private `VerifiedArtifact` with retained bytes. No provider write, secret/config creation or build hook is allowed during verification. + +The following fictional release example shows the primitive commands. They require the approved gh version and authentication. `` comes from trusted consumer approval checked against the signed lightweight release target, not from the downloaded index. The literal #1235 D4 `--signer-workflow .../release.yml` fails because the certificate SAN names `release-build.yml`. Naming `release-build.yml` satisfies only the basic compatible prefix check; its prefix semantics do not meet this contract's exact-identity policy; `--cert-identity` is used instead. These flags are mutually exclusive. + +```sh +RELEASE_TAG=v1.2.0 +REVIEWED_COMMIT='' +SIGNER="https://github.com/IABTechLab/trusted-server/.github/workflows/release-build.yml@refs/tags/$RELEASE_TAG" + +gh release download "$RELEASE_TAG" \ + -R github.com/IABTechLab/trusted-server \ + --pattern 'trusted-server-fastly.wasm*' \ + --pattern 'release-index*' --pattern tsjs-inventory.json --pattern SHA256SUMS + +gh release verify "$RELEASE_TAG" -R github.com/IABTechLab/trusted-server + +verify_build() { + gh attestation verify "$1" \ + -R IABTechLab/trusted-server --hostname github.com \ + --cert-identity "$SIGNER" \ + --cert-oidc-issuer https://token.actions.githubusercontent.com \ + --source-ref "refs/tags/$RELEASE_TAG" --source-digest "$REVIEWED_COMMIT" \ + --signer-digest "$REVIEWED_COMMIT" --deny-self-hosted-runners \ + --predicate-type https://slsa.dev/provenance/v1 --format json +} + +verify_build release-index.json +verify_build trusted-server-fastly.wasm + +gh attestation verify trusted-server-fastly.wasm \ + -R IABTechLab/trusted-server --hostname github.com \ + --cert-identity "$SIGNER" \ + --cert-oidc-issuer https://token.actions.githubusercontent.com \ + --source-ref "refs/tags/$RELEASE_TAG" --source-digest "$REVIEWED_COMMIT" \ + --signer-digest "$REVIEWED_COMMIT" --deny-self-hosted-runners \ + --predicate-type https://spdx.dev/Document/v2.3 --format json + +gh release verify-asset "$RELEASE_TAG" release-index.json \ + -R github.com/IABTechLab/trusted-server +gh release verify-asset "$RELEASE_TAG" trusted-server-fastly.wasm \ + -R github.com/IABTechLab/trusted-server +gh release verify-asset "$RELEASE_TAG" trusted-server-fastly.wasm.spdx.json \ + -R github.com/IABTechLab/trusted-server +``` + +The full R-EVIDENCE checks, JSON Schema validation and membership checks for remaining companions/JS inventory are still required; these commands alone do not construct a verified capability. The implementation MUST provide `ts release verify --asset --expected-source-commit ` as a read-only complete online policy check, using the same verifier library as deployment. It MUST report policy version/digest, source, asset digest and required profile, and distinguish optional comparison outcomes. It does not export an offline publication-authorization record. + +## 10. Offline evidence limits + +### R-OFFLINE + +**Phase:** 2+. + +Offline build verification MUST use securely transported roots, an independently approved source commit and artifact SHA-256, and published provenance/SBOM bundles. Roots or digest pins supplied alongside arbitrary evidence MUST NOT become trust inputs. Preparation MAY obtain a root snapshot with `gh attestation trusted-root --hostname github.com`; artifacts/bundles are downloaded online. A file-mode bundle verification does not need GitHub authentication. Verification MUST work with network access denied and apply the same fixed certificate/source/predicate bounds. + +```sh +RELEASE_TAG=v1.2.0 +REVIEWED_COMMIT='' +SIGNER="https://github.com/IABTechLab/trusted-server/.github/workflows/release-build.yml@refs/tags/$RELEASE_TAG" + +gh attestation verify trusted-server-fastly.wasm \ + -R IABTechLab/trusted-server --hostname github.com \ + --bundle trusted-server-fastly.wasm.provenance.sigstore.json \ + --custom-trusted-root trusted_root.jsonl \ + --cert-identity "$SIGNER" \ + --cert-oidc-issuer https://token.actions.githubusercontent.com \ + --source-ref "refs/tags/$RELEASE_TAG" --source-digest "$REVIEWED_COMMIT" \ + --signer-digest "$REVIEWED_COMMIT" --deny-self-hosted-runners \ + --predicate-type https://slsa.dev/provenance/v1 --format json +``` + +Apply equivalent checks to index and SBOM bundles, use SPDX predicate type for the latter, compare the independent executable pin and all authenticated companion digests, and compare the SPDX document. Offline success establishes build evidence and approved byte identity only. It does not re-establish GitHub release membership, current release state or fresh revocation. `gh release verify-asset` requires the API; `gh attestation verify` is not an offline verifier for GitHub's release-attestation certificate profile. Automated offline official deployment and a `verified-publication.json` protocol are deferred; `ts deploy --release --offline` MUST be rejected in this cohort. + +## 11. Fastly verified-byte consumption and recovery + +### R-DEPLOY + +**Phase:** 2+. + +Canonical identity is SHA-256 of final `trusted-server-fastly.wasm`, not a publisher-specific tarball. The new `ts deploy --adapter fastly --release ` path MUST verify before side effects, retain the verified bytes in private owned storage and bypass source-build and manifest deploy overrides. Artifact flags without `--release`, unsupported variants/adapters, conflicting package/source arguments, arbitrary passthrough hooks and offline deployment MUST fail. A local `--artifact ` MAY be used only when its retained bytes equal the authenticated official asset and all online checks succeed. Matching source/version strings alone are insufficient. + +A delayed EdgeZero API MUST NOT block raw release publication or documented manual verified-byte production deployment. Integrated production/staging is separate completion item AC-8 below, not a prerequisite to build/sign/publish. Source deployment remains available for development/custom distributions, with an explicit diagnostic that canonical identity has not been established. + +### R-PACK + +**Phase:** 2+. + +Packing MUST use a validated deployment-only manifest derived from the selected publisher-owned manifest and retained verified wasm in a fresh private directory. The sanitized package name MUST be nonempty and MUST NOT equal `package`; that reserved name collides with the generic archive. With pinned Fastly CLI 15.1.0 and an accepted name, `compute pack --wasm-binary` writes `pkg/.tar.gz` and also an unrelated `pkg/package.tar.gz`; it can sweep pre-existing package contents. + +The chosen archive MUST have exactly `/`, `/bin/`, `/bin/main.wasm` and `/fastly.toml`. Inspection MUST reject links, traversal, duplicates, extra entries or mismatched manifest/wasm bytes, with bounded extraction. The archive MUST be selected by validated name and contents, not a guessed generic filename. Recheck retained archive/member digests immediately before submission. + +The deployment-only manifest MUST exclude `[local_server]`, inline fixture settings/secrets, active app-config payloads and build hooks; preserve only reviewed provider envelope/resource-reference fields. The original operator manifest is not silently edited. Fastly pack and `compute validate` can succeed on non-wasm bytes and neither strips local fixtures, so they MUST NOT be used as proof of wasm validity or privacy. + +Producer validation MUST parse the actual final core module with a pinned validator and execute an application request under pinned Viceroy; `/health` alone bypasses app settings. Live provider/runtime validation remains separate. Tar metadata is nondeterministic: package SHA-256 identifies this operation's envelope, not the canonical release. + +### R-CONTEXT + +**Phase:** 2+. + +Before any provider mutation, the integrated route MUST resolve an existing service, selected base version, approved logical config ID, physical resource mapping and runtime selectors, and compare declarations with observed links/selectors on that exact version. Existing-service `[setup]` declarations alone do not provision or update bindings. New-service provisioning MUST be a separate supported operation, otherwise fail read-only. Store/config/secrets push remains separately authorized and MUST NOT be hidden in verification. + +After clone/upload, the route MUST validate actual bindings and selectors on the exact returned draft version before staging or activation. Production MUST preserve intended config mappings; staging MUST use a verified isolated config mapping and selectors rather than silently retain inherited production links. Missing logical IDs, absent links, contradictory observed selectors or incomplete isolation MUST fail before activation/stage. Staging isolation here covers app-config mappings/selectors, not automatic isolation of KV/Secret Stores or external side effects. The approved context MUST explicitly declare shared versus isolated resources, and secret validation MUST use references/presence/metadata, not pretend write-only secret plaintext can be read back. Participating operations MUST serialize at service/shared-resource scope; an environment-only lock does not cover shared production/staging resources or other operators. Mutable context MUST be revalidated at the applicable phase; this is not a transaction or a guarantee against concurrent provider changes. + +### R-EDGEZERO + +**Phase:** 2+. + +The pinned EdgeZero adapter interface is `execute(AdapterAction, &[String]) -> Result<(), String>`; context is string tokens and outcomes are logs. Implementing `deploy_prebuilt` requires a new additive request/result API and reviewed upstream work. Its initial scope MUST be Fastly only. The request MUST contain retained package/path/size/SHA-256, existing service ID, selected base version, production/staging mode and resolved manifest/store context. The result MUST contain service ID, exact version if known, last confirmed phase, whether mutation was attempted and known/unknown outcomes. + +EdgeZero MUST own concrete clone/upload/context-validation/stage/activation operations and payload integrity without implementing official release policy. It MUST NOT run `compute build`, `compute publish`, `compute update --autoclone --version=active` as a combined guessed-version path, or infer the uploaded version from whichever version is now active/highest. It MUST inspect the exact selected version and capture the version returned by its own clone/upload operation. `compute deploy` activates internally and cannot expose the required draft-binding validation boundary, so the integrated API cannot be implemented by merely wrapping its exit status. + +### R-RECOVERY + +**Phase:** 2+. + +Before first mutation, the integrated route MUST durably persist a restricted operation journal plus retained package/evidence and original resolved context. The journal MUST bind a unique operation ID, policy/source/artifact/package identities, intended service/base version/mode/mappings, previous active version when observed, last confirmed phase and mutation/uncertainty state. It MUST exclude credentials, resolved secrets and active configuration payloads. Immediately before every clone, upload, selector/link write, stage or activation request, the journal MUST atomically persist and flush a pending/attempted marker and explicit uncertainty. A crash between request transmission and response MUST NOT leave a false no-mutation claim. Confirm responses in atomic durable updates through `Prepared → VersionAllocated → Uploaded → ContextBound → Staged/Activated`; allocated version and uploaded package are distinct observations. An abrupt interruption MUST leave enough durable intent/state for inspection. + +After any attempted write, retained recovery inputs MUST survive temporary-directory cleanup, a failed final receipt write and process termination until explicit reconciliation or abandonment. Before the first write, failed preparation MAY clean up. The consumer MUST serialize operations for the same service and any shared resources across environments; no distributed lock is promised. EdgeZero owns read-only reconciliation of exact observed/allocated versions and uncertain writes. The operator/orchestrator owns the retry, explicit rollback or abandonment decision. Recovery MUST NOT rebuild from today's edited manifest, blindly retry activation/upload or guess a rollback target. If the first clone commits but its response is lost before the allocated version is known, reconciliation MAY remain unresolved: the endpoint exposes no documented caller operation ID/idempotency key. Candidate versions, timestamps or matching packages are not unique ownership proof. Recovery MUST preserve unknown and MUST NOT adopt, activate, delete or roll back a guessed candidate. The local operation ID is not a provider transaction ID. Local receipt output MAY summarize observations but is non-normative for #161 and proves neither publisher authority nor live execution. + +The proposed integrated interface is new work: + +```sh +ts deploy --adapter fastly --service-id '' \ + --release v1.2.0 --expected-source-commit '' \ + --receipt deployment-receipt.json + +ts deploy --adapter fastly --service-id '' --staging \ + --release v1.2.0 --expected-source-commit '' \ + --receipt staged-deployment-receipt.json +``` + +### Manual production route during integration + +### R-MANUAL + +**Phase:** 1 for the reduced acceptance subset in Phasing; remaining clauses are Phase 2+. + +Operator guidance MUST provide verification, private-directory packing, R-PACK inspection, observed existing-service binding checks and an explicit production submission command before the first release. A lower-level verified-byte transport can use: + +```sh +fastly compute pack --wasm-binary ./trusted-server-fastly.wasm +fastly compute deploy --package './pkg/.tar.gz' \ + --service-id '' --version '' --no-default-domain +``` + +Run in a private directory with exactly the validated deployment-only `fastly.toml`; inspect the package before deployment. At pinned 15.1.0, `compute deploy` clones an active/locked version and activates production, has no staging mode, and can successfully skip deployment when the selected version already has the same package hash. No upload/new version/activation can be inferred from exit zero. It reads a working-directory manifest before the packaged one and otherwise may create a random default domain; explicit service selection, the inspected integer `--version` and `--no-default-domain` are required here. The operator MUST serialize manual changes to that service and its shared resources across environments and recheck the selected version's bindings; omitting `--version` resolves active at execution time and falls back to latest if no active version exists, which can select a different version from the inspected one. `[setup]` stores are applied only for new services. Built-in availability checks do not establish application correctness. + +This manual route preserves inspected wasm bytes but does not supply the integrated preactivation binding check, staging isolation or durable structured outcome. Operators MUST document that limit, record the version actually reported, and independently inspect binding/context rather than claim the integrated contract passed. The proposed integrated route is the eventual default; this documented verified-byte production route satisfies #1235's initial artifact-deployment choice while upstream integration is pending. + +## 12. Follow-up ownership and stable adapter invariants + +### R-FOLLOWUP + +**Phase:** 2+. + +Additional adapter support MUST preserve configuration outside canonical bytes, external complete-byte SHA-256 identity, verification and action authorization before provider mutation/process start, and artifact-specific provenance/SBOM, reproducibility policy, runtime/binding, readiness and recovery proof. It MUST be selected in a reviewed cohort/policy before execution. One adapter's successful proof MUST NOT qualify another. These invariants do not make additional adapters part of #1235's acceptance or require a cumulative nine-artifact launch. + +The following are investigation inputs for separate specs, not approved transports/ABIs in this contract: + +| Follow-up | Questions and proof to resolve | +| ---------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Reproducible operator rebuild | Retained authenticated source/dependency/tool inputs, legally obtainable SDKs, separately bootstrapped replay and offline official-release policy. Consider a supported library verification of GitHub-signed release attestations rather than a consumer-authored publication record. Promote native comparison gates individually | +| Cloudflare (existing [KV/Worker-variable mismatch #1140](https://github.com/IABTechLab/trusted-server/issues/1140), deferred here) | Define final executable modules and inventory subjects (prefer raw module assets plus `modules.json` unless an archive is justified). The Worker reads `TRUSTED_SERVER_CONFIG` and caches settings, while current EdgeZero config push writes KV; these channels MUST be reconciled in the adapter spec with explicit config-envelope/secret references and positive/negative publisher-request fixtures. Define prepared version upload and explicit activation identities | +| Cloudflare build/transport | Verify/preseed worker-build 0.8.5 tools: wasm-bindgen matching locked 0.2.126, Binaryen 130 and esbuild 0.28.1 (distinct from the JS lockfile). Account for `WASM_BINDGEN_BIN`, `WASM_OPT_BIN`, `ESBUILD_BIN`, cache/download fallbacks, `CUSTOM_SHIM`, `RUN_TO_COMPLETION`, coredump switches, minification, optimization, panic-unwind and producers metadata. Current `build.sh` installs without `--locked` and loads operator env. For inheritance, investigate `bindings_inherit=strict`. Ordinary script/Upload Version inheritance is latest-uploaded-only; an explicit non-latest `version_id` requires the beta Workers API Versions endpoints and a separately reviewed API contract. Do not prescribe that beta field for the ordinary endpoint; latest uploaded can differ from deployed | +| Spin | `spin-sdk` 6.0.0 locks `wasip3` with WASI `0.3.0-rc-2026-03-15`. A `wasm32-wasip2` compiler target alone does not define a compatible ABI. Pin/test that exact world and runtime, migrate all owning targets/manifests/CI/commands together, and reassess RC support lifetime. Existing secret `default = ""` fields serialize into published locked-app data; define unresolved secret references. Default OCI composition and current-time annotations MUST not silently transform the canonical component; registry manifest identity is observed separately | +| Axum | Define direct verified local launch and a tested bridge from EdgeZero's local config envelope to runtime selectors/config variables and secret references. The binary uses `PORT` and loopback, not the source launcher's adapter port or `axum.toml`; a living error router is not readiness. Require an application request, child cleanup and signal handling. No generic remote deployment implied | +| #161 | Publisher authority, actual loaded snapshot, claims/freshness, remote execution limits and optional authenticated deployment observations. Fastly default is per-request; reusable Fastly/Axum/Cloudflare/Spin have distinct snapshot lifetimes | + +The Cloudflare configuration mismatch and inaccurate configuration comments predate this spec. Runtime repair is separate work; no issue is automatically opened by this document. + +## Feasibility evidence and implementation dependencies + +The [feasibility audit](2026-10-12-1235-release-attestation-feasibility.md) covers every requirement and identifies concrete implementation boundaries. Local JS/wasm/native builds, application execution and pinned gh primitives were exercised. Hosted production recipes, full real-artifact SBOM coverage, GNU/Intel qualification, actual project signing/publication and live Fastly submission remain unproven release gates. Approval of prose does not waive them. The audit records the known unavailable old-macOS matrix, final-CLI JS distinction, primitive verifier limits, manifest fixture leakage and irreducible first-clone uncertainty corrected here. + +## Test plan and requirement traceability + +These are implementation acceptance tests, not tests claimed to pass in this specification and implementation PR. Every normative requirement maps below to a test family and acceptance item. D1–D8 refer to the eight “Done when” bullets of #1235 in their original order. + +| Requirements | Test family and essential positive/negative cases | Acceptance | #1235 Done when | +| --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------- | ----------------------------- | +| R-COHORT, R-PLATFORM, R-VARIANT | T-1 exact four assets/profiles; no downgrade on required failure; CLI B mismatch reported; Fastly failure blocks; ELF symbol/link/runtime checks; macOS 15 ARM/Intel startup; old-OS claims remain unqualified; reject unsupported reuse | AC-1, AC-2 | D1, D3 | +| R-CONFIG, R-INPUTS, R-ENV | T-2 build with differing fictional publisher inputs gives identical permitted bytes; operator values absent; unchanged repository manifest; inventory covers embedded roots/generated assets/native inputs; prohibited flags/files/env fail | AC-2 | D2, D5 | +| R-JS | T-3 #1214 private prebuilt path; skip absent/empty/0/1; missing npm, failed install/test, stale/empty/extra module; inventory tampering; two JS builds differ; cross-artifact digests differ; missing esbuild with network denied | AC-3 | D5 | +| R-BUILD, R-REPEAT, R-NATIVE | T-4 separate run/job/ID/digest binding; wrong-run/artifact substitution; fresh outputs; varied declared ambient inputs; final wasm equality/no normalized comparison; native A/B outcomes and signature/path observations; enforced socket-denial controls | AC-3 | D3, D6 | +| R-SBOM | T-5 actual target and host proc-macro/build roles, dual-role package, dev exclusion, real JS contribution, Prebid type-only exclusion, plugin helper, AWS-LC/SQLite/ring native coverage and forbidden system selection, SPDX relationship/root/predicate equality and size limit | AC-4 | D3, D4 | +| R-FACTS | T-6 target/feature ownership; malformed/missing commit fails without git fallback; source header vs service version; two tags on same commit do not change bytes; no final checksum/config/run timestamp embedded | AC-5 | D2 | +| R-SOURCE, R-SIGN, R-PUBLISH | T-7 approved main/RC tag; unauthorized RC and annotated tag rejected; pending/cancelled/skipped/path-filtered/wrong-commit checks; timeout; untrusted caller; permission separation; matching/conflicting draft reruns; immutable/mutable/unknown publish state | AC-6 | D3, D7 | +| R-INDEX, R-EVIDENCE, R-POLICY | T-8 schema positive fixture and unknown enums/version/extra field/duplicate keys/names/traversal; tampered subjects/companions; policy cannot self-select; wrong caller/build type/material/source/builder/prefix lookalike, trailing control characters, actual size limits; no extra unapproved subjects | AC-4, AC-7 | D3, D4 | +| R-BOOTSTRAP, R-BOUNDS, R-PUBLICATION | T-9 verified CLI first-use fixture; absent token; isolated gh config/host; unsupported tool; hangs/cancel/oversize/nesting/partial JSON; unpublished or mutable artifact; source approval mismatch; all fail before provider mutation | AC-7 | D4, D6 | +| R-OFFLINE | T-10 pinned roots/source/digest with networking denied; artifact/predicate tampering; hostile transferred roots; offline build success cannot construct official-deploy capability; offline deployment rejected | AC-7 | D4 (specified offline limits) | +| R-DEPLOY/R-PACK integrated portion, R-CONTEXT, R-EDGEZERO, R-RECOVERY | T-11 retained-file substitution/package/member mutation; all four Fastly pack members plus extra manifest-only archive; observed links disagree; exact draft mismatch prevents activation; staging isolation; no source hooks; exact/unknown outcomes; interrupted selector writes, first-clone/send-before-response crashes including indistinguishable candidate allocations, secret/reference privacy, shared-resource isolation boundaries and final receipt failure; read-only reconciliation | AC-8 | D6 (integrated enhancement) | +| R-DEPLOY/R-PACK manual portion, R-MANUAL | T-12 provider-free pack/member equality; documented native production limits including skipped upload and manifest precedence; authorized disposable-service production test with submitted wasm equality | AC-9 | D6 | +| R-FOLLOWUP | T-13 docs/policy reject unsupported adapters/actions; #1235 criteria contain no adapter migration, operator replay or offline deploy gate; January supersession link | AC-10 | D1 | +| Operator/CLI/changelog guidance | T-12 manual-route instructions and T-13 unsupported-action diagnostics; document source-header identity and bootstrap prerequisites | AC-9 | D8 | + +Implementation changes MUST run appropriate target-matched tests/lints after each behavioral change and the complete AGENTS.md CI gates before PR handoff. The deferred AC-8 milestone additionally requires upstream EdgeZero API/lifecycle tests; those are not a prerequisite for the AC-9 manual route. No real customer/domain/secret fixture data is permitted. PR #1262 contains implementation as well as documents/schema/plans. Document/schema formatting, committed positive/negative fixtures, reference/traceability checks and automated design review accompany implementation tests; these do not establish pending hosted qualification or maintainer approval. + +## Rollout + +1. Review this contract and its plans, then implement on the same `spec/1235-release-attestation` branch and PR #1262. Approve the contract/code together before merge; this explicitly replaces the separate-spec-PR delivery procedure for this work, without waiving qualification or release authorization. Extend merged #1214/#1213 and reconcile permission-config separation. +2. Prototype Fastly A/B equality, shared JS output, SBOM native/host/JS fixtures and CLI compatibility baselines with throwaway recipes. Record feasibility and review any cohort change before production work; this step does not promise replay of nonexistent retained inputs. +3. Implement reviewed release recipes/tools/cohort, JS inventory, composition generator, facts, validation, measurements, schema/policy, signing and immutable publication. Remove obsolete claims that configuration is baked into wasm from CI workflows, the integration-test setup action, adapter comments/tests and integration helpers, not just `test.yml`. +4. Prepare governance/settings/tag rules, trusted gh/CLI bootstrap and documentation. Exercise publication and verification in a disposable public repository using policy fixtures for that repository; no production-policy override is added. Document/validate the manual verified-byte Fastly production route. Publish the first eligible four-artifact release after all its selected requirements pass. +5. Implement the separate Fastly-only EdgeZero prebuilt request/result and recovery lifecycle, review/pin that upstream revision, then adopt integrated online production/staging in `ts`. Upstream delay does not block step 4, and MUST NOT cause the artifact path to rebuild. +6. Update `docs/guide/getting-started.md`, `docs/guide/edgezero.md`, `docs/guide/error-reference.md` and `docs/guide/cli.md`, annotate #1213's historical header goals, add `docs/guide/release-verification.md` and navigation, and update `CHANGELOG.md`. Document platform/tool/token prerequisites, policy identities, manual/integrated distinctions, offline limits and optional comparison results. Historical source builds MUST NOT be relabeled attested. +7. Follow-up specs independently qualify operator reconstruction, offline deployment, each other adapter and stronger builder isolation. #161 continues publisher/config/runtime work; none gates #1235 closure. + +## Acceptance criteria + +- [ ] **AC-1:** Spec, plans and implementation reviewed/approved together in PR #1262; implementation records the selected cohort/profiles before release execution. +- [ ] **AC-2:** Configuration independence, complete input inventory, selected variant and qualified CLI compatibility hold for every selected Build A. +- [ ] **AC-3:** The two fresh JS builds produce byte-identical bundles. Fastly Build A and Build B produce byte-identical `trusted-server-fastly.wasm`. Each native CLI Build B records `matched`, `mismatched`, `failed` or `unavailable` before its deadline; its result MUST NOT change whether that CLI's mandatory A, SBOM or provenance checks pass. +- [ ] **AC-4:** Per-artifact provenance/SPDX/bundles/index/checksums and composition coverage pass their exact evidence and schema checks. +- [ ] **AC-5:** Source facts reconcile #1213, derive target/variant correctly and keep release tags, service versions and final digests distinct. +- [ ] **AC-6:** Approved main (or future explicitly reviewed exact RC) lightweight `v*` tags run same-commit gates and publish the exact immutable verified distribution with least-privilege authority. +- [ ] **AC-7:** Trusted bootstrap, complete online verification, bounded subprocesses and offline build-evidence checks pass; unsupported offline official deployment fails before writes. +- [ ] **AC-8 (integration milestone):** New reviewed/pinned EdgeZero Fastly API supports byte-preserving online production/staging, observed context validation and durable recovery. This item tracks the subsequent integrated enhancement and MUST NOT keep #1235 open when AC-9 satisfies its deployment choice. +- [ ] **AC-9:** Operator verification/CLI/changelog guidance and the manual verified-byte production route are implemented and validated; ordinary source builds cannot be mistaken for canonical bytes. +- [ ] **AC-10:** January binary-attestation guidance is superseded; #161 and deferred adapter/reconstruction scopes are explicit and unsupported actions fail. + +#1235 can close when AC-1 through AC-7, AC-9 and AC-10 satisfy its D1–D8 criteria. AC-8 remains a separately tracked integration milestone if upstream work is delayed. There is no requirement to implement Cloudflare, Spin, Axum, native CLI reproducibility, retained archives or operator replay to close #1235. + +## Appendix A. Review disposition and rationale + +This table accounts for all 32 inline comments in [review 5484754857](https://github.com/IABTechLab/trusted-server/pull/1262#pullrequestreview-5484754857). It records proposed spec resolution; GitHub conversations are not automatically marked resolved and maintainer acceptance is still required. + +| Review concern | Resolution / requirement | +| ----------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| Reusable sandbox (4243031371) | R-VARIANT retains current production default and plans qualified attested reuse; R-FACTS corrects snapshot lifetime; removes nonexistent test feature | +| All-artifact reproducibility gate (4243031376) | R-COHORT/R-REPEAT select Fastly equality, native observation; reconstruction/offline deploy deferred | +| Repository-root scaffold (4243031380) | R-INPUTS names the external-to-CLI-crate repository-root input | +| Ambient variation and job output binding (4243031389) | R-REPEAT varies declared-insensitive inputs and binds artifact IDs/digests to job outputs | +| Distinct assurance dimensions (4243031391) | Profiles chosen before execution; claim/threat tables retain independent limits | +| CLI host baselines (4243031395) | R-PLATFORM keeps glibc 2.34 as a qualification target not yet demonstrated; macOS loader floor 13 with initial hosted-15 qualification, matching-host recipes first; cross-build requires full qualification and actual Intel execution | +| Index/schema and informational records (4243031397) | R-INDEX links closed JSON Schema and field contracts; recipe/rebuild consumer semantics informational; receipts private observations | +| #1214 fit (4243031404) | R-JS adds release delta to private prebuilt interface, no competing strict mode | +| One identical release-wide tsjs set (4243031409) | R-JS twice-builds one shared set, checks prepared inputs and separately checks modules actually retained after linking | +| Compiler wrapper/Syft cost (4243031418) | R-SBOM uses Cargo/lock/Rollup evidence and a repository-owned SPDX generator | +| Native and esbuild coverage (4243031423) | R-SBOM declares/validates actual native selection; R-JS tests denied fallback download | +| Release branches and in-progress CI (4243031426) | R-SOURCE approves exact main/RC policy and runs bounded same-commit release validation | +| Local signer/source revision (4243031429) | R-POLICY pins equal signer/source revisions and defines future identity migration | +| Attestation permission (4243031433) | R-SIGN removes `artifact-metadata: write` for file subjects | +| Settings token (4243031442) | R-PUBLISH uses governance plus actual immutable release check/recovery; no settings-read secret | +| gh prerequisites and bootstrap (4243031449) | R-BOOTSTRAP specifies token/env/path/trust chain; exact qualified gh version | +| Normative evidence graph (4243031456) | R-EVIDENCE/R-POLICY fix all digest/source/builder/caller/action constraints | +| Annotated tag commit confusion (4243031459) | R-SOURCE requires lightweight tags and explicit commit-object validation | +| Prefix signer match (4243031464) | R-POLICY/commands use exact SAN `--cert-identity` | +| Resource bounds (4243031467) | R-BOUNDS supplies concrete limits/deadlines, failure and child cleanup | +| Offline observation protocol cost (4243031470) | R-OFFLINE retains bundle/pin checks; offline official deployment/record deferred | +| Actual EdgeZero surface (4243031476) | R-EDGEZERO sizes new Fastly-only API and bypasses existing build/log/version fallbacks | +| Observed binding validation (4243031479) | R-CONTEXT checks existing base version and exact resulting draft before stage/activation | +| Pack archive facts (4243031481) | R-PACK specifies four prefixed entries, fresh directory and separate manifest-only archive | +| Durable recovery ownership (4243031485) | R-RECOVERY persists before mutation, survives failures/crashes and assigns reconciliation/retry ownership | +| Native deploy limits (4243031488) | R-MANUAL documents production activation, skip, domain creation and manifest precedence | +| Derived facts / #1213 (4243031494) | R-FACTS uses established name, source-only override, Cargo/adapter derivation, external release tag | +| Cloudflare config mismatch (4243031498) | §12 preserves concrete binding-channel investigation and fixture prerequisites for separate adapter spec | +| Spin ABI and OCI facts (4243031502) | §12 records exact WASI 0.3 RC dependency, secret defaults and composition/annotation caveats; no premature migration gate | +| Rollout dependencies (4243031506) | Feasibility precedes recipes; publication/manual route precedes independent upstream integration | +| Stale config assertions beyond CI (4243031509) | Rollout step 3 explicitly covers workflows/action/adapter comments/tests/helpers | +| Phase 2 closure scope (4243031515) | R-FOLLOWUP/acceptance remove cumulative adapter gates from #1235 | + +Body-level findings are addressed by the threat model, BCP 14 requirements, requirement→test→acceptance→issue mapping, named producer-policy paths and the January supersession banner. Deferred Cloudflare notes are retained in §12 rather than lost during scope reduction. + +Verified-byte deployment is selected first because it directly preserves an attested subject without requiring an operator SDK/compiler. Fastly A/B adds affordable measured repeatability; native CLI reconstruction has separate linker/SDK/availability costs. Selected required profiles remain blocking, so this is a reviewed assurance split, not a fail-open release. A custom whole-lockfile SBOM or bare wasm scan misses target/host/JS/native selection; an extra compiler wrapper does not solve native library provenance. A complete-package attestation would vary with publisher metadata, so raw wasm identity and observed package identity stay separate. A future independent builder strengthens a different threat boundary than matching project-controlled builds. + +## Appendix B. Supersession and primary references + +This draft supersedes the binary-attestation direction in [the January proposal](2026-01-15-attestation-design.md); after approval it is the implementation contract for those sections. January runtime/configuration examples remain historical reference for #161, not approved claims, embedded active-config guidance or self-reported executable proof. + +- [Original issue](https://github.com/IABTechLab/trusted-server/issues/1235), [review](https://github.com/IABTechLab/trusted-server/pull/1262#pullrequestreview-5484754857), [JS PR #1214](https://github.com/IABTechLab/trusted-server/pull/1214), [version PR #1213](https://github.com/IABTechLab/trusted-server/pull/1213). +- GitHub [artifact attestations](https://docs.github.com/en/actions/concepts/security/artifact-attestations), [immutable releases](https://docs.github.com/en/code-security/concepts/supply-chain-security/immutable-releases), [release verification](https://docs.github.com/en/code-security/how-tos/secure-your-supply-chain/secure-your-dependencies/verify-release-integrity), and [reusable workflow limits](https://docs.github.com/en/actions/how-tos/secure-your-work/use-artifact-attestations/increase-security-rating). +- gh 2.97.0 [exact/prefix identity policy](https://github.com/cli/cli/blob/55dbb4dc6b7edb10b48e3d7fc5bccd32318d1b55/pkg/cmd/attestation/verify/policy.go) and [tag-target lookup](https://github.com/cli/cli/blob/55dbb4dc6b7edb10b48e3d7fc5bccd32318d1b55/pkg/cmd/release/shared/fetch.go); [verifier manual](https://cli.github.com/manual/gh_attestation_verify), [bundle download](https://cli.github.com/manual/gh_attestation_download), [trusted roots](https://cli.github.com/manual/gh_attestation_trusted-root). +- Pinned [attest 4.2.2 dependency lock](https://github.com/actions/attest/blob/1e69f48acb82d1966a394da916b4c1698aa569d6/package-lock.json) and matching [toolkit provenance semantics](https://github.com/actions/toolkit/blob/7987771a2b727fe14b36351f6c877fa3a56b9c36/packages/attest/src/provenance.ts). +- Pinned [Sigstore certificate summary fields](https://github.com/sigstore/sigstore-go/blob/v1.2.2/pkg/fulcio/certificate/summarize.go). +- [SLSA build requirements](https://slsa.dev/spec/v1.2/build-requirements), [SPDX relationships](https://spdx.github.io/spdx-spec/v2.3.1/relationships-between-SPDX-elements/), [BCP 14 keyword clarification](https://www.rfc-editor.org/rfc/rfc8174.html). +- Cargo [build messages](https://doc.rust-lang.org/cargo/reference/external-tools.html), [metadata](https://doc.rust-lang.org/cargo/commands/cargo-metadata.html), [source remapping](https://doc.rust-lang.org/rustc/remap-source-paths.html) and [Apple targets](https://doc.rust-lang.org/rustc/platform-support/apple-darwin.html); [Rollup output hooks](https://rollupjs.org/plugin-development/), [npm ci](https://docs.npmjs.com/cli/v11/commands/npm-ci/). +- Fastly 15.1.0 [packing](https://github.com/fastly/cli/blob/v15.1.0/pkg/commands/compute/pack.go), [deployment](https://github.com/fastly/cli/blob/v15.1.0/pkg/commands/compute/deploy.go), [build annotation](https://github.com/fastly/cli/blob/v15.1.0/pkg/commands/compute/build.go). +- Pinned EdgeZero [adapter surface](https://github.com/stackpop/edgezero/blob/683202c66948146ec360f490126f1d60f920288b/crates/edgezero-adapter/src/registry.rs), [Fastly lifecycle](https://github.com/stackpop/edgezero/blob/683202c66948146ec360f490126f1d60f920288b/crates/edgezero-adapter-fastly/src/cli.rs), [CLI dispatch](https://github.com/stackpop/edgezero/blob/683202c66948146ec360f490126f1d60f920288b/crates/edgezero-cli/src/lib.rs). +- [Cloudflare binding inheritance](https://developers.cloudflare.com/workers/configuration/multipart-upload-metadata/), [worker-build pipeline](https://github.com/cloudflare/workers-rs/blob/v0.8.5/worker-build/src/main.rs), [tool versions](https://github.com/cloudflare/workers-rs/blob/v0.8.5/worker-build/src/versions.rs); existing [Axum bridge guidance](../../guide/axum-dev.md). + +Tool availability and API semantics MUST be revalidated at implementation pin selection. Incompatible changes require reviewed policy/spec updates rather than silent fallback. diff --git a/docs/superpowers/specs/2026-10-12-1235-release-attestation-feasibility.md b/docs/superpowers/specs/2026-10-12-1235-release-attestation-feasibility.md new file mode 100644 index 0000000000..df928fa01a --- /dev/null +++ b/docs/superpowers/specs/2026-10-12-1235-release-attestation-feasibility.md @@ -0,0 +1,121 @@ +# Release attestation feasibility and implementation dependencies + +**Date:** 2026-10-12. + +**Contract:** [Release artifact attestation and verified consumption](2026-10-09-1235-release-attestation-design.md), PR #1262, issue #1235. + +**Status:** Feasibility evidence for a proposed contract. The core build, verification and packaging mechanisms work in local prototypes. The complete four-target release is **not qualified**: hosted production recipes, full SBOM coverage, signing/publication rehearsal, Linux compatibility, Intel execution and live Fastly submission remain required evidence. + +Document/schema checks and independent design review establish consistency. They do not establish that a new workflow can publish, that every native target runs, or that provider operations preserve an intended runtime context. + +**2026-10-14 implementation correction:** Audit prototype scripts and their Python paths/counts are historical measurements, not active implementation instructions or Rust migration proof. Introduced Python release tooling has been superseded by the independent host-only Rust producer in the [migration addendum](2026-10-14-release-tooling-rust-migration.md). Existing Node composition remains. The original native acquisition/SDK, actual hosted-platform, full SPDX, same-run transfer, signing/publication and live-provider gaps remain mandatory; local macOS 26.6.2 does not establish macOS 15 execution. + +## Evidence scope + +The historical audit baseline was `7f6056bf83eb96aa4167efca64d43280299fba73`. Read-only PR checks identified #1214 head `e150780b4a922be0cd0dfbbe1167c72aa076d3e8` and #1213 head `959e503b9e35b0aecd42439edc85ad744bbb92f2`. Both were open during that audit; both merged on 2026-10-12 and are ancestors of the current implementation planning baseline `44c5b5620bfb1f82e5b959ab5ba8ec69c7091e52`. Open-PR overlays are historical probe conditions, not current prerequisites. Isolated JS/interface probes overlaid #1214 files onto the baseline; its old `build-all.mjs` diff requires reconciliation with the current refactor. These synthetic snapshots are not an approved release commit. + +Local tools were Rust 1.95.0, Node 24.12.0/npm 11.6.2, macOS 26.6.2 ARM64 with SDK 15.5, Fastly CLI 15.1.0, Viceroy 0.17.0 and downloaded GitHub CLI 2.97.0. The Fastly CLI separately reports a bundled Viceroy 0.21.1; that was not substituted for the pinned runtime probe. Compilation outputs were fresh; Rust source caches/compiler were shared. Prototype epoch input was fixed rather than produced by the final commit-derived recipe. Source-fact #1213 changes and CLI checkout-fallback removal were not applied to the full artifact builds. + +Build outputs and application fixtures stayed in temporary directories. A gh probe with omitted `HOME`/`XDG_STATE_HOME` briefly created anonymous device state relative to the repository; the behavior was reproduced, the exact generated file and empty directories were removed, and R-BOOTSTRAP now requires private home/state handling. Provider operations, repositories, workflows, releases and attestations were not created or modified. Online probes read existing public GitHub release evidence. Runtime requests used localhost fixtures with fictional configuration and secrets. + +## Historical local measurements + +| Probe | Measured result | What it proves | What it does not prove | +| -------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | +| Fresh locked JS A/B | All 13 final bundles byte-identical; each test run passed 50 files and 1,208 tests | The historical bundler could produce stable shared input under varied local paths/locale/timezone | Separate hosted workers, all package-origin joins or independent toolchain trust | +| #1214 prebuilt interface | Exact copied module set/hashes; succeeded with npm absent; 7 bundle-set tests passed | Rust can consume one prepared JS set without rerunning npm | Trusted expected-inventory input and a release presence-reject guard; historical upstream accepted skip alongside prebuilt | +| Missing esbuild binary | Network-denied bundle step exited 1 without fetching replacement | Selected Darwin bundler does not silently succeed without its prepared tool | Every installation-time/platform download fallback | +| Network confinement | Unconfined localhost socket succeeded; confined socket failed with `EPERM`; full Rust/JS builds ran inside that profile | Local macOS process-level network denial is available | Qualification of each actual hosted recipe or hostile-runner resistance | +| Full Fastly release A/B | Both complete files were 69,647,265 bytes and matched SHA-256 `2a740c43e93c47b31237861118e1bd75c5bd2940eb3daf1bb382d1104547c658`; no normalization | Fresh Cargo outputs can repeat across source/output paths, umask, locale/timezone and 4-versus-2 build jobs; all 13 JS strings remained | Final Linux hosted recipe, fresh independent dependency materialization, final source facts, publication or operator reconstruction | +| Actual Fastly runtime | Viceroy 0.17.0 `/health` returned 200; publisher `/` separately returned 200, origin fixture body and injected script | This complete prototype wasm parses/links and executes meaningful app behavior with the test envelope | Fastly upload/activation, PoP behavior, production credentials/config or remote execution proof | +| Full ARM64 CLI release | Network-denied `--frozen` release build succeeded; final file 37,977,296 bytes; outside-checkout help/version/config init succeeded | Current whole CLI graph builds/starts locally; scaffold bytes equal repository template | macOS 13/14/15, Intel or GNU startup; final release build-facts/version policy | +| CLI native inputs | Build messages/static archives identified `aws-lc-sys` 0.42.0 (vendored AWS-LC 5.1.0), ring 0.17.14 and `libsqlite3-sys` 0.30.1 (bundled SQLite 3.46.0); four native archives; loader floor 13.0, SDK 15.5 | Whole-CLI native evidence is observable and the explicit source-selection recipe works locally | Complete source/SDK/sysroot/license provenance, final native reachability or cross-platform determinism | +| Final JS byte membership | All 13 complete bundles present in Fastly, none present in the final CLI, despite JS crate compilation | OUT_DIR/compiled membership is not final embedded payload; per-artifact classification is necessary | Exact final function reachability; absence of every possible transformed fragment | +| CLI path literal | Compiled temporary checkout fallback survived Rust path remapping | Literal fallback removal needs code; remapping is insufficient | A qualified official CLI resource policy | +| Cargo dual-context fixture | Compiler JSON separated host/target units/features; metadata merged their feature sets | Effective unit features can be collected from messages; package metadata alone is insufficient | Exact compiler dependency DAG from metadata/tree | +| Rollup hook | Contributions collected for all 13 outputs without changing bundle bytes; current contributors were project modules, not full external Prebid.js | Hook-based contribution evidence is available | Full third-party/license joins, generated helper and elimination fixtures | +| Limited native SPDX prototype | Actual native-linked fixture ran; 30 packages/23 Rust units; 21,933-byte document passed official SPDX 2.3 schema | Selected-unit/native evidence can be serialized as valid SPDX | Production generator coverage, full CLI/Rust/sysroot/JS semantics or signed SBOM equality | +| Exact gh offline verification | gh 2.97.0 verified a signed reusable-workflow fixture under denied networking and absent token; wrong SAN/source/predicate rejected | Exact certificate/source flags, offline roots/bundle primitive and flattened certificate JSON work | Future action/workflow OIDC, official offline release membership or deployment | +| gh state isolation | An omitted home/state path created relative anonymous device state despite isolated gh config; reproduced and cleaned | `GH_CONFIG_DIR` alone does not isolate gh state | Production environment hygiene without explicit home/state paths | +| Live public release verification | gh 2.97.0 verified its immutable release and archive; independent REST checks reported immutable release and commit tag type | Current online release primitives work | This project's governance, RC permissions or new publication workflow | +| Membership negative probe | Renamed downloaded archive still passed `gh release verify-asset` | Command checks digest, not expected local/published filename; wrapper needs signed-name checks | Full release policy from exit zero | +| Pinned toolkit execution | Integrity-checked toolkit 3.2.0 emitted expected current build type/caller/builder/source/run shape with mocked OIDC claims | Current action's predicate construction matches the proposed semantics | Actual certificate issuance for the future hosted local reusable workflow | +| Fastly packaging negatives | Named archive retained exact bytes; generic archive lacked wasm; extra package file was swept in; local settings/secrets were copied; ASCII non-wasm passed both pack and validate | Fresh directory, nonreserved package name, strict inspection, deployment-only manifest and separate wasm/runtime validation are necessary | Deployability or confidentiality from pack/validate exit zero | + +These are measurements from this audit, not claims about a published release. The CLI result is a conservative byte check plus source-use analysis; unused JS must not be added to the CLI merely to make a cross-artifact inventory assertion true. + +## Corrections made to the contract + +1. **Remove unavailable macOS qualification gates.** macOS 13 hosted runners retired on December 4, 2025; macOS 14 retires November 2, 2026. A hosted-only 13/14/15 startup requirement cannot be treated as a routine supported matrix. The initial required matrix is macOS 15 on ARM64 and Intel. Deployment target 13.0 is a loader floor; older runtime support remains unqualified. Intel cross-build is an optional replacement after whole-project qualification, not an assumed first implementation. [macOS 13 retirement](https://github.blog/changelog/2025-09-19-github-actions-macos-13-runner-image-is-closing-down/), [macOS 14 retirement](https://github.blog/changelog/2026-10-01-github-actions-macos-14-runner-image-retirement/). +2. **Distinguish prepared JS from retained code.** One shared input set is feasible, but the current optimized CLI discards its complete bundles. Only final retained modules get an embedded-payload claim. Compiler inputs remain build material. +3. **Make native selection enforceable.** Target-suffixed AWS-LC variables can override a generic selector; SQLite pkg-config can override the bundled feature. The recipe must control both channels and validate actual archives/link directives. Effective Rust features come from compiler messages, not metadata unions. +4. **Implement release policy beyond gh exit statuses.** Additional REST/verified-statement checks are needed for raw tag type, immutability, subject names and source authority. Signed release subjects have names/digests but no size; lengths bind through the signed index, actual bytes and REST metadata. The successful renamed-file negative probe prevents treating `verify-asset` as a name check. +5. **Attribute SBOM comparison correctly.** gh returns the verified predicate; it does not compare a downloaded SPDX file. The strict duplicate-rejecting document comparison belongs to the new Trusted Server wrapper and producer validation. +6. **Match actual artifact-transfer identity.** Actions artifact metadata contains run/source identity but no independent producer-job/attempt field. Job binding comes from the expected upload outputs and workflow topology. Archive transfer hash is distinct from executable SHA-256; mismatch warnings must fail the collector. +7. **Close schema gaps.** Newline-suffixed tag/names matched regex end anchors. Explicit newline rejection now accompanies patterns. Inventory and companion declarations have role-specific size limits, including 16 MiB SBOM. Parser/download limits and cross-document checks still belong to the implementation, not JSON Schema alone. +8. **Prevent deployment-envelope fixture leakage.** Packing carries the entire manifest, including local test values. The production envelope must use an allowlisted deployment-only manifest, without local-server sections, active settings or secrets; such a minimal manifest packed successfully. Sanitized empty/`package` names are rejected to avoid archive selection collisions. Pack/validate do not establish valid wasm; actual core-module parsing and app execution are separate gates. +9. **Bound recovery and isolation claims.** The clone API has no documented caller idempotency key. A lost first-clone response can leave multiple indistinguishable candidate versions; reconciliation can remain unknown and must not mutate a guess. Config staging isolation is not automatic KV/secret/external-effect isolation. Secret values are write-only, so readback validates references/metadata rather than plaintext. [Fastly version API](https://www.fastly.com/documentation/reference/api/services/version/), [resource links](https://www.fastly.com/documentation/reference/api/services/resource/), [secret-store semantics](https://www.fastly.com/documentation/reference/api/services/resources/secret-store-secret/). +10. **Qualify Cloudflare inheritance by endpoint.** Ordinary upload inherits latest and rejects a different version ID. Explicit non-latest version inheritance belongs to beta Workers API Versions endpoints and its own reviewed adapter contract. [Cloudflare error 10057](https://developers.cloudflare.com/workers/observability/errors/). + +## Requirement feasibility register + +“Demonstrated” means only the stated local probe. “Supported” means pinned source/docs expose the needed primitive. “New code” is an implementable contract absent from the existing application. “Unqualified” means a mandatory shipping proof is missing. A composite row can have more than one status because a working primitive does not supply the full policy. + +| Requirement | Status and evidence | Implementation or qualification still needed | +| ------------- | --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| R-COHORT | New code; fixed profiles are structurally expressible | Source-controlled cohort/collector; all four canonical Build A qualification gates | +| R-PLATFORM | ARM build/run and loader floor demonstrated; GNU/Intel/hosted 15 unqualified | Actual glibc 2.34 builder and ELF symbols/UBI/Ubuntu/Debian tests; hosted ARM/Intel execution; review any baseline change before enablement | +| R-VARIANT | Supported by current Cargo/runtime source; default artifact ran locally | Release variant allowlist/diagnostic; reuse remains separate qualification | +| R-CONFIG | Source-supported separation; runtime fixture loaded independently of wasm | Whole-release differing-publisher-input negative fixture; permission-spec compatibility | +| R-INPUTS | Important inputs and native/script env evidence observed | Complete tracked source/dependency/sysroot/tool/native identities; new generated input coverage | +| R-ENV | Denied socket and selected fresh environments demonstrated | Checked release env whitelist, private gh home/state, all skip values, Cargo/config layers, Vite env suppression and source mutation guards | +| R-JS | Fresh A/B, tests, prebuilt handoff, Rollup/missing-tool probes demonstrated | Current Rust recipe proof, trusted inventory binding, final retained classification, hosted transfer | +| R-BUILD | Full frozen network-denied local builds demonstrated | Digest-pinned hosted recipes; verified fresh material extraction; every selected confinement profile and native bootstrap | +| R-REPEAT | Complete Fastly equality demonstrated locally | Separate hosted Linux jobs, final facts/source, explicit upload outputs/transfer failure gate; optional-native terminal outcomes | +| R-NATIVE | Whole ARM CLI and static native evidence demonstrated | Effective tool/SDK pins, platform linker/signature policy; GNU/Intel measurements; no native equality shipping claim | +| R-SBOM | Limited selected-unit/native serialization prototype demonstrated | Full real artifact SPDX coverage, source/license/roles/sysroot/native/JS joins and negative fixtures; real selected-artifact validation beyond prototype serialization | +| R-FACTS | Cargo inputs and exact #1213 helper semantics supported/probed | Strict full-commit release validation, no git fallback, owning adapter variant, typed facts, CLI source version and literal fallback removal | +| R-SOURCE | Existing branch/CI shape inspected; APIs supported | Protected main/approved-RC policy, trusted same-commit workflow-call validation and pending-check deadline; annotated-tag rejection | +| R-SIGN | Action inputs/toolkit predicate source-supported and shape executed | Real hosted exact-version provenance+SPDX certificates, caller permission propagation, least-privilege jobs and collector | +| R-PUBLISH | Existing public immutable release consumer path demonstrated | Authorized disposable publication rehearsal, repository settings/rules/bypass audit and RC workflow-difference/minimal-token test | +| R-INDEX | Schema and negative structural checks demonstrated | Producer and strict consumer parser; all semantic source/name/policy/digest/size joins | +| R-EVIDENCE | Existing signed bundle/release graphs observed | Full new index/executable/companion/SPDX relationship checker and actual project fixtures | +| R-POLICY | Exact tool flags/certificate JSON demonstrated | Embedded current/historical policy identity, action/caller/run checks, closed profile/action permissions | +| R-BOOTSTRAP | Auth requirement and isolated no-auth offline primitive demonstrated | Trusted gh installation, external first-use CLI verification procedure, explicit token/env/path handling | +| R-BOUNDS | Actual fixture outputs measured; limits expressible | Concurrent bounded-pipe reader, timeouts/cancel/kill/reap, strict JSON/depth/duplicate checks and negative tests | +| R-PUBLICATION | Primitive signatures work; missing name/type/immutable checks confirmed | REST plus verified-statement wrapper and source approval; cannot use command success alone | +| R-OFFLINE | Exact build proof with denied network and rejection cases demonstrated | Document transport trust/pins; official offline deployment remains intentionally unsupported | +| R-DEPLOY | Canonical byte check/source-supported transport | Shared verifier/retained handles/action validation; integrated upstream API is separate milestone | +| R-PACK | Byte-preserving pack and adversarial local envelope cases demonstrated | Allowlisted deployment-only manifest, strict bounded archive inspector and final real-module/runtime validation | +| R-CONTEXT | Exact links/selectors endpoints source-supported | Preflight/resulting-draft readbacks and explicit sharing policy; live propagation/readiness proof; secret plaintext cannot be read | +| R-EDGEZERO | Clone → exact draft update → context → stage/activate APIs supported | New typed upstream API and structured outcomes, no source/manifest hooks; current adapter does not implement it | +| R-RECOVERY | Filesystem journal design implementable; provider uncertainty boundary identified | Flush/atomic records before each write, exact-version inspection where known, unresolved clone handling/crash fixtures; no exactly-once guarantee | +| R-MANUAL | Pack/flags verified; actual wasm ran only locally | Complete verified policy/inspection instructions and separately authorized disposable Fastly production submission | +| R-FOLLOWUP | Existing adapter facts inspected with endpoint correction | Independent Cloudflare/Spin/Axum/reconstruction specs; no additional issue-closure gate here | + +## Executable implementation order + +The remaining implementation MUST produce these concrete interfaces and evidence; this is a dependency analysis, not a claim that the named scripts already exist. + +1. **Reconcile inputs and facts.** Extend merged #1214 and #1213; add exact expected JS inventory validation, strict release fact/skip/env handling and disable the compiled CLI checkout fallback. Preserve ordinary development behavior separately. Exercise these guards before adding signing. +2. **Qualify actual platform recipes.** The approved-source native Rust producer, reached through thin `scripts/release/build.sh`/`bootstrap.sh` adapters, owns source/material preparation, effective settings, confinement and final artifacts; CLI/core/JS dependencies cannot enter the producer bootstrap. Linux can use a digest-pinned OCI environment with no compile network. macOS needs a tested confinement/SDK/native profile on the selected hosted machines. Material preparation and network-denied compilation are separate. Establish GNU 2.34 and macOS 15 ARM/Intel startup before approving the fixed first cohort; a failed baseline requires a reviewed change, not an after-failure downgrade. +3. **Generate actual evidence and SPDX.** Collector inputs are Cargo JSON, selected metadata/tree, checked material identities, Rollup inventory, native output/link declarations, sysroot/tools and final files. Produce prepared and retained-module lists separately. Validate real Fastly and each CLI SBOM, not only a toy fixture. Then qualify the Rust schema evaluator with explicit offline Draft 2020-12 and Draft 7 semantics, retain the authoritative schema raw-byte identity, and prove negative/actual coverage tests. +4. **Implement same-commit validation and transfers.** Refactor current CI into reviewed callable validation or equivalent explicit release jobs. Run required validations at the tag SHA, shared JS A/B, Fastly A/B and required native Build A with bounded optional comparisons. Upload selected raw outputs; pass artifact ID/archive hash through explicit job outputs. Fetch exact current-run ZIP bytes, fail transfer mismatch and inspect/extract only declared bounded members. Do not restore compiled state across the supposedly independent builds. +5. **Sign and publish through a tested boundary.** A separate signing/collector job authenticates source/input/output/evidence relationships before invoking the pinned action twice per artifact and signing the index. An existing lightweight tag avoids relying on automatic tag creation. Caller job permissions must allow the called signing jobs while build jobs explicitly remain read-only. The publication job handles draft/resume/upload/redownload/publish/reconcile with contents-write. Rehearse main and RC workflow divergence, partial drafts, reruns, optional failures and mutable/unknown publication before production enablement. [Release API permission behavior](https://docs.github.com/en/rest/releases/releases#create-a-release). +6. **Implement the online consumer.** A shared library performs strict schema/policy joins, invokes bounded pinned gh on retained bundles, checks all certificate/provenance/SPDX fields and verifies release API/statement semantics. Its private verified result retains selected bytes. `ts release verify` is read-only; manual packing uses that result plus a deployment-only envelope and archive inspector. Exact identity flags alone do not implement the evidence graph. +7. **Deliver the manual verified-byte production route.** Authorize a disposable provider test separately and retain the submitted-member/version observations. This route does not certify integrated staging, preactivation validation or remote execution. It can precede the upstream API. +8. **Implement the separate Fastly upstream milestone.** EdgeZero `deploy_prebuilt` takes retained payload identity, exact base/service/mode and resolved context. Use explicit clone, non-autoclone update of the returned draft, exact draft link/selector validation, stage/activate. Durable journal/pending markers and read-only reconciliation belong here. Lost first-clone identity remains unknown if the provider cannot uniquely establish it; do not invent an exactly-once contract. + +Future stronger builder isolation, independently reconstructed executables, offline official membership and additional adapters use separate proof boundaries. They are not shortcuts to close missing evidence in this initial pipeline. + +## Remaining qualification boundaries + +The local machine had no usable Docker daemon, old-macOS or Intel execution host. GNU compatibility was not inferred from Cargo metadata or an ARM native fixture. Actual hosted action/OIDC, artifact transfer, repository governance and RC release-token behavior require an authorized disposable GitHub rehearsal. Live package/bindings/stage/activation require an authorized disposable Fastly service. No such resources were mutated during this audit. + +These gaps do not establish impossibility of the narrowed architecture; their APIs and locally measured primitives support implementation. They do prevent claiming that all required release behavior is already feasible as a tested composition. Release enablement remains blocked until those selected qualifications and the new code pass. The known unavailable-host gate and overstated existing-tool/final-JS/recovery claims have been removed or corrected rather than passed to an implementation plan as assumptions. + +## Primary implementation references + +Historical temporary probe scripts/logs are not retained or reviewer-reproducible current proof. The measurements above preserve their historical scope. Current committed Rust document/schema tests supply local utility evidence; actual selected native boundaries, complete recipes and hosted release/provider proofs remain pending. + +Pinned implementation references: [gh 2.97.0](https://github.com/cli/cli/tree/55dbb4dc6b7edb10b48e3d7fc5bccd32318d1b55), [attest 4.2.2](https://github.com/actions/attest/tree/1e69f48acb82d1966a394da916b4c1698aa569d6), [toolkit provenance](https://github.com/actions/toolkit/blob/7987771a2b727fe14b36351f6c877fa3a56b9c36/packages/attest/src/provenance.ts), [Fastly 15.1.0 pack](https://github.com/fastly/cli/blob/v15.1.0/pkg/commands/compute/pack.go), [validate](https://github.com/fastly/cli/blob/v15.1.0/pkg/commands/compute/validate.go), [clone](https://github.com/fastly/cli/blob/v15.1.0/pkg/commands/service/version/clone.go), [EdgeZero adapter](https://github.com/stackpop/edgezero/blob/683202c66948146ec360f490126f1d60f920288b/crates/edgezero-adapter/src/registry.rs). Cargo [metadata](https://doc.rust-lang.org/cargo/commands/cargo-metadata.html) and [build messages](https://doc.rust-lang.org/cargo/reference/external-tools.html), [SPDX 2.3 schema](https://github.com/spdx/spdx-spec/blob/v2.3/schemas/spdx-schema.json). diff --git a/docs/superpowers/specs/2026-10-14-release-tooling-rust-migration.md b/docs/superpowers/specs/2026-10-14-release-tooling-rust-migration.md new file mode 100644 index 0000000000..d291333901 --- /dev/null +++ b/docs/superpowers/specs/2026-10-14-release-tooling-rust-migration.md @@ -0,0 +1,55 @@ +# Release tooling Rust migration addendum + +**Status:** Draft implementation revision for PR #1262 on `spec/1235-release-attestation`. The user authorized implementation on this branch and directed removal of introduced Python tooling on 2026-10-14. This addendum implements that language constraint; maintainer contract/code approval, independently approved source and every selected release qualification remain outstanding. No new branch, worktree or PR is required. + +**Authority:** The [governing contract](2026-10-09-1235-release-attestation-design.md), [feasibility register](2026-10-12-1235-release-attestation-feasibility.md) and [22-task implementation plan](../plans/2026-10-13-attested-release-implementation.md) retain their security requirements. This migration does not amend the index schema, cohort, capabilities or vendor-source acquisition rules. + +## Decision and alternatives + +Use a small domain-specific host-only `crates/trusted-server-release` binary. It follows the existing host-only codegen workspace-member precedent, uses Rust 2024 and pinned Rust 1.95.0, and is a workspace member outside `default-members`. It has no dependency on core, CLI or JS crates and is not a fifth canonical release asset. Existing `collect-js.mjs` and the Node composition observer remain responsible for actual bundling/contribution capture; POSIX wrappers remain thin fixed adapters. + +Moving the producer into `ts` would reuse command dispatch but introduces CLI → core → JS build dependencies before JS preparation. That bootstrap cycle prevents the CLI from producing its own prerequisite JS evidence. Tasks 17–20 therefore keep `ts` as the read-only consumer. Expanding Node to own all native release security would reuse an installed runtime but duplicate native process/archive/platform enforcement and still require separately authenticated Node acquisition. A generic attestation crate adds an unsupported abstraction and unnecessary public APIs. The independent Rust tool is the smallest safe boundary for the existing producer requirements. + +## Modules and data flow + +Create only modules needed by the current migration stage. `documents`, `policy`, `preflight` and `source` establish bounded parsing and independently approved fixed inputs. `materials`, `process`, `boundary` and `preparation` authenticate original acquisitions and execute fresh work under the selected native denial boundary. `javascript` invokes retained Node tooling, records actual lifecycle observations, completes two independent offline installations/tests/builds and compares all final bundles. `qualification` checks reviewed platform evidence. `recipe`, `inspect` and `composition` drive actual Cargo compilation, final-byte/runtime acceptance and observed Rust/native/sysroot/JS evidence. `spdx`, `collection`, `distribution`, `publication` and `package` supply the remaining existing producer tasks without changing signing/publication ownership. + +The flow is independently approved source → authenticated acquisitions → current native installed-tool rehash → actual confined execution → private typed completion → checked artifact/composition/transfer records → isolated signer → sole immutable publisher. Constructors for source/acquisition approval and JS completion remain private to their successful verification/execution paths. Serialized booleans, source labels, an arbitrary inventory or a pathname plus checksum cannot construct authority. Retained bytes are rehashed at handoff/use. All-cohort remotely reviewed qualification receipts are separate from evidence about tools actually installed in the current process environment; neither substitutes for the other. + +Unit tests remain private alongside their modules or in registered child `tests.rs` files. Public command-process tests live in `tests/commands.rs`; data fixtures live in `tests/fixtures/`. Register tests before filtered runs and require a nonzero selection with the plan's POSIX `awk` guard. Do not expose internal APIs for integration tests. + +Reuse existing workspace serde/serde_json, error-stack, derive_more, clap, SHA, log, tempfile, toml, gix, libc and flate2 only where needed, with minimal dependency features. Existing bounded TAR/ZIP, Draft 2020-12/Draft 7 schema and ELF/Mach-O/core-Wasm requirements justify maintained format parsers. Exact replacement versions require primary-source research, lockfile review, acquisition identity and qualification; no library selection itself is a qualified tool pin. The researched [jsonschema 0.58.6 options](https://docs.rs/jsonschema/0.58.6/jsonschema/struct.ValidationOptions.html) are a candidate for explicit-dialect, offline-only validation with default network/file features disabled. No optional language binding, arbitrary public schema path or automatic reference retrieval is selected. + +Strict JSON parsing rejects duplicate keys through a recursive Serde visitor before insertion, limits container depth to 64, preserves original bytes for SHA-256 and enforces every role size bound before allocation/download. Before `deserialize_any`, a string-aware lexical scanner validates every numeric token across the whole document, including opaque fields and top-level values: negative integers fit `i64`, nonnegative integers fit `u64`, and decimal/exponent tokens parse as finite `f64`. Quoted number-like text and escaped string contents remain text. The visitor's finite-float check alone cannot reject integer overflow that Serde converts to a finite float; the plan's example is an ambiguity parser requiring this strict prevalidation. The existing JS build inventory scanner supplies a behavioral reference without a JS-crate dependency. Tests cover opaque/top-level integer overflow, decimal/exponent overflow and quoted-text controls; closed role checks additionally enforce safe-integer limits and negative/noninteger-length rejection. Schema checks accompany semantic joins and actual coverage. Governing schemas use Draft 2020-12; the official SPDX 2.3 schema uses Draft 7. The retained [official schema source](https://github.com/spdx/spdx-spec/blob/aadf3b0b8dbbabdb4d880b0fc714255fea436ff7/schemas/spdx-schema.json) is 45,312 bytes, SHA-256 `239208b7ac287b3cf5d9a9af23f9d69863971102a5e1587a27a398b43490b89b`. This measured candidate identity is not schema-engine or real-SBOM qualification. + +## Bootstrap and unchanged release gates + +POSIX bootstrap first establishes independently approved source and an authenticated OS-native Rust source-build chain, then builds/selects the host tool and records its complete executable source/dependency/tool closure. A colocated self-authored SHA manifest does not authenticate that chain. Developer `cargo run/test` commands establish local behavior only; the source-built tool has no official release-binary proof. The producer executable is not distributed as a canonical asset. Pending approval, acquisition roots or measured pins fail closed; downloaded evidence cannot open a candidate/production selector. + +The producer helper MUST itself be built and executable inside the selected GNU builder at glibc <=2.34, using an independently approved image/toolchain/material channel. An ambient Ubuntu 24 build cannot be assumed to load in UBI 9. Bind helper target, loader/runtime, source/dependency closure and actual native executable identity before execution. macOS helpers require the approved native CPU/SDK/floor-compatible chain. Initial shell/hash/Rust/Cargo authority is independent of the tool; record and authenticate rustup only if it is actually used (direct signed-component installation does not require it); the tool cannot authenticate itself. Native Rust helper/staged POSIX drivers implement socket and lifecycle probes, without language substitutions. + +The four canonical assets remain default Fastly Wasm plus three `ts` executables. Complete final Fastly A/B equality, both final-byte application checks and all canonical A/SPDX proofs are mandatory. Native B remains a bounded optional measurement. GNU remains glibc ceiling 2.34 with three clean compatibility runtimes; macOS requires actual native 15 ARM/Intel execution with loader floor 13.0. macOS 26.6.2 local diagnostics cannot satisfy those gates. + +Real positive/denied native socket controls and actual Cargo/npm missing-tool/download attempts use the same boundary as production execution. Source/object stores and acquired shared inputs are read-only; private profile/install/output writes remain bounded. Children and descendants are killed/reaped on timeout, cancellation or pipe limits, and temporary profiles/resources plus Docker containers are cleaned up with bounded stop/remove behavior. Rust does not waive acquisition/SDK/native archive identity, Cargo effective unit features, final retained JS classification or conservative reachability limits. Original Fastly vendor archive authentication remains a separate unsatisfied gate until independently reviewed vendor-source evidence is available. + +One `release.yml` owns publication and same-source `release-build.yml` owns signing. Exact action 4.2.2/full SHA and gh 2.97.0 contracts, SLSA Build Level 2 target, exact current-run artifact/attempt/topology bindings, strict SPDX/graph joins and immutable publication remain unchanged. `ts release verify` remains read-only. Manual Fastly consumption inspects the approved four-member archive with verified exact Wasm bytes; integrated EdgeZero deployment/recovery and future packaging retain their separate approval/proof boundaries. + +## Migration order and evidence + +The active plan keeps Task IDs 1–22 and prepends migration checkpoints: M0 verify removal/fail-closed wrappers; M1 Rust documents/preflight/source; M2 original materials/process/OS boundaries; M3 real two-installation JS completion; M4 actual canonical recipes/inspection/composition; then finish Tasks 10–22. Full AGENTS CI and independent review remain required before handoff. No resource creation/dispatch, signing, publication or provider operation follows from this language correction. + +Introduced Python producer tooling and requirements are removed. The host-only crate implements strict documents/policy, local source/preflight checks, a helper-closure fingerprint, bounded process supervision and final-artifact inspection. Rust build-fact and checked-inventory guards and Node composition are implemented. The experimental macOS profile kills and rejects descendants that escape their process group, even with a scrubbed environment, by identifying its per-run sandbox instance; it remains unqualified for M2 lifecycle containment. Original npm/Cargo Git material verification and M3 real two-installation JS orchestration are implemented; the full Linux A/B execution test passed in ordinary PR CI ([evidence](../../../scripts/release/qualification/2026-10-15-js-two-builds.md)). That CI candidate is not release qualification. Qualification of M2 and M4 (candidate recipes for all four assets pass ordinary CI, unqualified), qualified macOS lifecycle containment, independently approved acquisition/bootstrap roots and every selected hosted qualification remain pending. Production execution remains closed. + +## Mandatory replacement regressions + +Tasks 3 and M2/Tasks 7–9 MUST carry the original failure cases into real Rust tests before qualification: + +- Audit the true Git common directory for linked-worktree alternates/grafts/shallow/replacements/config/environment channels; reject unsupported linked indirections instead of partially checking them. Preserve bounded detailed Git errors. Reject untracked extensionless `rust-toolchain` before toolchain selection. Join approved source and original inventory commits. +- Audit inherited hosted/setup variables before clearing them. Exact reviewed/qualified values and path identities are required for accepted `RUSTFLAGS`, `VCPKG_INSTALLATION_ROOT` and other selectors; unreviewed values fail. Production selection MUST reject all pending/null pins by default. Named audit diagnostics cannot construct authorization state. +- Docker MUST use its default private PID namespace, not invalid `--pid=private`. Both host-owned seccomp profiles and empty Docker configuration MUST be outside container-writable roots; scan before creation and rehash both before every host command. Select authenticated client/config/Unix daemon, no TTY/plugins/context/environment overrides, read-only anchors, and reject escaping/dangling aliases in the container namespace. Actual positive/denial/whole-recipe execution is required, not argv checks alone. +- Every immutable macOS source/object regular file MUST have `nlink == 1` before execution. Actual hard-link write-through/restoration negatives are required. Signals MUST be restricted to `same-sandbox`, with own-child success and outside same-user sibling/orchestrator denial. +- Observations MUST join the approved-source catalog, selection/version/authentication identities, SDK/raw receipts and the complete compiled helper closure, including all path-dependency manifests such as `crates/trusted-server-openrtb/Cargo.toml`, tool-version files and selected executable digests. Source changes invalidate the closure. Labels, boolean claims and checksums cannot self-qualify. +- Container CPU/memory limits MUST fit actual host capacity; Cargo A jobs 4/B jobs 2 does not require four container CPUs on a two-vCPU host. Record actual errno instead of inventing `ETIMEDOUT`, hash the actual shell instead of assuming `/bin/sh --version`, and use host-independent environment/tool-path fixtures without ambient shim or interpreter trust. +- Both actual JS A/B test processes MUST unconditionally inject `TRUSTED_SERVER__RELEASE_JS_TESTS=1` and execute the full registered suite/type checks. Lifecycle/Cargo/compiler/proc-macro/native children MUST inherit OS-enforced denial. Timeout/cancellation/pipe limits MUST kill and reap descendants; token/provider/injection environment is cleared. + +The [primary plan](../plans/2026-10-13-attested-release-implementation.md#threat-to-negative-proof-trace) maps these pending proofs to TH IDs and existing T-1–T-13 families. Ordinary native CI checks utility behavior and nonzero test selection; it does not qualify native bootstrap or recipes. Manual verified-byte consumption/documentation is AC-9/D6/D8; integrated context, staging, preactivation and recovery remain AC-8 and do not block the manual route. diff --git a/docs/superpowers/specs/schemas/release-index-v1.schema.json b/docs/superpowers/specs/schemas/release-index-v1.schema.json new file mode 100644 index 0000000000..1adaa0d20f --- /dev/null +++ b/docs/superpowers/specs/schemas/release-index-v1.schema.json @@ -0,0 +1,581 @@ +{ + "$comment": "Evaluate explicitly with JSON Schema Draft 2020-12 as required by R-INDEX. This dialect declaration is part of the normative specification.", + "title": "Trusted Server release-index version 1", + "description": "Normative structural contract. The specification adds exact cohort membership, unique asset/companion names, companion naming, byte digest checks, source approval and evidence-policy checks.", + "type": "object", + "additionalProperties": false, + "required": [ + "schema_version", + "repository", + "tag", + "source_commit", + "recipe_source_commit", + "cohort_source_commit", + "cohort", + "policy_version", + "policy_sha256", + "tsjs_inventory", + "artifacts" + ], + "properties": { + "schema_version": { + "const": 1 + }, + "repository": { + "const": "IABTechLab/trusted-server" + }, + "tag": { + "type": "string", + "maxLength": 128, + "pattern": "^v(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*)(?:\\.(?:0|[1-9][0-9]*|[0-9]*[A-Za-z-][0-9A-Za-z-]*))*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?$", + "not": { + "pattern": "[\\r\\n]" + } + }, + "source_commit": { + "type": "string", + "pattern": "^[0-9a-f]{40}$", + "not": { + "pattern": "[\\r\\n]" + } + }, + "recipe_source_commit": { + "type": "string", + "pattern": "^[0-9a-f]{40}$", + "not": { + "pattern": "[\\r\\n]" + } + }, + "cohort_source_commit": { + "type": "string", + "pattern": "^[0-9a-f]{40}$", + "not": { + "pattern": "[\\r\\n]" + } + }, + "cohort": { + "const": "phase1-fastly-cli" + }, + "policy_version": { + "type": "integer", + "minimum": 1, + "description": "Producer policy at source_commit. Must match an independently trusted accepted historical producer policy; does not select consumer policy." + }, + "policy_sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "description": "SHA-256 of producer policy bytes at source_commit; consumer acceptance is defined by the specification.", + "not": { + "pattern": "[\\r\\n]" + } + }, + "tsjs_inventory": { + "allOf": [ + { + "$ref": "#/$defs/file" + }, + { + "properties": { + "name": { + "const": "tsjs-inventory.json" + }, + "size": { + "maximum": 1048576 + } + } + } + ] + }, + "artifacts": { + "type": "array", + "minItems": 4, + "maxItems": 4, + "uniqueItems": true, + "items": { + "$ref": "#/$defs/artifact" + }, + "allOf": [ + { + "contains": { + "properties": { + "name": { + "const": "trusted-server-fastly.wasm" + } + }, + "required": ["name"] + }, + "minContains": 1, + "maxContains": 1 + }, + { + "contains": { + "properties": { + "name": { + "const": "ts-x86_64-unknown-linux-gnu" + } + }, + "required": ["name"] + }, + "minContains": 1, + "maxContains": 1 + }, + { + "contains": { + "properties": { + "name": { + "const": "ts-aarch64-apple-darwin" + } + }, + "required": ["name"] + }, + "minContains": 1, + "maxContains": 1 + }, + { + "contains": { + "properties": { + "name": { + "const": "ts-x86_64-apple-darwin" + } + }, + "required": ["name"] + }, + "minContains": 1, + "maxContains": 1 + } + ] + } + }, + "$defs": { + "file": { + "type": "object", + "additionalProperties": false, + "required": ["name", "size", "sha256"], + "properties": { + "name": { + "type": "string", + "maxLength": 160, + "pattern": "^[A-Za-z0-9][A-Za-z0-9_-]*(?:\\.[A-Za-z0-9_-]+)*$", + "not": { + "pattern": "[\\r\\n]" + } + }, + "size": { + "type": "integer", + "minimum": 0, + "maximum": 536870912 + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "not": { + "pattern": "[\\r\\n]" + } + } + } + }, + "artifact": { + "type": "object", + "additionalProperties": false, + "required": [ + "name", + "size", + "sha256", + "role", + "kind", + "package", + "target", + "variant", + "assurance_profile", + "capabilities", + "compatibility", + "js_inventory_sha256", + "companions" + ], + "properties": { + "name": { + "enum": [ + "trusted-server-fastly.wasm", + "ts-x86_64-unknown-linux-gnu", + "ts-aarch64-apple-darwin", + "ts-x86_64-apple-darwin" + ] + }, + "size": { + "type": "integer", + "minimum": 1, + "maximum": 536870912 + }, + "sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "not": { + "pattern": "[\\r\\n]" + } + }, + "role": { + "enum": ["server", "operator-cli"] + }, + "kind": { + "enum": ["wasm-core", "native-executable"] + }, + "package": { + "enum": ["trusted-server-adapter-fastly", "trusted-server-cli"] + }, + "target": { + "enum": [ + "wasm32-wasip1", + "x86_64-unknown-linux-gnu", + "aarch64-apple-darwin", + "x86_64-apple-darwin" + ] + }, + "variant": { + "const": "default" + }, + "assurance_profile": { + "enum": ["provenance-sbom-repeatable", "provenance-sbom"] + }, + "capabilities": { + "type": "array", + "minItems": 1, + "maxItems": 2, + "uniqueItems": true, + "items": { + "enum": ["fastly-production", "fastly-staging", "cli-execute"] + } + }, + "compatibility": { + "$ref": "#/$defs/compatibility" + }, + "js_inventory_sha256": { + "type": "string", + "pattern": "^[0-9a-f]{64}$", + "not": { + "pattern": "[\\r\\n]" + } + }, + "companions": { + "type": "object", + "additionalProperties": false, + "required": [ + "sbom", + "provenance", + "sbom_attestation", + "build_evidence", + "build_recipe", + "rebuild_check" + ], + "properties": { + "sbom": { + "allOf": [ + { + "$ref": "#/$defs/file" + }, + { + "properties": { + "size": { + "maximum": 16777216 + } + } + } + ] + }, + "provenance": { + "allOf": [ + { + "$ref": "#/$defs/file" + }, + { + "properties": { + "size": { + "maximum": 33554432 + } + } + } + ] + }, + "sbom_attestation": { + "allOf": [ + { + "$ref": "#/$defs/file" + }, + { + "properties": { + "size": { + "maximum": 33554432 + } + } + } + ] + }, + "build_evidence": { + "allOf": [ + { + "$ref": "#/$defs/file" + }, + { + "properties": { + "size": { + "maximum": 33554432 + } + } + } + ] + }, + "build_recipe": { + "allOf": [ + { + "$ref": "#/$defs/file" + }, + { + "properties": { + "size": { + "maximum": 33554432 + } + } + } + ] + }, + "rebuild_check": { + "allOf": [ + { + "$ref": "#/$defs/file" + }, + { + "properties": { + "size": { + "maximum": 33554432 + } + } + } + ] + } + } + } + }, + "oneOf": [ + { + "properties": { + "name": { + "const": "trusted-server-fastly.wasm" + }, + "role": { + "const": "server" + }, + "kind": { + "const": "wasm-core" + }, + "package": { + "const": "trusted-server-adapter-fastly" + }, + "target": { + "const": "wasm32-wasip1" + }, + "variant": { + "const": "default" + }, + "assurance_profile": { + "const": "provenance-sbom-repeatable" + }, + "capabilities": { + "type": "array", + "minItems": 1, + "maxItems": 2, + "uniqueItems": true, + "items": { + "enum": ["fastly-production", "fastly-staging"] + }, + "contains": { + "const": "fastly-production" + } + }, + "compatibility": { + "properties": { + "runtime": { + "const": "fastly-compute" + }, + "architecture": { + "const": "wasm32" + } + } + } + } + }, + { + "properties": { + "name": { + "const": "ts-x86_64-unknown-linux-gnu" + }, + "role": { + "const": "operator-cli" + }, + "kind": { + "const": "native-executable" + }, + "package": { + "const": "trusted-server-cli" + }, + "target": { + "const": "x86_64-unknown-linux-gnu" + }, + "variant": { + "const": "default" + }, + "assurance_profile": { + "const": "provenance-sbom" + }, + "capabilities": { + "const": ["cli-execute"] + }, + "compatibility": { + "properties": { + "runtime": { + "const": "linux-gnu" + }, + "architecture": { + "const": "x86_64" + } + } + } + } + }, + { + "properties": { + "name": { + "const": "ts-aarch64-apple-darwin" + }, + "role": { + "const": "operator-cli" + }, + "kind": { + "const": "native-executable" + }, + "package": { + "const": "trusted-server-cli" + }, + "target": { + "const": "aarch64-apple-darwin" + }, + "variant": { + "const": "default" + }, + "assurance_profile": { + "const": "provenance-sbom" + }, + "capabilities": { + "const": ["cli-execute"] + }, + "compatibility": { + "properties": { + "runtime": { + "const": "macos" + }, + "architecture": { + "const": "aarch64" + } + } + } + } + }, + { + "properties": { + "name": { + "const": "ts-x86_64-apple-darwin" + }, + "role": { + "const": "operator-cli" + }, + "kind": { + "const": "native-executable" + }, + "package": { + "const": "trusted-server-cli" + }, + "target": { + "const": "x86_64-apple-darwin" + }, + "variant": { + "const": "default" + }, + "assurance_profile": { + "const": "provenance-sbom" + }, + "capabilities": { + "const": ["cli-execute"] + }, + "compatibility": { + "properties": { + "runtime": { + "const": "macos" + }, + "architecture": { + "const": "x86_64" + } + } + } + } + } + ] + }, + "compatibility": { + "type": "object", + "additionalProperties": false, + "required": ["runtime", "minimum_version", "architecture"], + "properties": { + "runtime": { + "enum": ["fastly-compute", "linux-gnu", "macos"] + }, + "minimum_version": { + "type": "string", + "minLength": 1, + "maxLength": 64 + }, + "architecture": { + "enum": ["wasm32", "x86_64", "aarch64"] + } + }, + "oneOf": [ + { + "properties": { + "runtime": { + "const": "fastly-compute" + }, + "minimum_version": { + "const": "wasip1" + }, + "architecture": { + "const": "wasm32" + } + } + }, + { + "properties": { + "runtime": { + "const": "linux-gnu" + }, + "minimum_version": { + "const": "glibc-2.34" + }, + "architecture": { + "const": "x86_64" + } + } + }, + { + "properties": { + "runtime": { + "const": "macos" + }, + "minimum_version": { + "const": "13.0" + }, + "architecture": { + "enum": ["x86_64", "aarch64"] + } + } + } + ] + } + } +} diff --git a/scripts/release/js-composition.md b/scripts/release/js-composition.md new file mode 100644 index 0000000000..aeb877b790 --- /dev/null +++ b/scripts/release/js-composition.md @@ -0,0 +1,269 @@ +# JS composition producer interface + +`collect-js.mjs` reuses the ordinary build pipeline with a read-only Rollup +observer. Its version-1 output follows the closed Task 5 root/module schema. +It requires the existing `TRUSTED_SERVER__RELEASE_BUILD=1` control and a full +`TRUSTED_SERVER__GIT_VERSION`. Any skip presence or invalid release control fails. +Expected entry points come from the selected full commit’s Git tree, independently +of the current index. The collector builds only its own checkout, so imported +build/tool code cannot be measured as a different checkout’s installation. Source-byte immutability remains preparation’s check. +The release options disable Vite configuration and `.env` loading and select +production mode; Vitest uses the same release controls. These options do not +remove inherited Node/npm/Vite environment variables: release preflight owns +that rejection. + +## Contribution semantics + +Contributions with positive Rollup `renderedLength` carry +`stage: "rollup-rendered"` and `rendered_length`. Type-only and wholly eliminated +modules are excluded. Later esbuild minification can remove additional code. +These records describe conservative post-Rollup reachability, not final function +reachability or attribution of final bundle bytes. Final module sizes and SHA-256 +are measured from files after Vite/esbuild finishes. + +Source contributions contain repository-relative `path`, original-file `sha256` +and `transform` (`identity`, `raw` or `inline`). npm contributions additionally +contain exact `lock_path`, `package_name`, `package_version` and archive +`integrity`; their paths are relative to the owning installed package. Only the selected registry’s canonical HTTPS tarball sources are supported; +links, missing sources and file/git sources fail. Parent/root aliases are +normalized before nearest-owner lookup; package-root aliases and escapes fail. Unsupported +queries, unmapped owners, symlink escapes and ambiguous identities fail. + +Generated contributions contain `id`, `plugin`, `owner_lock_path` and +`generated_sha256` of the actual transformed virtual code. The reviewed mappings +are Vite's bundled CommonJS helper, CommonJS ES-import wrappers and Vite's preload +helper. Wrappers additionally record the underlying file's `input` origin. +Unknown virtual producers fail. Wrapper IDs use normalized input paths; +transformed-code hashes still measure actual code, which can include checkout +paths. Contribution inventories are not a claim that different checkout paths +produce identical virtual-code hashes. The bundle-byte comparison is separate. + +## Checked preparation input + +The producer accepts a bounded strict UTF-8 JSON document with exactly: + +- `schema_version: 1`; +- `packages`: the full measured installed `npm-package` material set; +- `distributions`: records with exactly `material` and `path`; +- `tools`: records with `name`, `version`, `role`, `material_id`; +- `lifecycle_events`: observed executions, including executions with no output; +- `lifecycle_outputs`: observed output files. + +A package material contains `kind: "npm-package"`, `id: "npm:"`, +`lock_path`, `name`, `version`, `integrity` and `installed_tree_sha256`. +Only installed locked locations are recorded; optional absent platform packages +are not claimed installed. The selected native esbuild and Rollup packages are +mandatory through the actual-tool checks. + +Each distribution material contains `kind: "tool-distribution"`, `id`, `name`, +`version`, `sha256`. `path` selects the actual measured tool entry file or native +executable, never a command to run. Required tools are Node, npm, Vite, Rollup, +esbuild, Vitest and the selected esbuild/Rollup native executables. Paths, versions, +roles and file digests MUST match the producer's actual tool selection. Whole +locked npm package trees are recorded separately. Tool entry/native file hashes +are measured identities; they are not authenticated distribution acquisition or +qualified host evidence. Task 8 supplies that boundary. + +Lifecycle events contain `kind: "lifecycle-event"`, unique `id`, +`package_material_id`, `event` and `script_sha256`. The script digest is SHA-256 +of the UTF-8 command string in the owning installed package's +`package.json.scripts[event]`. Outputs contain `kind: "lifecycle-output"`, unique +`id`, the same event/script/package join, installation-relative `path`, `size` +and `sha256`. Each output MUST remain inside its owning package and match an +explicit observed event. Registry-install coverage requires exactly the installed packages’ declared +preinstall, install and postinstall hooks, including hooks producing no files. +Registry dependency prepare scripts do not run in the selected npm ci recipe. +Implicit binding.gyp installs without explicit reviewed hooks are rejected. Neither array is inferred from lockfile flags. Task 7 MUST produce +these records from actual preparation execution; semantic checks here cannot +prove that an execution occurred or that the observations are exhaustive. +Synthetic fixture observations exercise joins but do not claim genuine execution +history. Missing required events cannot produce an inventory. + +## Installed tree hash + +`installed_tree_sha256` hashes the UTF-8 domain separator +`trusted-server-installed-tree-v1\n`, then entries in recursive sorted pathname +order. A directory contributes compact JSON `[relative_path,"directory"]` plus +newline. A regular file contributes compact JSON +`[relative_path,"file",byte_length,is_executable]`, newline, actual bytes and +newline; `is_executable` means any executable bit is set. A symlink contributes +compact JSON `[relative_path,"symlink",relative_target]` plus newline and MUST +resolve inside its package. Separately locked nested package roots and their +contents are excluded from the parent's hash. Timestamps, owners and +non-executable permission bits are excluded. Files are read through held +regular-file descriptors with mutation checks and bounded buffers. + +JSON and final inventory are limited to 1 MiB; source and final bundle reads are +limited to 32 MiB. Duplicate keys, invalid UTF-8, BOM, unpaired surrogates, +unsafe integers, non-finite numbers and more than 64 nested containers fail. +The inventory writer sorts module/material/tool records and appends one newline. +The inventory MUST live outside the bundle directory. This task does not perform +fresh installation, network confinement, A/B comparison, signing or publication. + +## Independent Rust preparation recipe + +The private Rust preparation and comparison recipe uses the retained Node collector. +`build-js.sh` remains a thin POSIX adapter to the independently approved-source +OS-native `trusted-server-release build-js` command. Until its authenticated Rust +bootstrap and qualified source-owned selections are implemented, the wrapper +refuses before creating work directories. No public fixture bypass, unchecked +interpreter or colocated self-authored SHA file establishes bootstrap authority. +The host-only tool has no core/CLI/JS crate dependency; it retains this Node +collector and composition pipeline. See the [migration addendum](../../docs/superpowers/specs/2026-10-14-release-tooling-rust-migration.md). + +The recipe lives in private `materials`, `process`, `boundary`, +`preparation` and `javascript` modules. Each installation owns a fresh copy of +approved tracked source bytes, cache, node_modules, HOME, TMP, lifecycle observations +and bundles. The parent authenticates the approved Git objects and derives the +complete module set; it supplies that set and the source commit through a protected +read-only source-context document. JS working copies contain no inherited `.git`, +configuration or history. Git material snapshots for Cargo remain separate. +Original sources/object stores and shared authenticated archives/tools are read-only; +sibling data/metadata, shared writes and network sockets are denied. The shared JS +execution path selects Linux containers with owned descendant cleanup; macOS native +recipes require their own lifecycle containment and qualification. Current installed-tool +rehashing is separate from independently reviewed all-cohort hosted evidence. +Local macOS 26.6.2 diagnostics cannot qualify macOS 15 ARM/Intel or GNU. + +Archive filenames remain locked SHA-512 hex digests followed by `.tgz`. Authenticate +original bytes before bounded decompression/TAR inspection. Preserve the source-owned +exception for three exact locked npm archives containing identical +`package/./dist/index.js` and `package/dist/index.js` entries: agent-base 7.1.4, +http-proxy-agent 7.0.2 and https-proxy-agent 7.0.6. Resolve their original locked +SHA-512 identities and require matching regular type, size, mode and bytes; +materialize the canonical file once. Other duplicates, dot aliases, traversal, +links, devices and privileged modes fail. Eight exact locked type-package +versions use reviewed legacy roots: `@types/chai` 5.2.3 (`chai`), +`@types/deep-eql` 4.0.2 (`deep-eql`), `@types/estree` 1.0.8 (`estree`), +`@types/jsdom` 27.0.0 (`jsdom`), `@types/json-schema` 7.0.15 (`json-schema`), +`@types/json5` 0.0.29 (`json5`), `@types/node` 24.10.13 (`node v24.10`), +and `@types/tough-cookie` 4.0.5 (`tough-cookie`). Seven of these originals +(all except json5) may use the observed node-tar timestamp layout: empty +130-byte prefix, a zero separator, two eleven-digit octal timestamps with +NUL terminators, and zero remaining reserved bytes. No timestamp propagates +to extracted files. Other hidden name/prefix bytes fail. + +Only locked `gulp-wrap` 0.15.0 may carry local PAX metadata. Each bounded +record uses the reviewed metadata-key allowlist; duplicate, unknown and +malformed fields fail. Its path and size must exactly match the immediately +following raw regular-file header, and its own name must be +`PaxHeader/`. Global, consecutive and dangling records and any +path/size override fail. These exceptions apply only after original-byte +authentication and exact name/version selection; they are not general +extension support. + +Registry archive SHA-256/exact-root and +Git revision/whole-tree validation remain available to Cargo preparation. Query +selected Git object identity/type/size before body acquisition: reject objects +over 32 MiB or cumulative bodies over 512 MiB, then rejoin exact body headers. +This reader assumes the sanitized trusted Git implementation and an immutable +object store, rather than claiming adversarial race-proof streaming. + +The Rust dependency archive inspector limits each authenticated compressed +original to 64 MiB, decoded TAR bytes to 512 MiB, each member to 128 MiB, +entries to 100,000, path depth to 64 and path text to 4,096 bytes. It accepts +ASCII regular-file and directory names under the exact locked root, rejects +case aliases and unsupported extension records (including unreviewed PAX, +GNU long-name and sparse records), and requires a single complete gzip member with complete +TAR zero terminators and no hidden trailing data. These are dependency archive +limits, separate from toolchain distributions and Git object limits. The +128 MiB member limit accommodates the current locked optional OpenBSD Rollup +binary expands to 110,275,248 bytes. Hashing and extraction remain streamed; +this does not increase the compressed-original or total decoded-byte limits. Extraction +owns a fresh private directory and preserves original compressed bytes. File +permissions become owner read/write plus the archive's executable bits; archive +ownership, timestamps and other permission bits do not propagate. This stage +does not establish an installed dependency, executed hook or completed JS build. + +Seed each private cache through selected npm's public `cache add` API with only +verified archives, offline and scripts disabled. Run fresh pinned npm 11.19.0 +`ci --offline --include=dev --no-audit --no-fund` with hooks in the foreground +under the qualified lifecycle denial boundary. Reject root install/prepare hooks +and implicit binding.gyp installs. Preserve the two exact reviewed dependency +hooks and actual starts/completions/failures/script hashes/changed-owned-files, +including no-output hooks and swallowed optional failures. Bind bare `node` to +selected Node 24.20.0 despite package-bin shadowing. Missing prepared native +Vite/esbuild tools MUST fail without fetching substitutes. Observed lifecycle +records cannot be inferred from lockfile flags or successful npm exit alone. + +The source-owned declaration policy in the Rust material reviewer permits only +these exact dependency install-hook tuples. It checks the name/version against +the approved lock, the manifest bytes against the authenticated extraction, and +the command bytes against this policy: + +| Package | Version | Event | Exact command | +| --------- | -------- | ------------- | --------------------------------------------------- | +| `core-js` | `3.48.0` | `postinstall` | `node -e "try{require('./postinstall')}catch(e){}"` | +| `esbuild` | `0.27.3` | `postinstall` | `node install.js` | + +Any added hook or command/version change requires a source policy review. +Root review rejects all ten `npm ci` lifecycle hooks: `preinstall`, `install`, +`postinstall`, `prepublish`, `preprepare`, `prepare`, `postprepare`, +`predependencies`, `dependencies` and `postdependencies`. The last three also run +through pinned npm's Arborist when installation changes the dependency tree. These +results describe declarations only. They grant no execution authority and supply +no starts, completions, failures or changed-file observations. Full installation +tree mutation checks and actual lifecycle observations remain separate gates. + +`observe-npm.mjs` adapts the selected npm 11.19.0 lifecycle entry before npm loads +its dependency graph. It records real starts, fulfilled/rejected execution, +exit status and changed owned-file facts in memory. This includes hooks with no +output and optional hook failures that npm would otherwise swallow. Root calls +for absent scripts remain no-ops; root commands and unreviewed dependency hooks +fail. Selected Node replaces the reviewed command's bare `node` and leads the +child PATH after npm prepends package bins. Lifecycle children receive no parent +stdin or additional inherited descriptors. + +The Linux supervisor loads the authenticated native non-dumpable-process guard +before giving the observer its private request key. The guard and the process +memory syscall denials prevent lifecycle children from reading or modifying the +trusted npm process. Observations and the checked preparation document are sealed +together with HMAC-SHA-256 over their exact original JSON bytes. The supervisor +joins the nonce, source, workspace, cache, guard and native esbuild expectation, +requires successful npm exit and complete successful events, and independently +checks the retained outputs. A child's printed prefix or writable JSON file does +not establish an observation. This adapter provides no macOS completion path and +does not itself authenticate Node/npm distributions or qualify a hosted runner. + +The reviewed esbuild 0.27.3 postinstall can hard-link its selected native package +binary to `node_modules/esbuild/bin/esbuild`. To retain the boundary's global +single-link policy, the observer detaches only this exact pair after execution. +Both paths must identify the same regular inode with exactly two links and match +the parent-derived authenticated native archive's digest, size and expected mode. +The observer copies the owned esbuild path into a fresh file and atomically +replaces it, then verifies both files have one link and unchanged executable +bytes. It records `esbuild-hardlink-copy-v1` separately in the sealed observations; +the changed owned executable remains a lifecycle output. Other hard links, +additional aliases, mismatched bytes/modes or unsafe paths fail. This +normalization changes inode ownership of the bytes, not executable content. + +Run collector and unconditional full Vitest/type checks under that same native +boundary, checking tracked source and unexpected files before/after each stage. +Preserve release Vitest's literal host and narrowly scoped native `node:` import +resolver; npm imports remain unmodified. `configFile: false` collector builds +remain separate from that test plugin. Existing ordinary development behavior +remains supported. Both actual A/B Vitest processes MUST unconditionally receive +`TRUSTED_SERVER__RELEASE_JS_TESTS=1` and run the full registered suite/type checks, +with no developer opt-out. Require all tracked core/integration final files +compared byte-for-byte. Ordinary development skips only the two slow Git +collector fixtures by default; CI enables the full suite. B inventory virtual-code hashes may differ because paths differ. + +After all actual preparation/lifecycle/test/build/control/comparison stages pass, +construct a private typed completion bound to approved source, authenticated tools, +settings, raw inventory digest and retained A module bytes. A parsed JSON boolean, +source label, arbitrary checked inventory or asserted path/hash cannot construct +that state. Atomically retain A's `bundles/` plus sibling `tsjs-inventory.json`, +with only JS files in `bundles/`; preserve A's original inventory encoding and +closed root/module shape, exact integer sizes and final digests. Rehash at handoff. +Use bounded child pipes/deadlines and kill/reap descendants on cancellation, +timeout or oversize output. Clean private profiles/resources and Docker containers +on success and failure with bounded stop/remove operations. + +Once Rust modules/tests exist, derive `HOST_TARGET=$(rustc -vV | sed -n 's/^host: //p')` +and run `cargo test -p trusted-server-release --target "$HOST_TARGET" javascript::tests` +using the [primary plan's nonzero-selection guard](../../docs/superpowers/plans/2026-10-13-attested-release-implementation.md#focused-rust-test-selection-guard). +Private fixtures belong alongside their registered modules; public process tests +use `tests/commands.rs`. Full local two-installation/native-probe execution MUST be +selected explicitly through reviewed test paths and cannot waive hosted gates. + +The committed Node collector/composition fixtures are current local interface checks; utility fixtures do not construct producer completion. Task 7's real two-installation orchestration passed its full Linux execution test in ordinary PR CI; see the [two-build evidence report](qualification/2026-10-15-js-two-builds.md). That candidate run is not release qualification: Task 8's selected hosted/native qualification, authenticated bootstrap and tool pins remain pending. diff --git a/scripts/release/phase1-build.sh b/scripts/release/phase1-build.sh new file mode 100644 index 0000000000..52784593ad --- /dev/null +++ b/scripts/release/phase1-build.sh @@ -0,0 +1,143 @@ +#!/usr/bin/env bash +# Phase 1 ordinary hosted build; no reproducibility or complete composition claim. +set -euo pipefail +build_package=${1:?package required} +build_target=${2:?target required} +release_asset=${3:?asset required} +case "$build_package/$build_target/$release_asset" in + trusted-server-adapter-fastly/wasm32-wasip1/trusted-server-fastly.wasm) executable=trusted-server-adapter-fastly.wasm ;; + trusted-server-cli/x86_64-unknown-linux-gnu/ts-x86_64-unknown-linux-gnu|trusted-server-cli/aarch64-apple-darwin/ts-aarch64-apple-darwin|trusted-server-cli/x86_64-apple-darwin/ts-x86_64-apple-darwin) executable=ts ;; + trusted-server-adapter-axum/x86_64-unknown-linux-gnu/trusted-server-axum-x86_64-unknown-linux-gnu|trusted-server-adapter-axum/x86_64-unknown-linux-musl/trusted-server-axum-x86_64-unknown-linux-musl|trusted-server-adapter-axum/aarch64-unknown-linux-gnu/trusted-server-axum-aarch64-unknown-linux-gnu) executable=trusted-server-axum ;; + *) echo 'Unsupported release build selection' >&2; exit 1 ;; +esac +: "${RUNNER_TEMP:?}" +: "${SOURCE_COMMIT:?}" +[[ "$SOURCE_COMMIT" =~ ^[a-f0-9]{40}$ ]] || { echo 'Invalid source commit' >&2; exit 1; } +[[ "$(git rev-parse HEAD)" == "$SOURCE_COMMIT" ]] || { echo 'Source commit mismatch' >&2; exit 1; } +for variable in TSJS_SKIP_BUILD TSJS_PREBUILT_DIR TSJS_TEST; do + if printenv "$variable" >/dev/null; then + echo "Refusing $variable in the release build" >&2 + exit 1 + fi +done +node scripts/release/phase1.mjs audit +# No operator values enter through inherited environment; these fictional canaries +# additionally catch known config embedding regressions in the final executable. +export TRUSTED_SERVER__RELEASE_BUILD=1 +export TRUSTED_SERVER__GIT_VERSION="$SOURCE_COMMIT" +export TRUSTED_SERVER__PUBLISHER__ORIGIN_URL=https://phase1-origin-canary.example.com +export TRUSTED_SERVER__PUBLISHER__PROXY_SECRET=phase1-secret-canary-32-characters +export EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME=phase1-physical-store-canary +export FASTLY_SERVICE_ID=phase1-service-account-canary +export MACOSX_DEPLOYMENT_TARGET=13.0 +[[ ! -e trusted-server.toml ]] +cat > trusted-server.toml <<'CONFIG' +# Fictional config must never become a build input. +[publisher] +origin_url = "https://phase1-origin-canary.example.com" +proxy_secret = "phase1-secret-canary-32-characters" +[permissions] +policy = "phase1-permission-policy-canary" +CONFIG +trap 'rm -f trusted-server.toml; if [[ -n "${build_container:-}" ]]; then docker rm -f "$build_container" >/dev/null 2>&1 || true; fi' EXIT +export CARGO_TARGET_DIR="$RUNNER_TEMP/phase1-target" +[[ ! -e "$CARGO_TARGET_DIR" ]] +mkdir -p release-assets +[[ -z "$(ls -A release-assets)" ]] +# The pinned Node distribution includes npm. npm verifies lockfile integrity for +# fetched packages; no generator dependencies are installed with unpinned npx. +npm --prefix crates/trusted-server-js/lib ci +if [[ "$build_target" == *-linux-gnu ]]; then + builder_base=registry.access.redhat.com/ubi9/ubi:9.6@sha256:dec374e05cc13ebbc0975c9f521f3db6942d27f8ccdf06b180160490eef8bdbc + docker build --build-arg "BASE_IMAGE=$builder_base" --iidfile "$RUNNER_TEMP/phase1-builder-id" - <<'DOCKERFILE' +ARG BASE_IMAGE +FROM ${BASE_IMAGE} +RUN dnf install -y gcc gcc-c++ glibc-devel git-core cmake make perl && dnf clean all +DOCKERFILE + builder=$(cat "$RUNNER_TEMP/phase1-builder-id") + echo "GNU builder: $builder_base $builder" + build_container=phase1-build-$RANDOM-$$ + mkdir "$CARGO_TARGET_DIR" + docker run --rm --name "$build_container" --workdir /source \ + --mount "type=bind,src=$PWD,dst=/source" \ + --mount "type=bind,src=$CARGO_TARGET_DIR,dst=/target" \ + --mount "type=bind,src=$(dirname "$(dirname "$(node -p process.execPath)")"),dst=/opt/node,readonly" \ + --mount "type=bind,src=$(readlink -f "$(command -v rustup)"),dst=/usr/local/bin/rustup,readonly" \ + --env TRUSTED_SERVER__RELEASE_BUILD --env TRUSTED_SERVER__GIT_VERSION --env TRUSTED_SERVER__PUBLISHER__ORIGIN_URL \ + --env TRUSTED_SERVER__PUBLISHER__PROXY_SECRET --env EDGEZERO__STORES__CONFIG__TRUSTED_SERVER_CONFIG__NAME \ + --env FASTLY_SERVICE_ID --env CARGO_TARGET_DIR=/target \ + "$builder" bash -euc ' + export PATH=/usr/local/bin:/opt/node/bin:/usr/bin:/bin CARGO_HOME=/tmp/cargo RUSTUP_HOME=/tmp/rustup + git config --global --add safe.directory /source + ldd --version | head -1 | grep -F "2.34" + rust_version=$(sed -n '\''s/^channel = "\([^"]*\)"/\1/p'\'' rust-toolchain.toml) + rustup toolchain install "$rust_version" --profile minimal --no-self-update + rustup target add --toolchain "$rust_version" "$2" + export PATH="$(dirname "$(rustup which --toolchain "$rust_version" rustc)"):$PATH" + rustup run "$rust_version" cargo build --package "$1" --release --target "$2" --locked + ' phase1 "$build_package" "$build_target" +else + if [[ "$build_target" == x86_64-unknown-linux-musl ]]; then + sudo apt-get update + sudo apt-get install --yes --no-install-recommends musl=1.2.4-2 musl-dev=1.2.4-2 musl-tools=1.2.4-2 + export REALGCC=gcc-13 CC_x86_64_unknown_linux_musl=musl-gcc CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_LINKER=musl-gcc + export CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS='-C target-feature=+crt-static' + musl-gcc -dumpfullversion + fi + cargo build --package "$build_package" --release --target "$build_target" --locked +fi +cp "$CARGO_TARGET_DIR/$build_target/release/$executable" "release-assets/$release_asset" +shasum -a 256 "release-assets/$release_asset" > "$RUNNER_TEMP/binary.sha256" +node scripts/release/phase1.mjs check "release-assets/$release_asset" +node scripts/release/phase1.mjs smoke "release-assets/$release_asset" "$build_target" +# Pin and checksum the generator source before extraction or execution. Cargo's +# --locked installation authenticates its transitive crates via its shipped lock. +tool_root="$RUNNER_TEMP/phase1-cyclonedx" +mkdir "$tool_root" +curl --fail --silent --show-error --location --retry 3 \ + https://crates.io/api/v1/crates/cargo-cyclonedx/0.5.9/download \ + --output "$tool_root/generator.crate" +printf '%s %s\n' 5d162f67705f0f5038759d73bf546a083bf30e8677c2e944b416bca48d9d69a8 "$tool_root/generator.crate" | shasum -a 256 --check +tar -xf "$tool_root/generator.crate" -C "$tool_root" +CARGO_TARGET_DIR="$tool_root/target" cargo install \ + --path "$tool_root/cargo-cyclonedx-0.5.9" --locked --root "$tool_root/install" +# The generator resolves the whole workspace, including unbuilt members. Acquire +# their selected sources under the lock before denying network resolution. +cargo metadata --locked --format-version 1 --filter-platform "$build_target" > "$tool_root/cargo-metadata.json" +# Pinned generator has no --locked flag. Resolve offline after the locked build +# with identical target/default features, then require the lock to be unchanged. +CARGO_NET_OFFLINE=true "$tool_root/install/bin/cargo-cyclonedx" cyclonedx \ + --manifest-path "crates/$build_package/Cargo.toml" --target "$build_target" \ + --format json --spec-version 1.5 --override-filename phase1 +npm --prefix crates/trusted-server-js/lib sbom --sbom-format cyclonedx > "$tool_root/npm.cdx.json" +node scripts/release/phase1.mjs merge \ + "crates/$build_package/phase1.json" "$tool_root/npm.cdx.json" \ + "release-assets/$release_asset" "$tool_root/combined.cdx.json" +# Convert the internal dependency graph into real SPDX 2.3 before publication. +case "$build_target" in + wasm32-wasip1|x86_64-unknown-linux-gnu|x86_64-unknown-linux-musl) + converter_asset=cyclonedx-linux-x64 + converter_sha=bfc8b2538da86fe239bc53658bbb63c1c8c510a293c1e6891aa5bea5d3c58746 ;; + aarch64-unknown-linux-gnu) + converter_asset=cyclonedx-linux-arm64 + converter_sha=b2e9fdf9665ef49868a2ec012171c6e785dcd69745bc5869e53e4f4bfb096a5f ;; + aarch64-apple-darwin) + converter_asset=cyclonedx-osx-arm64 + converter_sha=750c148780154833f6401f9067d08c5a4c31567b6ee3c26c062c3a95c62d741c ;; + x86_64-apple-darwin) + converter_asset=cyclonedx-osx-x64 + converter_sha=0ea92306fc7d7c30ed112a7463781a200585a3bf1cdaf77229181c5aabd1d4b6 ;; +esac +curl --fail --silent --show-error --location --retry 3 \ + "https://github.com/CycloneDX/cyclonedx-cli/releases/download/v0.33.1/$converter_asset" \ + --output "$tool_root/converter" +printf '%s %s\n' "$converter_sha" "$tool_root/converter" | shasum -a 256 --check +chmod +x "$tool_root/converter" +"$tool_root/converter" convert --input-file "$tool_root/combined.cdx.json" \ + --input-format json --output-format spdxjson \ + --output-file "release-assets/$release_asset.spdx.json" +node scripts/release/phase1.mjs validate-spdx \ + "release-assets/$release_asset.spdx.json" "release-assets/$release_asset" \ + "$tool_root/combined.cdx.json" +shasum -a 256 --check "$RUNNER_TEMP/binary.sha256" +git diff --exit-code -- Cargo.lock crates/trusted-server-js/lib/package-lock.json edgezero.toml diff --git a/scripts/release/phase1.mjs b/scripts/release/phase1.mjs new file mode 100644 index 0000000000..5f0d21fc54 --- /dev/null +++ b/scripts/release/phase1.mjs @@ -0,0 +1,723 @@ +/** Small Phase 1 build-inventory and configuration checks, without release authority. */ +import { createHash, randomUUID } from 'node:crypto' +import { execFileSync } from 'node:child_process' +import { createServer } from 'node:net' +import { once } from 'node:events' +import { readFileSync, readdirSync, statSync, writeFileSync } from 'node:fs' +import { join, resolve } from 'node:path' +import { pathToFileURL } from 'node:url' + +/** Check minimal wasm headers and final Linux loader requirements. */ +export function checkReleaseFormat(bytes, target, elf = '') { + if (target === 'wasm32-wasip1') { + if (!bytes.subarray(0, 8).equals(Buffer.from('0061736d01000000', 'hex'))) + throw Error('Invalid wasm magic or version') + } else if (target.includes('-linux-')) { + if (!bytes.subarray(0, 4).equals(Buffer.from('7f454c46', 'hex'))) + throw Error('Expected ELF binary') + if (target.endsWith('-musl')) { + if (/INTERP|\(NEEDED\)/.test(elf)) + throw Error('Expected fully static musl binary') + } else { + if (/GLIBC_[A-Z]/.test(elf)) throw Error('Unknown GLIBC requirement') + for (const [, major, minor, patch] of elf.matchAll( + /GLIBC_(\d+)\.(\d+)(?:\.(\d+))?/g + )) + if ( + +major > 2 || + (+major === 2 && (+minor > 34 || (+minor === 34 && +patch > 0))) + ) + throw Error('GLIBC requirement exceeds 2.34') + } + } else if (target.endsWith('-apple-darwin')) { + if ( + bytes.length < 32 || + bytes.readUInt32LE(0) !== 0xfeedfacf || + bytes.readUInt32LE(4) !== + (target.startsWith('aarch64-') ? 0x0100000c : 0x01000007) + ) + throw Error('Expected target-matched 64-bit Mach-O') + const end = 32 + bytes.readUInt32LE(20), + count = bytes.readUInt32LE(16) + if (end > bytes.length || count > Math.floor((end - 32) / 8)) + throw Error('Truncated Mach-O commands') + let offset = 32, + minimum + for (let i = 0; i < count; i++) { + if (offset + 8 > end) throw Error('Truncated Mach-O command') + const command = bytes.readUInt32LE(offset), + size = bytes.readUInt32LE(offset + 4) + if (size < 8 || offset + size > end) + throw Error('Invalid Mach-O command size') + if (command === 0x32 || command === 0x24) { + if ( + minimum !== undefined || + size < (command === 0x32 ? 24 : 16) || + (command === 0x32 && bytes.readUInt32LE(offset + 8) !== 1) + ) + throw Error('Invalid macOS deployment command') + minimum = bytes.readUInt32LE(offset + (command === 0x32 ? 12 : 8)) + } + offset += size + } + if (offset !== end || minimum === undefined || minimum !== 0x000d0000) + throw Error('Expected macOS minimum OS 13.0') + console.log('SUCCESS: Mach-O target and macOS minimum OS 13.0:', target) + } +} + +/** Complete converter output with the actual binary's SPDX package. */ +export function completeSpdx(document, binary, original) { + if ( + document.documentDescribes?.length || + document.relationships?.length || + document.packages.some((entry) => entry.SPDXID === 'SPDXRef-Artifact') + ) + throw new Error('Unexpected converter root or relationships') + document.creationInfo ??= {} + document.creationInfo.creators = [ + ...new Set([ + ...(document.creationInfo.creators ?? []), + 'Tool: cargo-cyclonedx-0.5.9', + 'Tool: cyclonedx-cli-0.33.1', + 'Tool: trusted-server-phase1', + ]), + ] + for (const item of document.packages) + for (const key of ['licenseDeclared', 'licenseConcluded']) + if (typeof item[key] === 'string') + item[key] = item[key].replace(/\s+/g, ' ').trim() + const root = original.metadata.component + document.packages.push({ + SPDXID: 'SPDXRef-Artifact', + name: root.name, + versionInfo: root.version, + downloadLocation: 'NOASSERTION', + filesAnalyzed: false, + licenseConcluded: 'NOASSERTION', + licenseDeclared: 'NOASSERTION', + copyrightText: 'NOASSERTION', + externalRefs: root.purl + ? [ + { + referenceCategory: 'PACKAGE-MANAGER', + referenceType: 'purl', + referenceLocator: root.purl, + }, + ] + : [], + checksums: [ + { + algorithm: 'SHA256', + checksumValue: createHash('sha256').update(binary).digest('hex'), + }, + ], + }) + document.documentDescribes = ['SPDXRef-Artifact'] + document.relationships = [ + { + spdxElementId: 'SPDXRef-DOCUMENT', + relationshipType: 'DESCRIBES', + relatedSpdxElement: 'SPDXRef-Artifact', + }, + ] + return validateSpdx(document, binary, original) +} + +/** Check the frozen SPDX root/hash join and preserved package identities. */ +export function validateSpdx(document, binary, original) { + checkBinary(binary) + if ( + document.spdxVersion !== 'SPDX-2.3' || + document.SPDXID !== 'SPDXRef-DOCUMENT' + ) + throw new Error('Expected an SPDX 2.3 document') + const packages = document.packages ?? [] + const ids = new Set(packages.map((entry) => entry.SPDXID)) + if (ids.size !== packages.length) + throw new Error('Duplicate SPDX package identity') + if ([...ids].some((id) => !/^SPDXRef-[A-Za-z0-9.-]+$/.test(id))) + throw new Error('Invalid SPDX package identifier') + const described = new Set(document.documentDescribes ?? []) + for (const edge of document.relationships ?? []) { + if ( + edge.spdxElementId === document.SPDXID && + edge.relationshipType === 'DESCRIBES' + ) + described.add(edge.relatedSpdxElement) + } + const root = packages.find((entry) => entry.SPDXID === [...described][0]) + if (described.size !== 1 || !root) + throw new Error('SPDX must describe exactly one root package') + const hashes = (root.checksums ?? []).filter( + (hash) => hash.algorithm === 'SHA256' + ) + if ( + hashes.length !== 1 || + hashes[0].checksumValue !== + createHash('sha256').update(binary).digest('hex') + ) + throw new Error('SPDX root SHA256 differs from binary') + const flatten = (items) => + items.flatMap((item) => [item, ...flatten(item.components ?? [])]) + const components = [ + original.metadata.component, + ...flatten(original.components ?? []), + ] + const used = new Set() + for (const component of components) { + const match = packages.find( + (entry) => + !used.has(entry.SPDXID) && + entry.name === component.name && + (!component.version || entry.versionInfo === component.version) && + (!component.purl || + entry.externalRefs?.some( + (ref) => + ref.referenceType === 'purl' && + ref.referenceLocator === component.purl + )) + ) + if (!match) throw new Error('SPDX lost component identity or PURL') + if (component === original.metadata.component && match !== root) + throw new Error('SPDX describes a different inventory root') + used.add(match.SPDXID) + } + return document +} + +// Adapted from #1262's native recipe scanner at 10167c6017b725d0a4d5ad0a0c8e6a96fd4aa807. +export const sentinels = [ + 'phase1-origin-canary.example.com', + 'phase1-physical-store-canary', + 'phase1-service-account-canary', + 'phase1-secret-canary-32-characters', + 'phase1-permission-policy-canary', +] + +export function checkBinary(bytes) { + if (bytes.length === 0) throw new Error('Release binary is empty') + for (const marker of sentinels) { + if (bytes.includes(Buffer.from(marker))) + throw new Error('Release binary contains planted operator configuration') + } +} + +// Resolve literal macro arguments, including nested concat!, without Rust dataflow claims. +function rustTokens(source) { + const tokens = [] + for (let i = 0; i < source.length; ) { + if (/\s/.test(source[i])) { + i++ + continue + } + if (source.startsWith('//', i)) { + i = source.indexOf('\n', i) + if (i < 0) break + continue + } + if (source.startsWith('/*', i)) { + let depth = 1 + i += 2 + while (i < source.length && depth) { + if (source.startsWith('/*', i)) { + depth++ + i += 2 + } else if (source.startsWith('*/', i)) { + depth-- + i += 2 + } else i++ + } + continue + } + const raw = source.slice(i).match(/^r(#{0,255})"/) + if (raw) { + const start = i + raw[0].length, + end = source.indexOf('"' + raw[1], start) + if (end < 0) throw Error('Unterminated Rust raw string') + tokens.push({ string: source.slice(start, end) }) + i = end + 1 + raw[1].length + continue + } + const character = source + .slice(i) + .match(/^'(?:\\(?:u\{[0-9a-fA-F_]+\}|x[0-9a-fA-F]{2}|.)|[^'\\\r\n])'/) + if (character) { + i += character[0].length + continue + } + if (source[i] === '"') { + let end = i + 1 + while (end < source.length && source[end] !== '"') { + if (source[end] === '\\') end++ + end++ + } + if (end === source.length) throw Error('Unterminated Rust string') + const value = source + .slice(i + 1, end) + .replace( + /\\(?:x([0-9a-fA-F]{2})|u\{([0-9a-fA-F_]+)\}|(\r?\n)\s*|([nrt0\\"']))/g, + (_, hex, unicode, newline, simple) => { + if (hex) return String.fromCodePoint(parseInt(hex, 16)) + if (unicode) + return String.fromCodePoint( + parseInt(unicode.replaceAll('_', ''), 16) + ) + if (newline) return '' + return { n: '\n', r: '\r', t: '\t', 0: '\0' }[simple] ?? simple + } + ) + tokens.push({ string: value }) + i = end + 1 + continue + } + const word = source.slice(i).match(/^[A-Za-z_][A-Za-z_0-9]*/) + if (word) { + tokens.push(word[0]) + i += word[0].length + } else tokens.push(source[i++]) + } + return tokens +} + +export function checkSource(file, source) { + const tokens = rustTokens(source) + const deployment = (value) => /^(?:TRUSTED_SERVER__|EDGEZERO__)/.test(value) + // Only release control and Cargo-owned target facts are additional build-time inputs. + const allowedBuild = new Set([ + 'TRUSTED_SERVER__GIT_VERSION', + 'TRUSTED_SERVER__RELEASE_BUILD', + 'TRUSTED_SERVER__BUILD_TARGET', + ]) + const expression = (start) => { + if ( + ['env', 'option_env'].includes(tokens[start]) && + tokens[start + 1] === '!' && + ['(', '[', '{'].includes(tokens[start + 2]) + ) { + const close = { '(': ')', '[': ']', '{': '}' }[tokens[start + 2]] + const argument = expression(start + 3) + if (argument && tokens[argument.end] === close) + return { value: '__BUILD_ENV__', end: argument.end + 1 } + return null + } + if (tokens[start]?.string !== undefined) + return { value: tokens[start].string, end: start + 1 } + if ( + tokens[start] !== 'concat' || + tokens[start + 1] !== '!' || + !['(', '[', '{'].includes(tokens[start + 2]) + ) + return null + const close = { '(': ')', '[': ']', '{': '}' }[tokens[start + 2]] + let cursor = start + 3, + value = '' + while (tokens[cursor] !== close) { + const part = expression(cursor) + if (!part) return null + value += part.value + cursor = part.end + if (tokens[cursor] === ',') cursor++ + else if (tokens[cursor] !== close) return null + } + return { value, end: cursor + 1 } + } + const operatorPath = (value) => + /(?:^|[/\\])(?:trusted-server\.toml|\.env(?:[./\\]|$))/.test(value) + for (let i = 0; i < tokens.length; i++) { + if ( + ['env', 'option_env', 'include_str', 'include_bytes'].includes( + tokens[i] + ) && + tokens[i + 1] === '!' + ) { + const argument = expression(i + 3) + if (!argument) continue + const value = argument.value + if ( + (['env', 'option_env'].includes(tokens[i]) && + deployment(value) && + !allowedBuild.has(value)) || + (tokens[i].startsWith('include_') && operatorPath(value)) + ) + throw Error(`${file}: deployment compile-time input`) + } + if (file.endsWith('build.rs')) { + const value = expression(i)?.value + if ( + value !== undefined && + ((deployment(value) && !allowedBuild.has(value)) || operatorPath(value)) + ) + throw Error(`${file}: operator compile-time build input`) + } + } +} + +function namespace(bom, prefix) { + if ( + bom.bomFormat !== 'CycloneDX' || + bom.specVersion !== '1.5' || + !bom.metadata?.component?.['bom-ref'] + ) { + throw new Error('Expected CycloneDX 1.5 with a root component') + } + const rename = (value, key) => { + if (Array.isArray(value)) return value.map((item) => rename(item, key)) + if (value && typeof value === 'object') + return Object.fromEntries( + Object.entries(value).map(([k, v]) => [k, rename(v, k)]) + ) + return typeof value === 'string' && + ['bom-ref', 'ref', 'dependsOn', 'provides'].includes(key) + ? `${prefix}-${createHash('sha256').update(value).digest('hex')}` + : value + } + return rename(bom) +} + +export function mergeSboms(cargo, npm, binary, name, version) { + checkBinary(binary) + const rust = namespace(cargo, 'cargo') + const javascript = namespace(npm, 'npm') + const root = rust.metadata.component + const npmRoot = javascript.metadata.component + const dependencies = [ + ...(rust.dependencies ?? []), + ...(javascript.dependencies ?? []), + ] + let edge = dependencies.find((entry) => entry.ref === root['bom-ref']) + if (!edge) dependencies.push((edge = { ref: root['bom-ref'], dependsOn: [] })) + edge.dependsOn = [...(edge.dependsOn ?? []), npmRoot['bom-ref']] + return { + bomFormat: 'CycloneDX', + specVersion: '1.5', + version: 1, + metadata: { + ...rust.metadata, + component: { + ...root, + name, + version: version ?? root.version, + hashes: [ + { + alg: 'SHA-256', + content: createHash('sha256').update(binary).digest('hex'), + }, + ], + }, + properties: [ + { + name: 'trusted-server:coverage', + value: + 'Selected Cargo packages plus the installed npm build tree used for this release build. Conservative inventory: components may be removed by linking, tree-shaking or minification. External Prebid is not bundled; its installed package is a build-tree entry only. SPDX conversion does not retain dependency relationships. Native C/assembly, system libraries, Rust standard library and compiler runtimes are not completely inventoried. Complete composition is Phase 2.', + }, + ], + }, + components: [ + ...(rust.components ?? []), + ...(root.components ?? []), + npmRoot, + ...(javascript.components ?? []), + ], + dependencies, + } +} + +function audit(root) { + for (const entry of readdirSync(root, { withFileTypes: true })) { + if (['node_modules', 'target', 'dist', '.git'].includes(entry.name)) + continue + const file = join(root, entry.name) + if (entry.isDirectory()) audit(file) + else if (entry.isFile() && file.endsWith('.rs')) + checkSource(file, readFileSync(file, 'utf8')) + } +} + +async function smokeRelease(file, target) { + const elf = target.includes('-linux-') + ? execFileSync( + 'readelf', + ['--program-headers', '--dynamic', '--version-info', file], + { encoding: 'utf8', maxBuffer: 8388608 } + ) + : '' + checkReleaseFormat(readFileSync(file), target, elf) + console.log( + 'SUCCESS: final-byte checks', + target, + [...new Set(elf.match(/GLIBC_[\w.]+/g) ?? [])].join(' ') + ) + if (target === 'wasm32-wasip1') return + const cli = file.split('/').at(-1).startsWith('ts-') + const expected = + 'ts package=' + + readFileSync('Cargo.toml', 'utf8').match( + /\[workspace\.package\][\s\S]*?^version = "([^"]+)"/m + )[1] + + ` source=${process.env.SOURCE_COMMIT} target=${target} variant=default` + const version = (output) => { + if (output.trim() !== expected) throw Error('Unexpected CLI version') + console.log('SUCCESS:', output.trim()) + } + if (cli) + version( + execFileSync(resolve(file), ['--version'], { + encoding: 'utf8', + timeout: 10000, + }) + ) + if (!target.includes('-linux-')) return + const docker = (args, input) => + execFileSync('docker', args, { + input, + encoding: 'utf8', + timeout: args[0] === 'pull' ? 180000 : 10000, + maxBuffer: 8388608, + }) + const images = target.endsWith('-musl') + ? [ + 'alpine:3.22.2@sha256:4b7ce07002c69e8f3d704a9c5d6fd3053be500b7f1c69fc0d80990c2ad8dd412', + 'scratch', + ] + : target.startsWith('aarch64-') + ? [ + 'debian:12-slim@sha256:7ff6ba31527b9e336162fa061746c366c4b2032f4d2cff2078bea43f78efe3f8', + ] + : [ + 'debian:12-slim@sha256:7ff6ba31527b9e336162fa061746c366c4b2032f4d2cff2078bea43f78efe3f8', + 'ubuntu:22.04@sha256:0a9887931b0e38af2e3487ed39e29436b0535c4f3f52e57745baaf79d1898956', + 'registry.access.redhat.com/ubi9/ubi-minimal:9.6@sha256:34880b64c07f28f64d95737f82f891516de9a3b43583f39970f7bf8e4cfa48b7', + ] + for (const reference of images) { + let image + if (reference === 'scratch') + image = docker(['import', '-'], Buffer.alloc(1024)).trim() + else { + docker(['pull', reference]) + image = docker([ + 'image', + 'inspect', + '--format', + '{{.Id}}', + reference, + ]).trim() + } + console.log('Runtime image:', reference, image) + const restrictions = [ + '--read-only', + '--cap-drop=ALL', + '--security-opt=no-new-privileges', + '--user=65534:65534', + '--entrypoint=/release', + '--mount', + `type=bind,src=${resolve(file)},dst=/release,readonly`, + ] + if (!cli) + restrictions.push( + '--mount', + 'type=bind,src=/etc/ssl/certs/ca-certificates.crt,dst=/ca.pem,readonly', + '--env=SSL_CERT_FILE=/ca.pem' + ) + const container = 'phase1-smoke-' + randomUUID() + let failed + try { + if (cli) { + version( + docker([ + 'run', + '--name', + container, + '--network=none', + ...restrictions, + image, + '--version', + ]) + ) + continue + } + for (const setting of ['EDGEZERO__ADAPTER__PORT', 'PORT']) { + let rejected = false + try { + docker([ + 'run', + '--rm', + '--network=none', + ...restrictions, + '--env', + setting + '=0', + image, + ]) + } catch (error) { + if ( + error.status !== 1 || + !(String(error.stdout ?? '') + String(error.stderr ?? '')).includes( + '1–65535' + ) + ) + throw error + rejected = true + } + if (!rejected) throw Error('Axum accepted port zero: ' + setting) + console.log( + 'SUCCESS: Axum rejects ' + + setting + + '=0; exit 1, ports must be 1–65535:', + image + ) + } + const reservation = createServer().listen(0, '127.0.0.1') + await once(reservation, 'listening') + const port = reservation.address().port + await new Promise((resolve) => reservation.close(resolve)) + // Fixed ASCII fixture keys are sorted for the envelope's canonical hash. + const data = { + ec: { passphrase: 'ec_passphrase' }, + handlers: [ + { + password: 'handler_password', + path: '^/_ts/admin', + username: 'smoke', + }, + ], + publisher: { + cookie_domain: '127.0.0.1', + domain: '127.0.0.1', + origin_url: 'http://127.0.0.1:9', + proxy_secret: 'publisher_proxy_secret', + }, + } + const envelope = { + data, + generated_at: '2026-01-01T00:00:00Z', + sha256: createHash('sha256').update(JSON.stringify(data)).digest('hex'), + version: 1, + } + const environment = [ + 'EDGEZERO__ADAPTER__PORT=8080', + 'EDGEZERO__ADAPTER__HOST=0.0.0.0', + `TRUSTED_SERVER_CONFIG_TRUSTED_SERVER_CONFIG_TRUSTED_SERVER_CONFIG=${JSON.stringify(envelope)}`, + 'TRUSTED_SERVER_SECRET_TRUSTED_SERVER_SECRETS_EC_PASSPHRASE=smoke-edge-cookie-passphrase-32-bytes', + 'TRUSTED_SERVER_SECRET_TRUSTED_SERVER_SECRETS_HANDLER_PASSWORD=smoke-admin-password-32-bytes-ok', + 'TRUSTED_SERVER_SECRET_TRUSTED_SERVER_SECRETS_PUBLISHER_PROXY_SECRET=smoke-publisher-proxy-secret-32-bytes', + ] + docker([ + 'run', + '--name', + container, + '-d', + '--publish', + `127.0.0.1:${port}:8080`, + ...restrictions, + ...environment.flatMap((value) => ['--env', value]), + image, + ]) + let healthy = false + for (let attempt = 0; attempt < 30; attempt++) { + if ( + docker([ + 'inspect', + '--format', + '{{.State.Running}}', + container, + ]).trim() !== 'true' + ) + throw Error('Axum exited before readiness') + let response + try { + response = await fetch(`http://127.0.0.1:${port}/health`, { + signal: AbortSignal.timeout(1000), + }) + } catch { + await new Promise((resolve) => setTimeout(resolve, 100)) + continue + } + const body = await response.text() + if (response.status !== 200 || body !== 'ok') + throw Error( + `Axum health mismatch: HTTP ${response.status}: ${body.slice(0, 500)}` + ) + healthy = true + break + } + if (!healthy) throw Error('Axum startup deadline exhausted') + console.log( + 'SUCCESS: Axum bound to 0.0.0.0:8080; host reached published container port, HTTP 200 ok:', + reference, + image + ) + } catch (error) { + failed = error + try { + console.error(docker(['logs', container]).slice(-4000)) + } catch {} + throw error + } finally { + try { + docker(['rm', '-f', container]) + } catch (error) { + if ( + !error.stderr?.toString().includes(`No such container: ${container}`) + ) { + if (!failed) throw error + console.error('Smoke container cleanup failed:', error.message) + } + } + } + } +} + +if ( + process.argv[1] && + import.meta.url === pathToFileURL(resolve(process.argv[1])).href +) { + const [command, ...args] = process.argv.slice(2) + if (command === 'audit' && args.length === 0) { + audit('crates') + } else if (command === 'check' && args.length > 0) { + for (const file of args) { + if (!statSync(file).isFile()) + throw new Error('Expected regular release binary') + checkBinary(readFileSync(file)) + } + } else if (command === 'smoke' && args.length === 2) { + await smokeRelease(...args) + } else if (command === 'merge' && args.length === 4) { + const [cargo, npm, binary, output] = args + writeFileSync( + output, + JSON.stringify( + mergeSboms( + JSON.parse(readFileSync(cargo)), + JSON.parse(readFileSync(npm)), + readFileSync(binary), + binary.split('/').at(-1), + process.env.RELEASE_VERSION + ), + null, + 2 + ) + '\n' + ) + } else if (command === 'validate-spdx' && args.length === 3) { + const [file, binary, original] = args + const document = JSON.parse(readFileSync(file)) + const inventory = JSON.parse(readFileSync(original)) + completeSpdx(document, readFileSync(binary), inventory) + document.creationInfo.creators.push( + `Tool: npm-${execFileSync('npm', ['--version'], { encoding: 'utf8' }).trim()}` + ) + const coverage = inventory.metadata.properties?.find( + (entry) => entry.name === 'trusted-server:coverage' + )?.value + if (typeof coverage !== 'string' || coverage.length === 0) + throw new Error('Missing conservative inventory coverage') + document.creationInfo.comment = [document.creationInfo.comment, coverage] + .filter(Boolean) + .join('\n') + writeFileSync(file, JSON.stringify(document, null, 2) + '\n') + } else { + throw new Error( + 'Expected audit, check , merge , or validate-spdx ' + ) + } +} diff --git a/scripts/release/phase1.test.mjs b/scripts/release/phase1.test.mjs new file mode 100644 index 0000000000..e53bedbea3 --- /dev/null +++ b/scripts/release/phase1.test.mjs @@ -0,0 +1,1060 @@ +import assert from 'node:assert/strict' +import { createHash } from 'node:crypto' +import { execFileSync } from 'node:child_process' +import { completeSpdx, validateSpdx, checkReleaseFormat } from './phase1.mjs' +import { test } from 'node:test' +import { + readFileSync, + mkdtempSync, + mkdirSync, + writeFileSync, + rmSync, +} from 'node:fs' +import { join } from 'node:path' +import { tmpdir } from 'node:os' +import * as fs from 'node:fs' +import vm from 'node:vm' +import { isDeepStrictEqual } from 'node:util' +import { checkBinary, checkSource, mergeSboms, sentinels } from './phase1.mjs' +test('rejects every planted operator value, including inside binary data', () => { + for (const marker of sentinels) { + assert.throws( + () => + checkBinary( + Buffer.concat([ + Buffer.from([0]), + Buffer.from(marker), + Buffer.from([255]), + ]) + ), + /operator configuration/ + ) + } + assert.doesNotThrow(() => + checkBinary(Buffer.from('fictional clean executable')) + ) + assert.throws(() => checkBinary(Buffer.alloc(0)), /empty/) +}) +test('rejects deployment compile inputs while permitting product inputs', () => { + for (const source of [ + 'env!("TRUSTED_SERVER__EC__PASSPHRASE")', + 'option_env!("EDGEZERO__STORES__CONFIG__NAME")', + 'include_str!("../../trusted-server.toml")', + 'include_bytes!(".env.production")', + ]) { + assert.throws(() => checkSource('src/lib.rs', source), /compile-time/) + } + assert.throws( + () => checkSource('build.rs', 'fs::read("trusted-server.toml")'), + /compile-time/ + ) + assert.doesNotThrow(() => + checkSource( + 'src/lib.rs', + 'option_env!("TRUSTED_SERVER__GIT_VERSION"); include_str!("trusted-server.example.toml")' + ) + ) +}) +test('merges independent graphs without colliding references and binds binary bytes', () => { + const bom = (name) => ({ + bomFormat: 'CycloneDX', + specVersion: '1.5', + version: 1, + metadata: { component: { type: 'application', name, 'bom-ref': 'root' } }, + components: [ + { type: 'library', name: `${name}-dependency`, 'bom-ref': 'dependency' }, + ], + dependencies: [ + { ref: 'root', dependsOn: ['dependency'] }, + { ref: 'dependency', dependsOn: [] }, + ], + }) + const output = mergeSboms( + bom('cargo'), + bom('npm'), + Buffer.from('binary'), + 'example.wasm' + ) + assert.equal(output.metadata.component.name, 'example.wasm') + assert.equal(output.metadata.component.hashes[0].alg, 'SHA-256') + assert.equal(output.metadata.component.hashes[0].content.length, 64) + assert.deepEqual( + output.dependencies.find( + (d) => d.ref === output.metadata.component['bom-ref'] + ).dependsOn, + [output.components[0]['bom-ref'], output.components[1]['bom-ref']] + ) + assert.equal( + new Set(output.components.map((c) => c['bom-ref'])).size, + output.components.length + ) + assert.ok( + output.components.every((entry) => + /^[A-Za-z0-9.-]+$/.test(entry['bom-ref']) + ) + ) + assert.throws( + () => mergeSboms({}, bom('npm'), Buffer.from('binary'), 'example'), + /CycloneDX/ + ) +}) +test('workflow signs only tags, separates privileged jobs, and never checks out in signing', () => { + const workflow = readFileSync( + new URL('../../.github/workflows/release-build.yml', import.meta.url), + 'utf8' + ) + const sign = workflow.split('\n sign:')[1]?.split('\n release:')[0] + assert.ok(sign, 'should define the isolated signing job') + assert.match(sign, /github.event_name == 'push'/) + assert.doesNotMatch( + sign, + /checkout@|cargo |npm |contents: write|artifact-metadata:/ + ) + assert.equal((sign.match(/uses: actions\/attest@/g) ?? []).length, 2) + assert.match( + readFileSync('.github/workflows/release.yml', 'utf8'), + /cancel-in-progress:.*pull_request/ + ) + assert.doesNotMatch(workflow, /workflow_dispatch|--clobber/) + for (const line of workflow + .split('\n') + .filter((line) => /uses: .*@/.test(line))) { + assert.match(line, /@[a-f0-9]{40} # v/) + } +}) +const assets = [ + 'trusted-server-axum-x86_64-unknown-linux-gnu', + 'trusted-server-axum-x86_64-unknown-linux-musl', + 'trusted-server-axum-aarch64-unknown-linux-gnu', + + 'trusted-server-fastly.wasm', + 'ts-aarch64-apple-darwin', + 'ts-x86_64-apple-darwin', + 'ts-x86_64-unknown-linux-gnu', +] +const hash = (bytes) => createHash('sha256').update(bytes).digest('hex') +function publicationScript() { + const workflow = readFileSync('.github/workflows/release.yml', 'utf8') + const match = workflow.match( + / \/\/ BEGIN PUBLICATION\n([\s\S]*?) \/\/ END PUBLICATION/ + ) + assert.ok( + match, + 'should expose the actual inline publication program for failure tests' + ) + return match[1] + .split('\n') + .map((line) => line.slice(10)) + .join('\n') +} +test('publication binds exact signing handoffs and refuses drafts, corruption and partial uploads', async () => { + const script = publicationScript() + for (const mode of [ + 'success', + 'idle-socket', + 'stable', + 'download-corrupt', + 'download-missing', + 'download-extra', + 'download-manifest', + 'download-transient', + 'download-never', + 'publication-disabled', + 'prerelease-mismatch', + 'post-read-error', + 'draft', + 'api-error', + 'missing', + 'extra', + 'changed', + 'empty-bundle', + 'upload-error', + 'mutable', + 'wrong-id', + 'attestation-404-then-present', + 'attestation-after-two-waits', + 'attestation-404-never', + 'attestation-non-200', + 'attestation-201', + 'attestation-auth-error', + 'attestation-malformed', + 'attestation-wrong-tag-shape', + 'attestation-wrong-id', + 'attestation-wrong-repository', + 'attestation-wrong-source', + 'attestation-wrong-assets', + 'attestation-missing-asset', + 'attestation-extra-asset', + 'attestation-duplicate', + 'attestation-other-tag', + 'attestation-other-tag-before-current', + 'attestation-other-malformed', + 'attestation-other-unverifiable', + 'attestation-other-transient', + 'attestation-substituted-bundle', + 'attestation-verification-failure', + ]) { + const root = mkdtempSync(join(tmpdir(), 'phase1-publish-')) + const calls = [] + const uploaded = [] + const stable = ['stable', 'attestation-other-tag'].includes(mode) + const waits = [], + ignored = [] + let downloads = 0, + otherReads = 0, + now = 0 + let polls = 0, + observed = [] + try { + for (const asset of assets) { + const build = join(root, 'build', asset) + const signed = join(root, 'signed', asset) + mkdirSync(build, { recursive: true }) + mkdirSync(signed, { recursive: true }) + const entries = [] + for (const suffix of [ + '', + '.spdx.json', + '.provenance.sigstore.json', + '.sbom.sigstore.json', + ]) { + const bytes = Buffer.from(`fixture ${asset}${suffix}`) + writeFileSync( + join(suffix.includes('sigstore') ? signed : build, asset + suffix), + bytes + ) + entries.push(`${hash(bytes)} ${asset}${suffix}\n`) + } + writeFileSync(join(signed, 'SHA256SUMS'), entries.join('')) + } + if (mode === 'missing') + fs.unlinkSync(join(root, 'build', assets[0], assets[0])) + if (mode === 'extra') + writeFileSync(join(root, 'build', assets[0], 'unexpected'), 'extra') + if (mode === 'changed') + writeFileSync(join(root, 'build', assets[0], assets[0]), 'different') + if (mode === 'empty-bundle') + writeFileSync( + join(root, 'signed', assets[0], assets[0] + '.sbom.sigstore.json'), + '' + ) + const release = (draft) => ({ + id: 42, + tag_name: stable ? 'v1.0.0' : 'v0.0.0-example', + prerelease: !stable && mode !== 'prerelease-mismatch', + draft, + immutable: mode !== 'mutable', + assets: uploaded, + }) + const attestation = (tag = release(false).tag_name) => { + const statement = { + _type: 'https://in-toto.io/Statement/v1', + predicateType: 'https://in-toto.io/attestation/release/v0.2', + predicate: { databaseId: '42', repository: 'example/project', tag }, + subject: [ + { + uri: `pkg:github/example/project@${tag}`, + digest: { sha1: 'a'.repeat(40) }, + }, + ...uploaded.map((a) => ({ + name: a.name, + digest: { sha256: a.digest.slice(7) }, + })), + ], + } + if (mode === 'attestation-wrong-id') + statement.predicate.databaseId = '43' + if (mode === 'attestation-wrong-repository') + statement.predicate.repository = 'example/other' + if (mode === 'attestation-wrong-source') + statement.subject[0].digest.sha1 = 'b'.repeat(40) + if (mode === 'attestation-wrong-assets') + statement.subject[1].digest.sha256 = '0'.repeat(64) + if (mode === 'attestation-missing-asset') statement.subject.pop() + if (mode === 'attestation-extra-asset') + statement.subject.push({ + name: 'unexpected', + digest: { sha256: '0'.repeat(64) }, + }) + if (mode === 'attestation-wrong-tag-shape') statement.predicate.tag = 42 + return { + initiator: 'github', + bundle: { + mediaType: 'application/vnd.dev.sigstore.bundle.v0.3+json', + verificationMaterial: {}, + dsseEnvelope: { + payloadType: 'application/vnd.in-toto+json', + payload: Buffer.from(JSON.stringify(statement)).toString( + 'base64' + ), + signatures: [{ sig: 'fixture' }], + }, + }, + } + } + const fetch = async (url, options = {}) => { + const u = new URL(url) + const method = options.method ?? 'GET' + if ( + calls.some(([kind]) => kind === 'gh') && + (mode === 'post-read-error' || + (mode === 'idle-socket' && options.headers.Connection !== 'close')) + ) + throw new TypeError('other side closed') + calls.push([method, u.pathname]) + const response = (body, status = 200) => + new Response( + typeof body === 'string' || Buffer.isBuffer(body) + ? body + : JSON.stringify(body), + { status } + ) + if (u.pathname.includes('/attestations/')) { + polls++ + assert.equal(method, 'GET') + assert.equal(u.searchParams.get('predicate_type'), 'release') + if (mode === 'attestation-non-200') return response({}, 503) + if (mode === 'attestation-201') return response({}, 201) + if (mode === 'attestation-auth-error') return response({}, 403) + if (mode === 'attestation-malformed') + return response({ attestations: null }) + const other = attestation('v1.0.0-rc.1') + if (mode === 'attestation-other-malformed') + other.bundle.dsseEnvelope.payload = '!invalid!' + const delay = + mode === 'attestation-404-then-present' + ? 1 + : [ + 'attestation-after-two-waits', + 'attestation-other-tag-before-current', + ].includes(mode) + ? 2 + : mode === 'attestation-404-never' + ? 5 + : 0 + if (polls <= delay && mode !== 'attestation-other-tag-before-current') + return response({ message: 'Not Found' }, 404) + observed = polls <= delay ? [] : [attestation()] + if (mode === 'attestation-duplicate') observed.push(attestation()) + if (mode.startsWith('attestation-other-')) observed.unshift(other) + return response({ attestations: observed }) + } + if (u.pathname.endsWith('/releases') && method === 'GET') + return mode === 'api-error' + ? response('denied', 503) + : response(mode === 'draft' ? [release(true)] : []) + if (u.pathname.endsWith('/releases') && method === 'POST') { + const creation = JSON.parse(options.body) + assert.equal(creation.prerelease, !stable) + assert.equal(creation.make_latest, stable ? 'legacy' : 'false') + const body = creation.body + assert.match(body, /--signer-workflow.*release-build\.yml/) + assert.match(body, /--cert-identity.*release-build\.yml@refs\/tags/) + assert.ok(body.includes('COMMIT=' + 'a'.repeat(40))) + for (const asset of assets) assert.ok(body.includes(asset)) + const normalize = (code) => + code.replace(/^(REPO|TAG|COMMIT|ASSETS)=.*$/gm, '$1=').trim() + const guide = readFileSync( + 'docs/guide/release-verification.md', + 'utf8' + ).match( + /\n\n```bash\n([\s\S]*?)\n```\n\n/ + ) + assert.ok(guide, 'should mark the canonical verification block') + assert.equal( + normalize(body.match(/```bash\n([\s\S]*?)\n```/)[1]), + normalize(guide[1]), + 'release commands should match the guide' + ) + execFileSync('bash', ['-n'], { + input: body.match(/```bash\n([\s\S]*?)\n```/)[1], + }) + return response(release(true), 201) + } + if (u.pathname.endsWith('/assets') && method === 'POST') { + if (mode === 'upload-error') return response('upload failed', 500) + const bytes = Buffer.from(options.body) + const name = u.searchParams.get('name') + const item = { + id: 100 + uploaded.length, + name, + size: bytes.length, + state: 'uploaded', + digest: `sha256:${hash(bytes)}`, + bytes, + } + uploaded.push(item) + return response(item, 201) + } + if (u.pathname.includes('/releases/tags/')) + return response({ + ...release(false), + id: mode === 'wrong-id' ? 43 : 42, + }) + if (u.pathname.endsWith('/releases/42')) + return response(release(method !== 'PATCH' && uploaded.length === 0)) + throw Error(`Unexpected ${method} ${url}`) + } + const context = { + Buffer, + Date: { now: () => now, parse: Date.parse }, + JSON, + AbortSignal, + URL, + console: { + log: (...args) => { + if ( + args[0] === 'Ignored verified release attestation for other tag:' + ) + ignored.push(args[1]) + }, + warn: () => {}, + }, + setTimeout: (resolve, delay) => { + waits.push(delay) + now += delay + resolve() + }, + fetch, + process: { + env: { + GH_REPO: 'example/project', + RELEASE_TAG: stable ? 'v1.0.0' : 'v0.0.0-example', + RELEASE_PUBLICATION_ENABLED: + mode === 'publication-disabled' ? 'false' : 'true', + SOURCE_COMMIT: 'a'.repeat(40), + }, + }, + require(name) { + if (name === 'node:fs') + return { + ...fs, + mkdtempSync: (prefix) => + fs.mkdtempSync(join(root, prefix)).slice(root.length + 1), + rmSync: (path, options) => fs.rmSync(join(root, path), options), + readdirSync: (path, ...rest) => + fs.readdirSync(join(root, path), ...rest), + lstatSync: (path) => fs.lstatSync(join(root, path)), + readFileSync: (path, ...rest) => + fs.readFileSync(join(root, path), ...rest), + mkdirSync: (path, ...rest) => + fs.mkdirSync(join(root, path), ...rest), + writeFileSync: (path, ...rest) => + fs.writeFileSync(join(root, path), ...rest), + copyFileSync: (a, b) => + fs.copyFileSync(join(root, a), join(root, b)), + } + if (name === 'node:crypto') return { createHash } + if (name === 'node:util') return { isDeepStrictEqual } + if (name === 'node:child_process') + return { + execFileSync: (...args) => { + calls.push(['gh', args]) + if (args[1][1] === 'download') { + downloads++ + assert.ok( + args[2].timeout > 10000 && args[2].timeout <= 60000, + 'should allow bulk transfers within the shared deadline' + ) + const dir = args[1][args[1].indexOf('--dir') + 1] + if ( + mode === 'download-never' || + (mode === 'download-transient' && downloads === 1) + ) + throw Object.assign(Error('timeout'), { code: 'ETIMEDOUT' }) + for (const item of uploaded) + fs.writeFileSync(join(root, dir, item.name), item.bytes) + if (mode === 'download-corrupt') + fs.writeFileSync(join(root, dir, assets[0]), 'corrupt') + if (mode === 'download-manifest') + fs.writeFileSync(join(root, dir, 'SHA256SUMS'), 'corrupt') + if (mode === 'download-missing') + fs.unlinkSync(join(root, dir, assets[0])) + if (mode === 'download-extra') + fs.writeFileSync(join(root, dir, 'extra'), 'extra') + return Buffer.from('downloaded') + } + if (args[1][1] !== 'verify') + return Buffer.from('verified asset') + const tag = args[1].at(-1) + if ( + mode === 'attestation-other-transient' && + tag === 'v1.0.0-rc.1' && + ++otherReads === 1 + ) + throw Object.assign(Error('timeout'), { code: 'ETIMEDOUT' }) + if ( + mode === 'attestation-verification-failure' || + (mode === 'attestation-other-unverifiable' && + tag === 'v1.0.0-rc.1') + ) + throw Error('Signature verification failed') + assert.ok( + polls > 0, + 'should read release-attestation availability before verification' + ) + const record = observed.find( + (r) => + JSON.parse( + Buffer.from(r.bundle.dsseEnvelope.payload, 'base64') + ).predicate.tag === tag + ) + const bundle = structuredClone(record.bundle) + if (mode === 'attestation-substituted-bundle') + bundle.dsseEnvelope.signatures[0].sig = 'different' + return Buffer.from(JSON.stringify({ attestation: { bundle } })) + }, + } + throw Error(name) + }, + } + const execute = () => + vm.runInNewContext(`(async()=>{${script}\n})()`, context) + const successful = [ + 'success', + 'idle-socket', + 'stable', + 'download-transient', + 'attestation-other-transient', + 'attestation-404-then-present', + 'attestation-after-two-waits', + 'attestation-other-tag', + 'attestation-other-tag-before-current', + ].includes(mode) + if (successful) await execute() + else + await assert.rejects( + execute, + mode === 'attestation-404-never' + ? /unavailable after five checks/ + : mode.startsWith('download-') && mode !== 'download-never' + ? /mismatch|Unexpected artifact contents|ENOENT/ + : undefined, + mode + ) + if ( + [ + 'draft', + 'publication-disabled', + 'api-error', + 'missing', + 'extra', + 'changed', + 'empty-bundle', + ].includes(mode) + ) + assert.ok(!calls.some(([method]) => method === 'POST'), mode) + if (mode === 'api-error' || mode === 'upload-error') + assert.equal(calls.length, mode === 'api-error' ? 1 : 3) + if (['upload-error'].includes(mode)) + assert.ok(!calls.some(([method]) => method === 'PATCH'), mode) + if (successful) { + assert.equal(uploaded.length, 29) + assert.equal( + calls.filter( + ([method, args]) => method === 'gh' && args[1][1] !== 'download' + ).length, + mode === 'attestation-other-transient' + ? 32 + : mode === 'attestation-other-tag' + ? 31 + : mode === 'attestation-other-tag-before-current' + ? 33 + : 30 + ) + } + assert.equal( + fs + .readdirSync(root) + .some((name) => name.startsWith('published-download-')), + false, + 'should clean every download attempt' + ) + if (mode.startsWith('download-')) { + assert.equal( + downloads, + mode === 'download-transient' ? 2 : mode === 'download-never' ? 3 : 1 + ) + assert.deepEqual( + waits, + mode === 'download-transient' + ? [2000] + : mode === 'download-never' + ? [2000, 5000] + : [] + ) + } + if (mode.startsWith('attestation-')) { + const expectedWaits = + mode === 'attestation-404-never' + ? [2000, 5000, 10000, 20000] + : [ + 'attestation-404-then-present', + 'attestation-other-transient', + ].includes(mode) + ? [2000] + : [ + 'attestation-after-two-waits', + 'attestation-other-tag-before-current', + ].includes(mode) + ? [2000, 5000] + : [] + assert.deepEqual(waits, expectedWaits, mode) + assert.equal( + polls, + mode === 'attestation-other-transient' ? 1 : expectedWaits.length + 1, + mode + ) + assert.equal( + calls.filter( + ([method, path]) => method === 'POST' && path.endsWith('/releases') + ).length, + 1, + mode + ) + assert.equal( + calls.filter( + ([method, path]) => method === 'POST' && path.endsWith('/assets') + ).length, + 29, + mode + ) + assert.equal( + calls.filter(([method]) => method === 'PATCH').length, + 1, + mode + ) + assert.deepEqual( + ignored, + ['attestation-other-tag', 'attestation-other-transient'].includes( + mode + ) + ? ['"v1.0.0-rc.1"'] + : mode === 'attestation-other-tag-before-current' + ? Array(3).fill('"v1.0.0-rc.1"') + : [], + mode + ) + } + } finally { + rmSync(root, { recursive: true, force: true }) + } + } +}) +test('acquires the locked workspace graph before offline SBOM resolution', () => { + const script = readFileSync( + new URL('./phase1-build.sh', import.meta.url), + 'utf8' + ) + const acquisition = script.indexOf( + 'cargo metadata --locked --format-version 1 --filter-platform' + ) + assert.ok( + acquisition >= 0, + 'should populate the locked workspace dependency sources' + ) + assert.ok( + acquisition < script.indexOf('CARGO_NET_OFFLINE=true'), + 'should acquire before offline generation' + ) +}) +// SPDX conversion retains the binary root and conservative package inventory. +const binary = Buffer.from('fictional binary') +function spdxFixture() { + const root = { + SPDXID: 'SPDXRef-Root', + name: 'example.wasm', + checksums: [ + { + algorithm: 'SHA256', + checksumValue: createHash('sha256').update(binary).digest('hex'), + }, + ], + } + const component = { + name: 'example-package', + version: '1.0.0', + purl: 'pkg:npm/example-package@1.0.0', + } + const original = { + metadata: { component: { name: root.name } }, + components: [component], + } + const document = { + spdxVersion: 'SPDX-2.3', + SPDXID: 'SPDXRef-DOCUMENT', + documentDescribes: [root.SPDXID], + packages: [ + root, + { + SPDXID: 'SPDXRef-Package', + name: component.name, + versionInfo: component.version, + externalRefs: [ + { referenceType: 'purl', referenceLocator: component.purl }, + ], + }, + ], + relationships: [ + { + spdxElementId: 'SPDXRef-DOCUMENT', + relationshipType: 'DESCRIBES', + relatedSpdxElement: root.SPDXID, + }, + ], + } + return { original, document } +} +test('SPDX preserves a binary-bound root and package versions/PURLs', () => { + const { original, document } = spdxFixture() + assert.equal(validateSpdx(document, binary, original), document) + delete document.documentDescribes + assert.doesNotThrow(() => validateSpdx(document, binary, original)) +}) +test('SPDX rejects root/hash ambiguity and lost package identities', () => { + for (const mutate of [ + (d) => { + d.spdxVersion = 'SPDX-2.2' + }, + (d) => { + d.documentDescribes.push('SPDXRef-Package') + }, + (d) => { + d.packages[0].checksums[0].checksumValue = '0'.repeat(64) + }, + (d) => { + d.packages[0].checksums.push(d.packages[0].checksums[0]) + }, + (d) => { + d.packages.push(d.packages[0]) + }, + (d) => { + d.packages.pop() + }, + (d) => { + d.packages[1].versionInfo = '2.0.0' + }, + (d) => { + d.packages[1].externalRefs = [] + }, + ]) { + const { document, original } = spdxFixture() + mutate(document) + assert.throws(() => validateSpdx(document, binary, original)) + } +}) +test('completes real converter package output with a binary-bound root', () => { + const { original, document } = spdxFixture() + document.packages.shift() + delete document.documentDescribes + delete document.relationships + completeSpdx(document, binary, original) + assert.deepEqual(document.documentDescribes, ['SPDXRef-Artifact']) + assert.equal(document.packages.at(-1).filesAnalyzed, false) + assert.throws(() => completeSpdx(document, binary, original), /Unexpected/) +}) +test('post-publish retries distinguish transient failures and cap Retry-After', async () => { + const code = publicationScript() + .split('// BEGIN READ RETRIES')[1] + ?.split('// END READ RETRIES')[0] + assert.ok(code, 'should exercise the actual post-publish retry helper') + for (const [error, failures, expected] of [ + [{ cause: { code: 'UND_ERR_SOCKET' } }, 1, [2000]], + [{ status: 503 }, 9, [2000, 5000]], + [{ status: 503, elapsed: 180000 }, 9, []], + [{ status: 429, retryAfter: '30' }, 1, [10000]], + [{ status: 429, retryAfter: '3' }, 1, [3000]], + [{ status: 403 }, 9, []], + [{ name: 'TimeoutError' }, 1, [2000]], + [{ status: 429, retryAfter: 'Thu, 01 Jan 1970 00:00:03 GMT' }, 1, [3000]], + [new Error('Published asset digest mismatch'), 9, []], + [{ stderr: Buffer.from('HTTP 503: Service Unavailable') }, 1, [2000]], + [{ stderr: Buffer.from('verification failed: HTTP 503') }, 9, []], + ]) { + const waits = [], + logs = [] + let attempts = 0, + now = 0 + const retry = vm.runInNewContext(`${code}; retryRead`, { + Date: { now: () => now, parse: Date.parse }, + setTimeout: (resolve, ms) => { + waits.push(ms) + now += ms + resolve() + }, + console: { warn: (line) => logs.push(line) }, + }) + const operation = () => { + now += error.elapsed ?? 0 + if (++attempts <= failures) throw error + return 'verified' + } + if (failures === 1) assert.equal(await retry(operation), 'verified') + else await assert.rejects(() => retry(operation)) + assert.deepEqual(waits, expected) + assert.equal(attempts, expected.length + 1) + assert.equal(logs.length, expected.length) + assert.ok(logs.every((line) => /attempt [23]\/3/.test(line))) + } +}) + +test('rejects invalid wasm, GNU symbols above 2.34 and dynamic musl', () => { + const wasm = Buffer.from('0061736d01000000', 'hex') + checkReleaseFormat(wasm, 'wasm32-wasip1') + for (const bytes of [ + wasm.subarray(0, 7), + Buffer.alloc(8), + Buffer.from('0061736d02000000', 'hex'), + ]) + assert.throws(() => checkReleaseFormat(bytes, 'wasm32-wasip1'), /wasm/) + const elf = Buffer.from('7f454c46', 'hex') + const gnu = 'x86_64-unknown-linux-gnu', + musl = 'x86_64-unknown-linux-musl' + checkReleaseFormat(elf, gnu, 'GLIBC_2.2.5 GLIBC_2.34') + for (const versions of [ + 'GLIBC_2.35', + 'GLIBC_3.0', + 'GLIBC_2.34.1', + 'GLIBC_ABI_DT_RELR', + ]) + assert.throws(() => checkReleaseFormat(elf, gnu, versions), /GLIBC/) + checkReleaseFormat(elf, musl, 'There is no dynamic section') + for (const dynamic of ['INTERP', '(NEEDED) libgcc_s.so.1']) + assert.throws(() => checkReleaseFormat(elf, musl, dynamic), /static/) + assert.throws(() => checkReleaseFormat(wasm, gnu, 'GLIBC_2.34'), /ELF/) +}) +test('audit resolves raw and nested concatenated deployment inputs', () => { + for (const source of [ + 'include_str!(concat!("trusted-", "server.toml"))', + 'include_bytes!(concat!(".en", r#"v.production"#))', + 'env!(r#"TRUSTED_SERVER__EC__PASSPHRASE"#)', + 'option_env!(concat!("EDGEZERO__", concat!("STORE", "S")))', + ]) + assert.throws(() => checkSource('src/lib.rs', source), /compile-time/) + for (const source of [ + 'std::env::var("TRUSTED_SERVER__EC__PASSPHRASE")', + 'std::env::var_os(concat!("EDGEZERO__", "STORE"))', + 'const SECRET: &str = r#"TRUSTED_SERVER__SECRET"#; env::var(SECRET)', + ]) + assert.throws(() => checkSource('build.rs', source), /compile-time/) + for (const source of [ + 'include_str!(concat!(env!("OUT_DIR"), "/generated.rs"))', + 'include_str!("trusted-server.example.toml")', + '// env!("EDGEZERO__SECRET")\nlet product = "literal";', + '/* include_str!("trusted-server.toml") */', + ]) + assert.doesNotThrow(() => checkSource('src/lib.rs', source)) + assert.doesNotThrow(() => + checkSource('build.rs', 'env::var("TRUSTED_SERVER__GIT_VERSION")') + ) + assert.doesNotThrow(() => + checkSource('src/lib.rs', 'std::env::var("EDGEZERO__ADAPTER__HOST")') + ) +}) +test('rejects non-Mach-O, wrong CPU and newer or absent macOS floor', () => { + const macho = (cpu = 0x0100000c, minimum = 0x000d0000) => { + const b = Buffer.alloc(56) + b.writeUInt32LE(0xfeedfacf, 0) + b.writeUInt32LE(cpu, 4) + b.writeUInt32LE(1, 16) + b.writeUInt32LE(24, 20) + b.writeUInt32LE(0x32, 32) + b.writeUInt32LE(24, 36) + b.writeUInt32LE(1, 40) + b.writeUInt32LE(minimum, 44) + return b + } + assert.doesNotThrow(() => checkReleaseFormat(macho(), 'aarch64-apple-darwin')) + assert.doesNotThrow(() => + checkReleaseFormat(macho(0x01000007), 'x86_64-apple-darwin') + ) + for (const b of [ + Buffer.alloc(8), + macho(0x01000007), + macho(0x0100000c, 0x000e0000), + macho().subarray(0, 40), + ]) + assert.throws( + () => checkReleaseFormat(b, 'aarch64-apple-darwin'), + /Mach-O|macOS/ + ) +}) +test('workflows select exactly the descriptive cohort and restrict actual build jobs', () => { + const cohort = JSON.parse(readFileSync('scripts/release/release-cohort.json')) + const names = cohort.artifacts.map((a) => a.name).sort() + const build = readFileSync('.github/workflows/release-build.yml', 'utf8') + const release = readFileSync('.github/workflows/release.yml', 'utf8') + assert.deepEqual( + [...build.matchAll(/^ asset: (\S+)/gm)].map((m) => m[1]).sort(), + names + ) + const sign = build.split('\n sign:')[1] + for (const name of names) assert.equal(sign.split(name).length - 1, 1) + const published = publicationScript().match(/const names = (\[[^;]+\]);/)[1] + assert.deepEqual(Array.from(vm.runInNewContext(published)).sort(), names) + for (const prefix of ['release-', 'signed-']) + assert.deepEqual( + [ + ...release.matchAll( + new RegExp(' name: ' + prefix + '(\\S+)', 'g') + ), + ] + .map((m) => m[1]) + .sort(), + names + ) + const actualBuild = build.split('\n build:')[1].split('\n sign:')[0] + assert.doesNotMatch(actualBuild, /id-token|attestations:/) + assert.match(release, /vars\.RELEASE_PUBLICATION_ENABLED == 'true'/) + assert.match( + build.split('\n sign:')[1], + /vars\.RELEASE_PUBLICATION_ENABLED == 'true'/ + ) +}) +test('release preflight rejects inherited controls in an isolated script fixture', () => { + const root = mkdtempSync(join(tmpdir(), 'phase1-preflight-')) + const script = join(root, 'scripts/release/phase1-build.sh') + const bin = join(root, 'bin'), + marker = join(root, 'tools-ran') + const source = 'a'.repeat(40) + const args = [ + 'trusted-server-cli', + 'x86_64-unknown-linux-gnu', + 'ts-x86_64-unknown-linux-gnu', + ] + try { + mkdirSync(join(root, 'scripts/release'), { recursive: true }) + mkdirSync(bin) + fs.copyFileSync('scripts/release/phase1-build.sh', script) + const stub = (name, body) => + writeFileSync(join(bin, name), '#!/bin/sh\n' + body + '\n', { + mode: 0o755, + }) + stub( + 'git', + `test "$*" = "rev-parse HEAD" || exit 96; printf '%s\\n' "$PREFLIGHT_FIXTURE_COMMIT"` + ) + stub('printenv', 'exec /usr/bin/printenv "$@"') + for (const tool of [ + 'node', + 'npm', + 'docker', + 'cargo', + 'rustup', + 'curl', + 'sudo', + ]) + stub(tool, `printf '%s\\n' "$0" >> "$PREFLIGHT_TOOL_LOG"; exit 97`) + const execute = (extra) => { + try { + execFileSync('/bin/bash', [script, ...args], { + cwd: root, + stdio: 'pipe', + env: { + PATH: bin, + RUNNER_TEMP: root, + SOURCE_COMMIT: source, + PREFLIGHT_FIXTURE_COMMIT: source, + PREFLIGHT_TOOL_LOG: marker, + ...extra, + }, + }) + assert.fail('should stop before any real tool executes') + } catch (error) { + return error + } + } + for (const variable of [ + 'TSJS_SKIP_BUILD', + 'TSJS_PREBUILT_DIR', + 'TSJS_TEST', + ]) { + const error = execute({ [variable]: '' }) + assert.equal(error.status, 1) + assert.match(error.stderr.toString(), new RegExp('Refusing ' + variable)) + assert.equal(fs.existsSync(marker), false) + } + for (const invalid of ['0'.repeat(40), 'A'.repeat(40), 'invalid']) + assert.equal(execute({ SOURCE_COMMIT: invalid }).status, 1) + assert.equal(fs.existsSync(marker), false) + assert.equal( + execute({}).status, + 97, + 'should reach the safe node stub when preflight passes' + ) + assert.equal(readFileSync(marker, 'utf8').trim(), join(bin, 'node')) + assert.equal(fs.existsSync(join(root, 'trusted-server.toml')), false) + } finally { + rmSync(root, { recursive: true, force: true }) + } +}) +test('SPDX identifies all producing tools and normalizes license spacing', () => { + const { document, original } = spdxFixture() + document.documentDescribes = [] + document.relationships = [] + document.packages = document.packages.filter( + (p) => p.SPDXID !== 'SPDXRef-Root' + ) + document.packages[0].licenseDeclared = 'Apache-2.0 OR MIT' + original.metadata.component.version = 'v1.2.3-example' + const output = completeSpdx(document, binary, original) + assert.equal( + output.packages.find((p) => p.SPDXID === 'SPDXRef-Artifact').versionInfo, + 'v1.2.3-example' + ) + assert.equal(output.packages[0].licenseDeclared, 'Apache-2.0 OR MIT') + assert.ok(output.creationInfo.creators.includes('Tool: cyclonedx-cli-0.33.1')) + assert.ok( + output.creationInfo.creators.includes('Tool: trusted-server-phase1') + ) +}) +test('audit handles Rust escapes and every macro delimiter', () => { + for (const source of [ + String.raw`env!("\x54RUSTED_SERVER__SECRET")`, + String.raw`env!("\u{54}RUSTED_SERVER__SECRET")`, + String.raw`include_str!("\x2eenv.production")`, + 'env![concat!["TRUSTED_SERVER__", "SECRET"]]', + 'env!{concat!{"TRUSTED_SERVER__", "SECRET"}}', + ]) + assert.throws(() => checkSource('src/lib.rs', source), /compile-time/) +}) +test('audit handles ordinary char literals and generated include prefixes', () => { + assert.doesNotThrow(() => + checkSource('src/lib.rs', `const QUOTE: char = '"';`) + ) + assert.throws( + () => + checkSource( + 'src/lib.rs', + 'include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/trusted-server.toml"))' + ), + /compile-time/ + ) + assert.doesNotThrow(() => + checkSource( + 'src/lib.rs', + 'include_str!(concat!(env!("OUT_DIR"), "/generated.rs"))' + ) + ) +}) + +test('post-publish reads cannot succeed after the shared deadline', async () => { + const code = publicationScript() + .split('// BEGIN READ RETRIES')[1] + .split('// END READ RETRIES')[0] + let now = 0 + const retry = vm.runInNewContext(`${code}; retryRead`, { + Date: { now: () => now, parse: Date.parse }, + console: { + warn: () => assert.fail('should not retry deadline exhaustion'), + }, + }) + await assert.rejects( + () => + retry(() => { + now = 180000 + return 'late success' + }), + /window exhausted/ + ) +}) diff --git a/scripts/release/qualification/2026-10-15-consumer-real-fixtures.md b/scripts/release/qualification/2026-10-15-consumer-real-fixtures.md new file mode 100644 index 0000000000..31d7d0a09a --- /dev/null +++ b/scripts/release/qualification/2026-10-15-consumer-real-fixtures.md @@ -0,0 +1,150 @@ +# Consumer verifier: measured public-fixture qualification + +Measured on 2026-10-15 on native Apple Silicon macOS. This report records +actual public upstream evidence, rather than fictional examples. No release, +workflow, repository or deployment was created. Downloaded subject archives +were never executed. The retained evidence is outside the repository at +`/private/tmp/ts-real-fixture-qualification` and is temporary local evidence; +the digests below detect changes but do not authenticate a future download. + +## Trust and bootstrap + +The independently installed `/opt/homebrew/bin/gh` reported version 2.87.2. +That existing verifier, rather than the downloaded executable, successfully +verified the public attestation on `cli/cli`'s +`gh_2.97.0_macOS_arm64.zip` before its executable was run. The verified +certificate bound the archive to source/signer commit +`55dbb4dc6b7edb10b48e3d7fc5bccd32318d1b55`, workflow +`.github/workflows/deployment.yml`, `refs/heads/trunk`, and run +`30597407850/attempts/1`. The archive SHA-256 was +`a58b8fd77b417a38f47a0b54d1370c59b0fcdb324ccc9ca002b0998f7c4c999e`. +The extracted executable reported exactly 2.97.0. Version output alone was not +used as authentication. This relies on the pre-existing verifier installation +as a local trust prerequisite; this work did not independently rebuild or +qualify that installation's own historical bootstrap. + +Primary inputs: + +- [Pinned verifier release](https://github.com/cli/cli/releases/tag/v2.97.0). +- [Bootstrap signing workflow at its verified commit](https://github.com/cli/cli/blob/55dbb4dc6b7edb10b48e3d7fc5bccd32318d1b55/.github/workflows/deployment.yml). +- [Measured fixture release](https://github.com/d2lang/d2/releases/tag/v0.9.0). +- [Fixture signing workflow at its verified commit](https://github.com/d2lang/d2/blob/d5a51743b6d5c00f1ec6f8340003f5d5a6ba4eda/.github/workflows/release-archives.yml). +- [Exact attest 4.2.2 action](https://github.com/actions/attest/tree/1e69f48acb82d1966a394da916b4c1698aa569d6). + +The bootstrap release used attest 4.2.0. It was used to authenticate the +verifier, not to stand in for the required 4.2.2 signing fixture. The D2 +workflow at the verified commit explicitly pins attest 4.2.2 for both +provenance and SPDX signing. + +## Successful measurements + +The authenticated gh 2.97.0 verified both provenance and SPDX signatures for +`d2-v0.9.0-linux-arm64.tar.gz`. Its real certificates reported: + +| Field | Observed value | +| ------------------------ | -------------------------------------------------------------------------------------- | +| Issuer | `https://token.actions.githubusercontent.com` | +| SAN and build signer | `https://github.com/d2lang/d2/.github/workflows/release-archives.yml@refs/tags/v0.9.0` | +| Source and signer digest | `d5a51743b6d5c00f1ec6f8340003f5d5a6ba4eda` | +| Source repository ID | `533087958` | +| Source owner ID | `242104691` | +| Trigger / runner | `push` / `github-hosted` | +| Invocation | `https://github.com/d2lang/d2/actions/runs/34165396038/attempts/1` | + +The actual output was an array of one processing result for each +`attestation verify` invocation. The real verification media type, +flattened certificate field names, SLSA v1 provenance fields, and SPDX +`https://spdx.dev/Document/v2.3` predicate matched the implemented consumer +contract. The separately downloaded `d2.spdx.json` compared equal as parsed +JSON to the signed SPDX predicate. + +Both bundles were replayed with the production-equivalent explicit issuer, +SAN, source ref, source digest, signer digest and hosted-runner restriction. +The process environment was cleared, with only private HOME/config and +noninteractive settings supplied; no token was present. SPDX verification +also succeeded under a macOS `sandbox-exec` profile denying `network*`, +using a local bundle and independently transferred trusted roots. The same +profile caused a separate curl DNS/network attempt to fail. This is an +actual consumer offline primitive check, not qualification of producer +process cleanup or the broader macOS build boundary. + +Pinned `gh release verify --format json` also successfully verified the D2 +and CLI public release manifests. Actual output was one processing-result +object rather than the attestation command's array; the release predicate +was `https://in-toto.io/attestation/release/v0.2`. + +## Retained evidence digests + +All digests are SHA-256 of exact retained bytes. + +| File | Digest | +| ----------------------------------- | ------------------------------------------------------------------ | +| `bootstrap-proof.json` | `b53f05f983872a6f5f2d9e95d54bf371f86b012a36fc324b80be5eebbf57fc66` | +| `d2-v0.9.0-linux-arm64.tar.gz` | `ac2c028697199479acb321db1e3d68caee9f2ba492ed73caa3cd13f3829bf913` | +| `d2.spdx.json` | `efaf407d3142ee52de4f3e90864f3023110dd9f430f8220143d6f3d9de86e689` | +| `d2.provenance.sigstore.json` | `d42456c264743abc09194a548e18019e4073eef4040abd182fd1c974616a9259` | +| `d2.spdx.sigstore.json` | `26cab9234beaa45c49510924ae7c8aa4cb058a3419aff673eb6c51c33bce3ad3` | +| `d2-isolated-provenance-proof.json` | `59045bc27b9d7bbf1f6747ca7939aa8ef2c77330e86d2229d7cee9daf3c7e2ca` | +| `d2-isolated-spdx-proof.json` | `bf43a603bb1a4f83cdcfeee68ae0b69b3d23e8fb1f46a20d083dafafc4220c8a` | +| `d2-offline-spdx-proof.json` | `bf43a603bb1a4f83cdcfeee68ae0b69b3d23e8fb1f46a20d083dafafc4220c8a` | +| `d2-release-proof.json` | `a1269c4c7da96bd0e5129b1d95030cf084d91fed2b8a085ad8fdcb633b979deb` | +| `gh-release-proof.json` | `dd934980d284943275aa2d9c574fdbcc5e3713e38a616ce49ca0ac682040f07c` | +| `trusted-root.jsonl` | `65ca537f6ed8a47fd0e560c421baa1f6c1efb8b25fc200d8c5c02c0e92eb2b9c` | + +## Replay commands + +First authenticate the pinned archive with the independently installed +verifier, then extract it. Fetch the D2 archive and standalone SPDX document +from the release linked above. Use the retained signed bundles, or obtain +and verify fresh ones from the fixed public repository; a hash here cannot +replace cryptographic verification. + +```sh +set -eu +EVIDENCE=/private/tmp/ts-real-fixture-qualification +PINNED_GH="$EVIDENCE/extracted/gh_2.97.0_macOS_arm64/bin/gh" +/opt/homebrew/bin/gh attestation verify \ + "$EVIDENCE/gh_2.97.0_macOS_arm64.zip" -R cli/cli --format json +/opt/homebrew/bin/gh attestation trusted-root --hostname github.com \ + > "$EVIDENCE/trusted-root.jsonl" + +# Run once per predicate, choosing the corresponding retained bundle. +PREDICATE=https://spdx.dev/Document/v2.3 +BUNDLE="$EVIDENCE/d2.spdx.sigstore.json" +env -i HOME="$EVIDENCE/isolated-home" \ + GH_CONFIG_DIR="$EVIDENCE/isolated-home" GH_PROMPT_DISABLED=1 \ + /usr/bin/sandbox-exec -p '(version 1)(allow default)(deny network*)' \ + "$PINNED_GH" attestation verify \ + "$EVIDENCE/d2-v0.9.0-linux-arm64.tar.gz" --bundle "$BUNDLE" \ + --custom-trusted-root "$EVIDENCE/trusted-root.jsonl" \ + -R d2lang/d2 --hostname github.com \ + --cert-identity https://github.com/d2lang/d2/.github/workflows/release-archives.yml@refs/tags/v0.9.0 \ + --cert-oidc-issuer https://token.actions.githubusercontent.com \ + --source-ref refs/tags/v0.9.0 \ + --source-digest d5a51743b6d5c00f1ec6f8340003f5d5a6ba4eda \ + --signer-digest d5a51743b6d5c00f1ec6f8340003f5d5a6ba4eda \ + --deny-self-hosted-runners --predicate-type "$PREDICATE" --format json +``` + +For provenance, use `https://slsa.dev/provenance/v1` and +`d2.provenance.sigstore.json`. Preserve the exact expected source identities; +never derive acceptance policy from an unverified proof's fields. Runtime +proofs and bundles remain outside the repository; checked-in compatibility +fixtures use fictional identities and contain no signatures or trusted proof +constructors. + +## Scope and remaining first-use work + +Task 18's real pinned-verifier/signed-public-fixture primitive requirement is +now measured. Its adversarial bounded-process tests remain the separate +source-built CLI verification evidence. + +Task 19's upstream format uncertainty is resolved for both predicate kinds +and release-manifest output. The public fixture has a different repository, +workflow and product graph and cannot satisfy Trusted Server policy. No +third-party result constructs official project authorization. Full official +first-use verification remains pending until the producer emits the canonical +signed index and all required companion assets, from independently approved +source, and an independently trusted/source-built verifier checks that actual +release. This report does not enable releases or qualify authenticated +producer bootstrap, hosted build recipes, or signing workflow execution. diff --git a/scripts/release/qualification/2026-10-15-js-two-builds.md b/scripts/release/qualification/2026-10-15-js-two-builds.md new file mode 100644 index 0000000000..6ef9467e9e --- /dev/null +++ b/scripts/release/qualification/2026-10-15-js-two-builds.md @@ -0,0 +1,170 @@ +# Task 7: real original materials and two fresh JS builds + +Status: the explicit original-material and Cargo Git tests passed locally on +native macOS, and the full Linux A/B execution test passed in +[ordinary PR CI](https://github.com/IABTechLab/trusted-server/actions/runs/38747798078) +on 2026-10-15. This is candidate execution evidence from an ordinary CI +runner. It does not qualify release tools, an OCI image, bootstrap, hosted +release environments or independently approved release source. Production +selectors remain closed. + +## Original-material validation + +The explicit native test +`materials::npm_bundle::tests::actual_current_lock_npm_originals` passed +locally on native macOS on 2026-10-15 in 283.74 seconds. It is ignored in +ordinary CI; there, original authentication runs only inside the A/B fixture's +npm original preparation. It authenticated all 651 locked installation +locations and 626 distinct compressed originals against the current npm +lock's SHA-512 identities, freshly extracted every original, reviewed every +manifest, retained the compressed originals, and rechecked all retained +archives and extracted trees. Its fictional source binding is a diagnostic +test input, not independent source approval or acquisition qualification. + +The exact npm lock SHA-256 is +`d5a1b74310b0455531c3133cacf86fa528682cbed91c40412fb3791ccd48c527`. +An independent local raw-header diagnostic, not retained in the repository, +inspected 30,723 entries. The largest +member is the locked optional OpenBSD Rollup binary, 110,275,248 bytes. The +reviewed member limit is 128 MiB; the compressed-original limit remains +64 MiB and total decoded limit remains 512 MiB per archive. Extraction and +rehashing stream bytes and honor cancellation/deadlines. + +Actual originals exposed the eight exact-version legacy type-package roots, +seven node-tar timestamp layouts, and local PAX metadata in `gulp-wrap` +0.15.0 documented in [JS composition](../js-composition.md). Only redundant +PAX path/size values matching the next raw regular member are accepted. +Unknown fields, duplicate keys, malformed framing, overrides, unsupported +record sequences, links, privileged modes, and hidden name bytes fail. +The only empty script entries were non-install separator labels in `ignore`; +empty lifecycle declarations remain rejected. The only actual dependency +install hooks were the reviewed `core-js` and `esbuild` tuples. + +The separate actual-current-Cargo-Git fixture also passed locally; it is +ignored in ordinary CI. The selected +closure contained 557 objects and 6,334,558 decoded object-content bytes, +excluding canonical object headers, with root tree +`411e16f40511f3f4867349d09784ec68beac022d`. This checks actual Git material +format and closure validation; it does not qualify Git acquisition, a native +toolchain, or a Cargo recipe. + +## Linux execution acceptance + +Orchestration commit: `2edd46ce6` +(`Build and compare two fresh offline JavaScript installations`); reviewed +archive layouts: `e2260d457b29b1202a39f8732e78d5d4bd9c323b`; CI runner configuration: +`cbd96047400099271534f110b5decdcf73b1a5c8`; collector fixture context and +failure diagnostics: `1a2c46075e18791b623f26ea73af8f833dc5f70b`. +The [Linux producer job](https://github.com/IABTechLab/trusted-server/actions/runs/38747798078/job/116465922121) +(`cargo test (release tooling and JS prebuilt, native) (ubuntu-24.04)`) +selects exactly one ignored full-execution test: +`preparation::tests::full::real_two_fresh_js_builds`. +It passed in 687.37 seconds (14:09:47Z to 14:21:15Z) after a two-minute +optimized test build: `1 passed; 0 failed; 0 ignored; 354 filtered out`. +Every job in that Run Tests workflow passed, as did the Run Format, CodeQL +and Integration Tests workflows for the same head. + +CI checked out the pull request merge commit +`15fcd34c64b0f726f42f4f4da5de5af7e3577aa2`, whose parents are `main` at +`a0b00c72705baf6b0f63c206d7721b68ee03e995` and branch head +`1a2c46075e18791b623f26ea73af8f833dc5f70b`. The fixture cloned that exact +checkout. The commit is a CI candidate, not independently approved release +source. The printed measurement was: + +```json +{ + "inventory_sha256": "2cdcb1da9c6318ce812cf7fd0b9cc0b1bdc9465c93d68953054db3e361b45042", + "kind": "ordinary-ci-two-fresh-js-builds", + "module_count": 13, + "private_roots_removed": 6, + "release_qualification": false, + "schema_version": 1, + "settings_sha256": "6bce0b31534321c4e36209caf09cad19a0ede1dbe7f6e39254b3f68657a8ea27", + "source_commit": "15fcd34c64b0f726f42f4f4da5de5af7e3577aa2", + "test_counts": [1275, 1275], + "tools_sha256": "4a461321d25eec779752960570ba574bae397102cb3b652b9b70cc24bb6431c6" +} +``` + +Both confined full suites passed all 1,275 registered tests including type +checks. All 13 final core/integration modules were byte-identical, six private +roots were removed and retained A bytes were rehashed at handoff. The test +writes this file only after every stage succeeds, so its presence records the +actual completed path; the file itself remains a diagnostic printout. + +After this run, the fixture gained explicit assertions that each side exposes +only its own private source, installation and metadata roots, shares only the +read-only originals and tool roots, and cannot reuse the other side's boundary. +Isolation was already enforced: exact `matches_roots` checks run before +execution and each installation, and every command path is translated only +through its own boundary; the next CI run on +this branch exercises the new assertions. A unit test also now rejects failed, +pending and unregistered Vitest report results. + +The fixture uses Node 24.12.0, npm 11.6.2, a source-built producer and native +observer guard, original compressed archives, and an ordinary local Linux +container image. It creates two independent tracked-source copies with no +`.git`, installed dependencies, generated bundles or shared writable cache. +Each side establishes an actual socket-denial proof in its own network/PID +boundary, seeds its private cache from authenticated originals, performs a +fresh offline installation with lifecycle observation, captures the original +collector inventory, exercises missing native tools and the actual esbuild +download fallback, and runs the full registered suite with release guards +and type checks. It compares every final core/integration bundle byte for +byte, closes all six private source/state/metadata roots, then rehashes +retained A bytes at handoff. No serialized measurement can construct private +completion authority. + +After success the test writes `js-execution-measurement.json` into its +external fixture root and CI prints it. This records actual source identity, +A/B test counts, module count, selected tool/settings hashes, original A +inventory hash and removed-root count. Its `release_qualification` field is +explicitly false; the file is diagnostic evidence, not an authorization +input or a canonical release companion. + +The full fixture uses test-profile optimization level 2 with debug assertions +and overflow checks explicitly enabled. Ordinary native tests retain their +existing debug profile. Cache seeding alone triggers at least 80 complete +original/tree/tool rechecks across A/B; optimization retains every check. +Local all-originals runtime fell from 283.74 seconds to 18.18 seconds after +a 48.44-second optimized compilation. A further run with both safety settings +explicitly enabled reused the same executable and passed in 17.93 seconds. +These are local performance measurements, not hosted execution proof. + +## Supporting checks and scope + +With the new unit test, the local producer suite passed 364 unit tests, four +boundary integration tests and 13 command tests on macOS. Two explicit +actual-material tests are ignored by that ordinary run and are exercised +separately. +All-target/all-feature producer Clippy, documentation, formatting and staged +domain lint passed. Independent spec and quality reviews found no remaining +Task 7 code blocker before the Linux run; that run has since passed. + +A separate local, unretained release-mode Vitest JSON run passed all 1,275 tests. Its +52 report entries cover exactly 51 distinct registered test/type-check files; +the type-check file appears twice. This confirms the Rust report parser's +real format/coverage join; it does not substitute for either confined A/B run. + +Two earlier full Linux attempts failed for concrete, since-fixed causes. The +[first](https://github.com/IABTechLab/trusted-server/actions/runs/38736177179/job/116425928695) +rejected the authentic 110,275,248-byte optional OpenBSD Rollup member under +the former 64 MiB member limit. The +[second](https://github.com/IABTechLab/trusted-server/actions/runs/38741529217/job/116444179601), +unoptimized, reached Vitest and failed one collector negative fixture after +2,027.48 seconds. That fixture passed parent +source context to its main call but omitted it from later rejection cases; +the duplicate-material case consequently failed at Git discovery in a fresh +source copy without `.git`. A separate local full no-`.git` reproduction confirmed +exactly that single failure. Copying only the corrected test file into the +same disposable tree made all 1,275 tests and type checks pass with zero +failed/pending tests. Every negative call now supplies the same source context, +preserving its intended semantic assertion. Failed Vitest runs also retain a +bounded sanitized list of registered failed test names in the process error +before checked cleanup; raw assertion messages and stacks are excluded. + +R-JS allows one shared Linux JS build for later Rust artifact jobs. Task 7 +does not qualify native Rust recipes or macOS lifecycle containment. The +macOS regression ports passed afterwards on [macOS 15 CI](https://github.com/IABTechLab/trusted-server/actions/runs/38765937295/job/116529009924), including the formerly residual scrubbed process-group escape, which is now killed and rejected. That boundary remains unqualified. Authenticated +bootstrap, selected hosted environments, actual Cargo recipes and official +release publication remain separate incomplete gates. diff --git a/scripts/release/qualification/2026-10-16-confined-cargo-candidate.md b/scripts/release/qualification/2026-10-16-confined-cargo-candidate.md new file mode 100644 index 0000000000..59c73bb74f --- /dev/null +++ b/scripts/release/qualification/2026-10-16-confined-cargo-candidate.md @@ -0,0 +1,102 @@ +# Task 9: first confined candidate Cargo build + +Status: on 2026-10-16 ordinary PR CI built `trusted-server-fastly.wasm` (role A) +through `PreparedRecipe::execute` inside the Linux Docker denial boundary. It +also proved that Cargo build scripts, a build-script grandchild and a +procedural macro inherit network denial. This is candidate execution evidence +from an ordinary CI runner and records `release_qualification: false`. It does +not qualify the toolchain, the image, the acquired crates, the vendored Git +manifests, bootstrap or any hosted release environment. Production tool +selections remain uninhabited, so production `execute` still returns +`Unqualified`. + +## Run + +The job +[`cargo test (confined release Cargo candidate, Linux)`](https://github.com/IABTechLab/trusted-server/actions/runs/38849941580/job/116808709738) +checked out the pull request merge commit +`309b9553657bd74c3e7dea03927284b68756c7e6` (branch head +`2d933b0e9debed3960f97f2cc3ebb5948da17471` merged into `main` at +`fa9837f2b95f817f58eadb16c929c38fcb581c9a`). Each step ran exactly one +selected ignored test, after a guard that the selection matched exactly one +test. + +The fixture acquired 706 registry originals (each checked against its locked +SHA-256), one Git object store for the locked `edgezero` revision, and eight +normalized Git manifests from `cargo vendor --offline`. Cargo 1.95.0 wrote no +`Cargo.toml.orig` for Git packages, so each original manifest was read from +the Git object database and paired with its normalized form. The acquisition +was unauthenticated ordinary CI. The tool root was a private single-link copy +of the host Rust 1.95.0 sysroot with `wasm32-wasip1`; the image was Ubuntu +24.04 with `gcc` and `libc6-dev`, identified only by its image ID. + +## Denial inheritance + +`recipe::tests::full::cargo_children_inherit_denial` passed in 25.17 seconds. +A build script, a build-script grandchild and a procedural macro each +attempted a loopback connection inside the boundary. All three reported errno +1 (`EPERM`), matching the boundary's own denial proof, not `ECONNREFUSED`. +The control crate built a real `denial-control.wasm` (2,013,985 bytes, SHA-256 +`60bd5bc1f0388a50a75dd3a0e87400282fb5c97499fc414a1e8dadd2343d8534`) through +the same `execute` path. + +## JS handoff into the Fastly build + +`recipe::tests::full::real_js_handoff_to_fastly_cargo_build` passed in 871.02 +seconds. It first repeated the full two-fresh-installation JS acceptance: both +sides passed 1,275 tests including type checks, all 13 final modules were +byte-identical and six private roots were removed. It then bound that +in-memory JS completion to a third fresh source copy, prepared the vendor +directory, ran `cargo build --release --frozen +--message-format=json-render-diagnostics` with umask 022 and four jobs, and +completed only after the JS handoff, tools and materials rehashed unchanged. +No Cargo registry or Git cache appeared in the private `CARGO_HOME`. The +confined build took 130 seconds. + +```json +{ + "artifact": { + "name": "trusted-server-fastly.wasm", + "sha256": "6a19dece46c11b40f352a0ac23a124a4f8d6af5742df56845d37a99dc2b713c5", + "size": 66885152 + }, + "asset": "trusted-server-fastly.wasm", + "boundary_fingerprint": "99af57442bb03eeaaa50c320d83dbe5bf8938de97d5eca1c9f62668c17170582", + "cargo_lock_sha256": "9d1ce5338f2511e9ae289ceb4e1679a2330cda8c43f205d80b6d01672589e4dc", + "derived_manifest_digests": 8, + "elapsed_seconds": 130, + "js_inventory_sha256": "f3645c7699165920ccc64bfc0f91c43479b01b771cfacf9b9bd4721bcaedf33e", + "js_settings_sha256": "5d341e8a8b58a638ddc71b663daa0dbada5d1970175f21391b7750d36ffdef02", + "js_tools_sha256": "ba26c9198ef338cf0afc94762d91cfaa3cdbd51987e65dfe0f8905980963da2d", + "kind": "ordinary-ci-confined-cargo-build", + "materials_digest": "da91ba65a309b9290460a2e3fb5fdf2dd6fee59f39225777264444932bf5ab17", + "plan_digest": "a743a63a68cd1ace19f434bae98cf4fc1fafcfe5913e141c5196e7302255afa8", + "release_qualification": false, + "role": "A", + "rust_tools_digest": "0a741d4f4c3b4f9087da3891120b3557925573867341cdf826cb0ce56f883879", + "schema_version": 1, + "source_commit": "309b9553657bd74c3e7dea03927284b68756c7e6", + "stderr_sha256": "ba7154da1ed3e07bfa14e0e6ab0c894684fbe7b4eeccc903262f7e1db0cfca1c", + "stdout_sha256": "9fb08fe022a5fccef541d756acc85df5a6480c9987b29275a7cdc86166953efc" +} +``` + +Eight derived manifest digests mark this build unqualified: the normalized +`edgezero` manifests came from `cargo vendor`, not from a reviewed producer +normalization. + +## Archive policy change + +Real registry crates required two narrowly reviewed compatibility encodings, +for registry originals only: a GNU `././@LongLink` record (regular mode, +exact join with the next header's 100-byte name, bounded, never consecutive, +dangling or mixed with PAX) and tar modes that carry the regular or directory +type bits. npm originals keep the stricter policy. Git packages are vendored +under their bare name so that intra-workspace relative paths resolve. + +## Not covered + +Role B and the A/B comparison, the three `ts` executables, path remapping, +final-byte Viceroy acceptance, producer-owned manifest normalization, +authenticated acquisition, qualified tool and image pins, and hosted +qualification remain open under Tasks 8 and 9. diff --git a/scripts/release/qualification/2026-10-16-fastly-canonical-reproduction-equality.md b/scripts/release/qualification/2026-10-16-fastly-canonical-reproduction-equality.md new file mode 100644 index 0000000000..afa9c00791 --- /dev/null +++ b/scripts/release/qualification/2026-10-16-fastly-canonical-reproduction-equality.md @@ -0,0 +1,60 @@ +# Task 9: Fastly canonical and reproduction builds are byte-identical + +Status: on 2026-10-16 ordinary PR CI built `trusted-server-fastly.wasm` twice +through `PreparedRecipe::execute`, as the canonical build (`Role::Canonical`, +the spec's Build A) and as the reproduction build (`Role::Reproduction`, Build +B), from one JS handoff and in separate fresh roots and denial boundaries. The +final bytes were identical, and a scan of those bytes proved that the path +remapping took effect. This is candidate evidence from an ordinary CI runner +and records `release_qualification: false`. The reproduction profile comes from +a test-only alternate; the production reproduction profile, the toolchain, the +image, the acquired crates and the `cargo vendor` Git manifests remain +unqualified. + +## Runs + +| Run | Source (PR merge commit) | Final SHA-256 | Size (bytes) | Test duration | +| ------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------ | ------------------------------------------------------------------ | ------------ | ------------- | +| [Primary, after the role rename](https://github.com/IABTechLab/trusted-server/actions/runs/38856856376/job/116830159922) | `4243e3a1c16bf1212ab585eecce8592bd5137950` | `a17a77cc5119384d2fb5f74d83cb76cbf264c768e508b8545cf8537db481f178` | 66,833,859 | 1,119.36 s | +| [Earlier, before the rename](https://github.com/IABTechLab/trusted-server/actions/runs/38855948935/job/116827324356) | `373463048c22237ba6e045e7fd0706bb4a104568` | `e5390f47e025f34b70190c944134d6e2b7209e792e8a4ec2d64c65ead594ca4b` | 66,833,943 | 1,064.12 s | + +Each run built both roles from the same commit and matched. The two runs +differ from each other because the source commit is embedded in the binary. +The earlier run's measurement still labels the roles `A` and `B`. Test +durations include the full two-fresh-installation JS acceptance (1,275 tests +per side, 13 byte-identical modules) that produces the shared JS handoff. Both +runs also repeated the build-script, grandchild and procedural-macro denial +test. + +## What varied + +| Dimension | Canonical | Reproduction | +| ---------------- | ------------------------------- | ------------------------------- | +| umask | 022 | 027 | +| Cargo jobs | 4 | 2 | +| Host checkout | separate fresh temporary copy | separate fresh temporary copy | +| Start time | recorded per run | recorded per run | +| Locale, timezone | normalized to `C`, `UTC` | normalized to `C`, `UTC` | +| Execution user | same CI user (unsupported here) | same CI user (unsupported here) | + +In the primary run the confined builds took 137 and 171 seconds. Inside the +Linux container both roles see every root at the same `/boundary//` +path, so the checkout-root difference is a host-only variation there. + +## Remapping proof + +The build remaps the checkout, vendor tree, Cargo home, Rust tool root and +target directory to fixed prefixes through one quoted +`--config target."".rustflags` entry, which Cargo appends to tracked +target rustflags. Because container paths match across roles on Linux, byte +equality alone would not prove the remapping. The test therefore scanned the +compared final bytes: they contained none of `/boundary/`, the host build +roots, the fixture root or `RUNNER_TEMP`, and they did contain the remapped +`/trusted-server` and `/cargo-vendor` prefixes. + +## Not covered + +The three `ts` executables, Viceroy final-byte acceptance, producer-owned Git +manifest normalization, the production reproduction profile, authenticated +acquisition, qualified tool and image pins, and hosted qualification remain +open under Tasks 8 and 9. diff --git a/scripts/release/qualification/2026-10-16-fastly-runtime-gate.md b/scripts/release/qualification/2026-10-16-fastly-runtime-gate.md new file mode 100644 index 0000000000..a9fe68a6aa --- /dev/null +++ b/scripts/release/qualification/2026-10-16-fastly-runtime-gate.md @@ -0,0 +1,60 @@ +# Task 9: Fastly final-byte runtime gate + +Status: on 2026-10-16 ordinary PR CI ran the Fastly final-byte gate on both +the canonical and the reproduction `trusted-server-fastly.wasm` outputs, and +both were accepted. The same run rejected a closed-origin control as a +health-only success. This is candidate evidence from an ordinary CI runner and +records `release_qualification: false`. Viceroy is pinned by version text and +recorded by digest, not by an independently reviewed digest; that pin, the +toolchain, the image and the acquired inputs remain unqualified. + +## Run + +The job +[`cargo test (confined release Cargo candidate, Linux)`](https://github.com/IABTechLab/trusted-server/actions/runs/38875569290/job/116889681434) +checked out the pull request merge commit +`ab6cba207192969c55547876f9adf7d625a64ea1` (branch head +`4cc5700e834c3238f99a81b3a567edb634fecfe1`). The ignored test +`recipe::tests::full::fastly_canonical_and_reproduction_builds_are_byte_identical` +passed in 1,113.82 seconds. It built both roles from one JS handoff (confined +builds of 132 and 166 seconds), found them byte-identical (66,833,956 bytes, +SHA-256 `0c21ec136d2ffabf2d3440142d741e96b2aef18d5a52c82d4cfc8022dc0866c9`), +then ran the gate on each output without rebuilding. The denial-inheritance +test passed again in the same job. + +## What the gate checks + +1. The exact final bytes are re-read against the recorded size and digest and + fully validated as a core Wasm command with the pinned wasmparser: no + component, only Fastly import modules, a no-argument `_start`, an exported + memory, the HTTP request and response modules, and debug sections. +2. The verified bytes are copied read-only into a private directory and served + by the explicitly selected Viceroy, whose `--version` output must be exactly + `viceroy 0.17.0` and whose binary is rehashed around every use. Unknown + imports fail to link. The configuration comes from the integration + harness's fixtures, which must match the built source. +3. `/health` only gates readiness. Acceptance requires a 200 HTML publisher + page that carries this run's unique origin marker once and exactly one + injected tsjs script, an origin request observed by the gate's loopback + origin, no guest failure in Viceroy's output, and a stop initiated by the + gate. The artifact rehashes unchanged afterwards. Responses are parsed + strictly over loopback only. + +## Results + +| Output | Health | Publisher | Injected script | Origin request | Viceroy stderr | Startup | +| ------------ | ------ | --------- | ----------------------------------------------------------------------------------------------------- | ---------------- | -------------- | ------- | +| Canonical | 200 | 200 | `/static/tsjs=tsjs-unified.min.js?v=662e376b054ac1fccc82a9c8f0d2a086bfa11541343cb0af8c046ef09ac5aa75` | `GET / HTTP/1.1` | empty | 4.26 s | +| Reproduction | 200 | 200 | same | `GET / HTTP/1.1` | empty | 4.26 s | + +Viceroy 0.17.0 binary SHA-256: +`05da4843fd8ae870409211e9fbc168aafbb1609445fbd0eefa3902e5a151cce1`. The +closed-origin control was rejected with "publisher page status is not 200" and +tagged as a health-only success. + +## Not covered + +The three `ts` executables, producer-owned Git manifest normalization, the +production reproduction profile, an independently reviewed Viceroy digest, +authenticated acquisition, qualified pins and hosted qualification remain open +under Tasks 8 and 9. Live deployment is separate. diff --git a/scripts/release/qualification/2026-10-16-linux-cli-candidate.md b/scripts/release/qualification/2026-10-16-linux-cli-candidate.md new file mode 100644 index 0000000000..ae9b265d3c --- /dev/null +++ b/scripts/release/qualification/2026-10-16-linux-cli-candidate.md @@ -0,0 +1,153 @@ +# Task 9: Linux CLI candidate builds, inspects and starts cleanly + +Status: ordinary PR CI passed the Linux `ts` candidate recipe on 2026-10-16. +The canonical and reproduction builds produced byte-identical executables. +The exact accepted canonical bytes passed final-path and ELF inspection and +started outside the checkout in all three reviewed Linux images. This is +candidate evidence with `release_qualification: false`; it does not qualify +production tools, image pins, acquisition or release enablement. + +## Subsequent reproduction scheduling + +This report retains its original equality observation. `3e3e30996` later +defers optional native reproduction on ordinary push/PR runs, measuring it on +scheduled/dispatched or appropriately labelled PR runs instead. Canonical +build/inspection/startup remain required. The [dated schedule report](2026-10-21-native-reproduction-schedule.md) +records the triggers, cache limits and subsequent 45-minute ordinary-CI per-build +wall. Future equality results must cite the measured run/job and built source; +a deferred run does not repeat this report's equality proof. + +## Run and artifact + +- [Successful candidate job](https://github.com/IABTechLab/trusted-server/actions/runs/38921701566/job/117042987142). +- Branch head: `0299c3a17812ce78def6b8df479f091a27120ce8`. +- Built source (PR merge commit): `4a6404b072a3bc4bd36a12b33e4201c5f5797522`. +- Asset: `ts-x86_64-unknown-linux-gnu`, variant `default`, package `0.1.0`. +- SHA-256, both builds: `a3a64d15ee1663ff747a21e0edc089703183a0a8685740ec65592d9fdd3a34b1`. +- Size, both builds: 285,604,680 bytes. +- Complete candidate test: 1,352.55 seconds; canonical Cargo execution: 293 seconds. +- Lock SHA-256: `c37beb7694512de99ac99c8ac214fa01f56f32226810e814e7cc26a717c8eedd`. +- Shared JS inventory SHA-256: `b0632a21ad06c0bec46be365aba30b62f89e8153a33e5517ce4109eca30793eb`. + +The job logs retain `linux-cli-candidate-measurement.json` and +`builder-filesystem-controls.json`, including tool/material/plan digests, +image IDs, debug-section sizes, startup output and reproduction comparison. +The source commit above, rather than the later documentation commit, is the +identity embedded in and measured for this executable. + +## Actual build and boundary + +The producer/probe itself was rebuilt inside the GNU baseline from the +tracked source and copied pinned Rust toolchain using cached offline Cargo +inputs. Both CLI roles then used `PreparedRecipe::execute` in the selected +UBI builder with glibc 2.34. The candidate acquired inputs through ordinary +unauthenticated CI preparation, including eight producer-normalized Git +package manifests and zero caller-derived manifests. + +Builder base: `registry.access.redhat.com/ubi9/ubi:9.6`. +Observed builder ID: +`sha256:c92872ae3dc77cff493a6d50ba5835961c03ba7f50aac94b5c3e643ddc8e7e71`. +The immutable image configuration was checked for absent/empty declared +volumes. Cargo, compiler and build children ran under the established +socket-denial boundary with no network and a read-only container root. + +Actual filesystem controls exited zero after observing: + +| Location | Observed control | +| ------------------------------------------------- | -------------------------------------------------------- | +| `/tmp` | Write denied: read-only filesystem | +| `/dev/shm` | Write denied: directory absent with IPC disabled | +| `/dev` | Write denied: permission denied | +| `/etc/hosts`, `/etc/hostname`, `/etc/resolv.conf` | Append-open denied: permission denied | +| Bound `TMPDIR` | Write, read and removal succeeded; no test file remained | + +These checks precede the real fresh JS and native builds. Global `/tmp` is +not writable scratch storage. The JS containment test uses a real symlink +between owned sibling directories relative to its configured source root; +it no longer clears the supplied `TMPDIR` to reach global temporary storage. + +The separate real timeout regression passed in 5.05 seconds. It starts only +after Docker acknowledges a full container ID, times out an attached child, +and verifies removal of that owned container. Build and startup cleanup use +the same acknowledged-create lifecycle; a failed/unresolved creation remains +fatal rather than being cleared by an early absence check. + +## Final bytes and startup + +Inspection of the copied final executable found: + +| Check | Measured result | +| ------------------------------ | ------------------------------------------------------------------------------- | +| Architecture | x86-64 ELF | +| Interpreter | `/lib64/ld-linux-x86-64.so.2` | +| Highest required GLIBC version | `2.34` across 27 version requirements | +| Needed libraries | `libgcc_s.so.1`, `libm.so.6`, `libc.so.6`, `ld-linux-x86-64.so.2` | +| `.debug_info` | 87,973,465 file-backed bytes | +| `.debug_line` | 33,681,096 file-backed bytes | +| Remapped prefixes retained | `/trusted-server`, `/cargo-vendor`, `/cargo-target` | +| Build paths | No bound host/execution roots, `/boundary/`, fixture roots or `RUNNER_TEMP` | +| Operator values | Three planted fictional variables removed by preflight; zero final-byte matches | + +Bound-root rejection and remap evidence are mandatory inside recipe +completion, before an accepted artifact can be returned. The candidate also +scans fixture/runner roots. The GNU-only generic `/tmp/` policy permits the +measured Chromiumoxide runtime Chrome lookup constant `/tmp/aws/lib`; it does +not permit the actual build roots. The closed scratch-storage controls above +support that distinction. macOS retains its separate temporary-path policy. + +The accepted executable was copied read-only, hashed before and after each +startup, and executed as `ts --version` without a checkout or operator config. +All starts exited zero with empty stderr and identical expected output: + +```text +ts package=0.1.0 source=4a6404b072a3bc4bd36a12b33e4201c5f5797522 target=x86_64-unknown-linux-gnu variant=default +``` + +| Startup image | Observed local image ID | +| ------------------------------------------------- | ------------------------------------------------------------------------- | +| `registry.access.redhat.com/ubi9/ubi-minimal:9.6` | `sha256:a2c5a85865a585c3bc8b10f6c269358fdf89fe32be4232885166889d85c76421` | +| `docker.io/library/ubuntu:22.04` | `sha256:e8df6c74f650ad385efe2e20103eca9b0ad50690fb3c63d49fa00762412bbf55` | +| `docker.io/library/debian:12` | `sha256:94926f0fd968cd68ac486502185a5dd78687874a9ec7727f64fe2af696e28311` | + +These are observed IDs from ordinary CI tag acquisition, not authenticated +production digest selections. Startup uses a non-root user, no network, +read-only mounts/root, dropped capabilities and bounded execution/cleanup. +This exercises executable loading and version reporting, not every CLI +command or a browser installation. + +## Reproduction and regression coverage + +The reproduction used separate fresh checkout, vendor, Cargo-state, target +and output roots from the shared JS handoff. The checked pairing required +identical source, lock, tools, material closure and JS inputs, with the +supported ambient differences recorded honestly: canonical umask 022/jobs 4 +versus reproduction umask 027/jobs 2, distinct host roots and start times. +Container paths remain fixed; locale/timezone are normalized and execution +user variation is unsupported here. Final-byte path inspection supplies +evidence beyond equality under those fixed container paths. + +The measured native result was `identical`, with no differing offset/section. +Native reproduction remains a measurement, rather than a release equality +gate; this successful pair does not qualify a general reproducibility claim. + +The [Linux release-tooling job](https://github.com/IABTechLab/trusted-server/actions/runs/38921701566/job/117042986647) +also passed its actual two-fresh-installation JS proof in 702.81 seconds: +1,275 tests per side, 13 matching bundles and six private roots removed. Its +registered native producer tests passed; prebuilt-JS tests recorded 37 passed +and one ignored. The [companion Fastly job](https://github.com/IABTechLab/trusted-server/actions/runs/38921701566/job/117042986628) +passed Cargo build-script, grandchild and procedural-macro +network-denial attempts, including a successfully compiled control retaining +an actual compiler-remapped source filename. It also passed complete Fastly +canonical/reproduction equality and both final-byte Viceroy acceptance gates. +Tests, formatting, integration tests and CodeQL all passed on branch head +`0299c3a17812ce78def6b8df479f091a27120ce8`; this report is a later +documentation-only update. + +## Remaining scope + +Task 9 and milestone M4 remain open. The two Apple CLI recipes and native +startup acceptance, qualified production ambient profiles, independently +authenticated acquisition/tool/image selections, Task 8 hosted qualification +and subsequent executable composition/evidence work are still required. +Production wrappers and qualified selection remain closed. This candidate +does not sign, publish, deploy or enable an official release. diff --git a/scripts/release/qualification/2026-10-16-producer-normalized-manifests.md b/scripts/release/qualification/2026-10-16-producer-normalized-manifests.md new file mode 100644 index 0000000000..36644f90d6 --- /dev/null +++ b/scripts/release/qualification/2026-10-16-producer-normalized-manifests.md @@ -0,0 +1,49 @@ +# Task 9: producer-normalized Git manifests + +Status: on 2026-10-16 ordinary PR CI built the Fastly canonical and +reproduction outputs using Git package manifests that the producer normalized +itself from the verified Git tree, instead of manifests supplied by +`cargo vendor`. Both builds report zero caller-derived manifests. This is +candidate evidence from an ordinary CI runner and records +`release_qualification: false`; the toolchain, image, acquisition and the +production reproduction profile remain unqualified. + +## Run + +The job +[`cargo test (confined release Cargo candidate, Linux)`](https://github.com/IABTechLab/trusted-server/actions/runs/38884296635/job/116918199285) +checked out the pull request merge commit +`5e2c68a5e30c6b4da7fa8ae83faf30c96c9511f3` for branch head +`bf3057d60f99a92693c0836305129ba68962afd5`. The +ignored test +`recipe::tests::full::fastly_canonical_and_reproduction_builds_are_byte_identical` +passed in 1,103.54 seconds and the denial-inheritance test passed again. + +## Results + +- The producer resolved workspace inheritance for all eight locked `edgezero` + packages: package fields, workspace dependencies with merged features and + relocated paths, and workspace lints. The originals are kept as + `Cargo.toml.orig`. +- Before each build, an evidence-only differential compared every producer + manifest with the one `cargo vendor` writes, on package identity, `links`, + features, every dependency's source, version, rename, optionality, default + features and features, and the build-script and procedural-macro settings: + 8 compared, 0 differing, for both roles. +- Each build recorded 8 producer-normalized manifest digests and 0 + caller-derived manifest digests, so no build is marked unqualified for its + manifests. +- The canonical (133 s) and reproduction (166 s) builds were byte-identical: + 66,833,914 bytes, SHA-256 + `8df10dfc2373dbec38fe5a21bdf6153dbb49bec51368034c475350c223368d88`. +- The Viceroy final-byte gate accepted both outputs and rejected the + closed-origin control as a health-only success. + +`cargo vendor` now supplies only comparison fixtures; nothing it writes is +trusted by the build. + +## Not covered + +The three `ts` executables, the production reproduction profile, an +independently reviewed Viceroy digest, authenticated acquisition, qualified +pins and hosted qualification remain open under Tasks 8 and 9. diff --git a/scripts/release/qualification/2026-10-19-apple-sdk-inputs.md b/scripts/release/qualification/2026-10-19-apple-sdk-inputs.md new file mode 100644 index 0000000000..dc389cf374 --- /dev/null +++ b/scripts/release/qualification/2026-10-19-apple-sdk-inputs.md @@ -0,0 +1,98 @@ +# Apple SDK input audit + +Status: local candidate-input investigation, not hosted qualification or +authenticated vendor acquisition. + +The selected local Xcode 16.4 installation exposes macOS SDK 15.5. An +independent read-only traversal confirmed the following layout on 2026-10-19: + +| Observation | Count | +| ------------------------------------ | ----------- | +| Entries below the selected SDK root | 46,233 | +| Directories | 8,781 | +| Regular files | 30,727 | +| Regular-file bytes across all paths | 656,877,420 | +| Largest regular file, bytes | 8,899,657 | +| Relative internal symlinks | 6,725 | +| Regular files with multiple links | 8,605 | +| Special files | 0 | +| Absolute, dangling or escaping links | 0 | + +These observations describe this installation only. They do not establish +either hosted runner's SDK layout, original distribution integrity or +release qualification. + +## Selection consequence + +The existing owned tool-tree policy rejects multiply linked regular files. +The installed SDK therefore cannot pass that policy directly. Hardlinked +headers are part of the input set; excluding documentation cannot solve the +problem. Hashing the original SDK before and after execution would also +leave an alias outside the selected tree able to modify its content. + +The selected implementation preserves the existing owned-tree policy: + +1. Copy the selected SDK into a fresh owned directory, reading vendor files + through held descriptors and creating independent destination files. +2. Preserve permission bits and relative internal symlinks. Reject special + files, unsafe aliases and exceeded bounds; do not preserve hardlinks or + silently omit inputs. +3. Validate the detached tree with the existing 512 MiB per-file, 2 GiB + total-byte and 100,000-entry limits. +4. Bind its content digest and lifetime to the selected Apple tools, select + that exact SDK in the recipe, grant it a read-only boundary root and + remap its temporary path to a fixed prefix. +5. Recheck it through the existing tool-verification hooks, using the + caller's deadline and cancellation. Record the tree digest as structured + installed-content evidence, not a raw archive checksum. + +This closes selected SDK-content coverage only when the implementation and +its actual native ARM/Intel recipe executions pass. Compiler resources, +helper executables, toolchain libraries such as libLTO, operating-system +inputs, authenticated acquisition and full native/tool closure remain +separate requirements. Overall composition coverage must remain incomplete. + +## Local detached-copy smoke + +The real installed-SDK smoke passed in 168.94 seconds. It copied and +measured all 46,233 entries and 656,877,420 regular-file bytes, reverified the +selected copy while retaining a cloned tool selection, and confirmed that +the copy disappeared after the last owner was dropped. The observed +structured-tree SHA-256 is +`72007de53280923111d03a4b0277e2d3cc986ec9594cfcc3c9807f2e97d02fc8`. +This is installed-tree evidence, not a distribution checksum or approved +production pin. + +The local machine lacks the selection symlink used by the CI constructor, +so this ignored smoke test used an explicit canonical developer directory +and the same private measurement/staging implementation. CI still requires +its selection symlink; no fallback or qualified constructor was added. + +Pure regressions cover modified headers/stubs/frameworks, unsafe aliases, +hardlinks in the destination, exhausted bounds, cancellation, stable +path-independent identities, final-byte path leaks and cleanup of restrictive +directory modes on error and final-clone drop. A review finding caught the +read-only-directory cleanup leak before the full smoke; its regression was +observed failing before the cleanup guard was added. + +## Hosted native recipe follow-up — 2026-10-20 + +[Run 39290139247](https://github.com/IABTechLab/trusted-server/actions/runs/39290139247) +finalized success for head `df4a6e5c30fe8c93df68748ead77a3bc559d16b2`. +Its actual merge source was `506d8ead69757556e1bd8ed29b578d0257d05169`. +Both Apple candidate recipes compiled, inspected and started their canonical +CLI artifacts using detached `/apple-sdk`, Xcode 16.4 and SDK 15.5.0. +Both emitted the same installed SDK tree identity: +`1482e88484d3ab4b430306607ed07113068eadd7581b232dedb20ea80842bce0`, +46,233 entries and 656,877,420 regular-file bytes. Their SDK-settings file +SHA-256 was `58a133735f0a55a624a1703067059f6e78925e51725ec6e1f966072e142c9c42`. +This hosted tree digest differs from the local smoke's installed tree; +neither is an authenticated distribution identity or production pin. + +ARM's second build was byte-identical. Intel's canonical artifact and +retained receipt exports passed; its optional second build failed on its +time bound. No Intel reproducibility result is inferred from the green job. +The successful fixtures additionally joined verified Rust packages through +the typed SPDX projection and checked retained export integrity after +dropping the original build storage. Complete native/tool coverage and +release qualification remain false. diff --git a/scripts/release/qualification/2026-10-20-apple-cli-candidates.md b/scripts/release/qualification/2026-10-20-apple-cli-candidates.md new file mode 100644 index 0000000000..b528625959 --- /dev/null +++ b/scripts/release/qualification/2026-10-20-apple-cli-candidates.md @@ -0,0 +1,157 @@ +# Task 9: Apple CLI candidates build, inspect and start on native macOS runners + +Status: ordinary PR CI built both Apple `ts` candidate recipes on native macOS +15 runners and started each canonical executable. Two earlier runs +(2026-10-19) and two later runs (2026-10-20) are recorded below. Every record +carries `release_qualification: false`. This is candidate evidence only: it +does not qualify production tools, the Xcode/SDK selection, acquisition or +release enablement, and macOS 15 qualification (Task 8) remains open. + +## Runs and jobs + +All four runs were `Run Tests` workflows on branch `spec/1235-release-attestation` +and concluded `success`. The Intel optional reproduction is a bounded +measurement; its failure does not fail the job (see below). + +| Run | Branch head | Built source (PR merge commit) | ARM job | Intel job | +| ------------------------------------------------------------------------------------ | ------------------------------------------ | ------------------------------------------ | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | +| [39273777887](https://github.com/IABTechLab/trusted-server/actions/runs/39273777887) | `d8fdb265748181f3d419b7902b3b0590a67c2ad9` | `cace4c508159bc7502d359e0cce49fef47d61f8b` | [118142122193](https://github.com/IABTechLab/trusted-server/actions/runs/39273777887/job/118142122193) | [118142122336](https://github.com/IABTechLab/trusted-server/actions/runs/39273777887/job/118142122336) | +| [39290139247](https://github.com/IABTechLab/trusted-server/actions/runs/39290139247) | `df4a6e5c30fe8c93df68748ead77a3bc559d16b2` | `506d8ead69757556e1bd8ed29b578d0257d05169` | [118196413038](https://github.com/IABTechLab/trusted-server/actions/runs/39290139247/job/118196413038) | [118196413106](https://github.com/IABTechLab/trusted-server/actions/runs/39290139247/job/118196413106) | +| [39417433680](https://github.com/IABTechLab/trusted-server/actions/runs/39417433680) | `bfab8ef2009fa6f28b1258135e2445ee22775dc8` | `b05bd8a18f137b43f7542c797f0a5d7856363d23` | [118617017385](https://github.com/IABTechLab/trusted-server/actions/runs/39417433680/job/118617017385) | [118617017568](https://github.com/IABTechLab/trusted-server/actions/runs/39417433680/job/118617017568) | +| [39420011152](https://github.com/IABTechLab/trusted-server/actions/runs/39420011152) | `1719cf15fd58fcf029f6b4ae5003215802bef405` | `b2273db76b8a2070f3c0d20d22788eed434d0029` | [118625917697](https://github.com/IABTechLab/trusted-server/actions/runs/39420011152/job/118625917697) | [118625917703](https://github.com/IABTechLab/trusted-server/actions/runs/39420011152/job/118625917703) | + +The job logs retain `macos-cli-candidate-measurement.json` (printed by the +workflow step) and the Cargo denial measurement for each host. The source +commit embedded in and measured for each executable is the merge commit above, +which is why digests differ between runs even though the build is +deterministic for a given source. + +## Hosts and tools + +Both hosts reported macOS product version 15.7.9 (ARM: `aarch64`, +`hw_optional_arm64: 1`, `proc_translated: 0`; Intel: `x86_64`). The selected +developer tools were Xcode 16.4 with SDK 15.5.0 and the boundary was +`macos-sandbox-exec`. + +| Measurement | Value | +| ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ | +| `clang` SHA-256 (ARM and Intel) | `4c458256bcdf913774de1bb4a37768244d3b41f32ad55afb2dfec3432f46f4fe` | +| ARM `ar` SHA-256 | `f362372395c1dd8ef349b6ceaad3b7958f1207a32e95d285721eba4c84ca55ae` | +| ARM `ld` SHA-256 | `648a8aac7f6f829a42305d24dddd7a3cfa48ee0bd9d4e7ec1ded5c51edea372b` | +| SDK settings SHA-256 | `58a133735f0a55a624a1703067059f6e78925e51725ec6e1f966072e142c9c42` | +| SDK tree, runs 39290139247 and later | 46,233 entries, 656,877,420 regular bytes, tree SHA-256 `1482e88484d3ab4b430306607ed07113068eadd7581b232dedb20ea80842bce0` on both ARM and Intel | +| SDK tree, run 39273777887 | ARM `e26d617a1870b3ecaf9bb0245da20662b0b8b6b66042ae15b2023a5fda37403f`, Intel `147b1549c1c3e34ca34a36ece451d474edb67e4a5343e8bf9f814b465bead527` | +| Native Cargo denial, control size/ARM | `denial-control-aarch64-apple-darwin`, 465,928 bytes, SHA-256 `c80e84f01196e51d420a0dffa0a4472db19ac956d51c67e0f66b104280abb8a8` | +| Native Cargo denial, control size/x86 | 451,072 bytes, SHA-256 `dadab281f3a2ec0542933a5b32ed4b0bb11a3d9add2d3499be477a1e47dff61d` | + +Run 39273777887 predates `df4a6e5c3` (detached SDK contents), so its SDK tree +digests differ between the two hosts. Runs from `df4a6e5c3` onward recorded +the same detached-tree digest on both. The Intel `clang` digest equals the ARM one in runs 39290139247 and +39417433680; other Intel tool digests are retained in the logs. + +Each host also passed the real native Cargo denial test +(`full_macos::cargo_children_inherit_denial_macos`): build-direct, +build-grandchild and procedural-macro attempts each failed with `errno` 1 +(`proof_errno` 1) under the boundary. The measurement reports +`descendant_cleanup: "post-run-reaped"`: after each run the sandbox instance's +processes are found, killed and the run is rejected if any survived. That is +weaker than a container owning its descendants, and only the `cfg(test)` +ordinary-CI tool selection accepts it (approved 2026-10-19). A qualified or +production selection still rejects the macOS boundary at the execute gate. + +## Executables and Mach-O facts + +Canonical builds used the same observed package, unit and build-script +counts in every run for a given host: ARM 486 packages, 649 units and 62 +build-script executions; Intel 487, 650 and 62. `material_native_coverage_complete` +stayed `false`. + +| Run | Host | Asset | SHA-256 | Size (bytes) | Canonical build (s) | +| ----------- | ----- | ------------------------- | ------------------------------------------------------------------ | ------------ | ------------------- | +| 39273777887 | ARM | `ts-aarch64-apple-darwin` | `65c68e611e3803cc9b80a0a48c218f629f3fa5752d787276a2b385a2f01874b9` | 43,260,288 | 663 | +| 39273777887 | Intel | `ts-x86_64-apple-darwin` | `c9da6682c713ecf597e5c85a2c01992af26294493b6249c6a9bb4a6fa5c6f54f` | 46,114,496 | 1,101 | +| 39290139247 | ARM | `ts-aarch64-apple-darwin` | `071ba0375ade6195552efaf363a384c8dd8684a9cfd8033c8c207a2b34def59d` | 43,260,288 | 560 | +| 39290139247 | Intel | `ts-x86_64-apple-darwin` | `3bfa14727f66915b76e95ce5445d5e19b68d11be78ba8caf69be6c1e3011ac42` | 46,114,496 | 1,140 | +| 39417433680 | ARM | `ts-aarch64-apple-darwin` | `923e5f7bddbfdb67596e45cdfe3adfcd5aaab353af94fb4f697d05ab4a32cd45` | 43,260,288 | 612 | +| 39417433680 | Intel | `ts-x86_64-apple-darwin` | `b892eb2a08c66c6ccbf945224a5e42ca1e59388ed0ae8b48d203c0a3c40ec367` | 46,114,496 | 879 | +| 39420011152 | ARM | `ts-aarch64-apple-darwin` | `1c28da620cb7654c5b376a7e09eff889fabc62cdc6e100a12b2c4d913c17e187` | 43,260,288 | 602 | +| 39420011152 | Intel | `ts-x86_64-apple-darwin` | `1fdceeae363ed7afe97f28056170240194eebcf1f3b216f7880add0d0fdf744d` | 46,114,496 | 914 | + +Mach-O inspection of the accepted canonical executable (identical across runs +except where noted): + +| Check | ARM | Intel | +| ---------------------- | ------------------------------------------------------------------------------- | ---------- | +| CPU | `arm64` | `x86_64` | +| Minimum OS (`minos`) | `13.0.0` | `13.0.0` | +| SDK | `15.5.0` | `15.5.0` | +| Signature | `linker-signed-ad-hoc` | `unsigned` | +| Dynamic libraries | Security, CoreFoundation, `libiconv.2`, `libSystem.B` | same four | +| Symbol count | 305,558 | 306,211 | +| Debug-map object files | 1,426 | 1,422 | +| Operator values | Three planted fictional variables removed by preflight; zero final-byte matches | same | + +Mach-O UUIDs differ per run because the embedded source commit differs (for +example ARM `ea89f38109c83fbea6bed4ac7de8f98b` and Intel +`47678b08fa0a38bcba792ffc65173f1f` in run 39290139247). The final-byte scan +rejected host build roots, the fixture root and `RUNNER_TEMP`, and found only +the remapped prefixes `/trusted-server` and `/cargo-vendor`. + +## Startup + +The accepted canonical executable was run as `ts --version` outside the +checkout with a scrubbed environment (`HOME`, `LC_ALL`, `PATH`, `TMPDIR` +only). Every start, in every run on both hosts, reported `status: accepted` +with exit code 0 and `ts package=0.1.0 source= target= variant=default`, for example (run 39290139247, ARM): + +```text +ts package=0.1.0 source=506d8ead69757556e1bd8ed29b578d0257d05169 target=aarch64-apple-darwin variant=default +``` + +The Intel output names `target=x86_64-apple-darwin`. The measurements record the host CPU and, on ARM, translation state +(`hw_optional_arm64: 1`, `proc_translated: 0`). This exercises loading and +version reporting on macOS 15.7.9 only. It does not exercise the macOS 13 +loader floor on a real macOS 13 host or every CLI command. + +## Reproduction measurement + +The reproduction (role B) uses separate fresh roots and the checked ambient +profile (umask 027, two jobs, different roots and start time). It is a +bounded measurement, never a release equality gate. + +| Run | ARM reproduction | Intel reproduction | +| ----------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------- | +| 39273777887 | `identical` (same SHA-256 and size as canonical) | `build-failed`: the confined Cargo build exceeded its wall-clock deadline (`sandbox-exec exceeded its wall-clock deadline`) while compiling | +| 39290139247 | `identical` | `build-failed`: `Release source is not authenticated` caused by `source preparation cancelled or timed out` | +| 39417433680 | `identical` | `identical` | +| 39420011152 | `identical` | `identical` | + +No Intel equality result exists for the two earlier runs and none is inferred. +The two later Intel results were identical, but the whole Intel candidate test +took 3,552 seconds (run 39417433680) and 3,305 seconds (run 39420011152) +against a 60-minute test deadline, after 3,844 seconds in run 39290139247 +where it was exceeded. No code change was made to widen that margin, so this +is a timing observation: it does not establish that Intel reproduction is now +reliable. Intel remains a bounded optional measurement. + +## Subsequent reproduction scheduling + +`3e3e30996` defers optional native reproduction on ordinary push/PR runs; +scheduled/dispatched runs and PRs carrying the reproduction label before a +triggering event measure it. Canonical Apple build/inspection/startup remain +mandatory. Run 39540057202 subsequently passed both canonical candidates, with +ARM reproduction identical and Intel reproduction failing at its 25-minute +confined-build wall. `b4c8e0c0f` raises that ordinary-CI wall to 45 minutes; +it does not establish Intel equality. See the [dated schedule report](2026-10-21-native-reproduction-schedule.md) +for exact jobs, built source and trigger limitations. Future equality statements +must cite the measured run/job and built source; a deferred pass supplies none. + +## Limits + +This evidence does not establish authenticated acquisition of Xcode, the SDK +or the Rust tree; original vendor identity; compiler resource, `libLTO` and +helper-tool coverage; production reproduction profiles; macOS 13 loader +behavior; or Task 8 qualification. The JS handoff came from a Linux ordinary-CI +artifact marked `authenticated: false`. Production wrappers and qualified +selection remain closed, and nothing here signs, publishes or enables an +official release. diff --git a/scripts/release/qualification/2026-10-20-linux-cli-gnu-follow-up.md b/scripts/release/qualification/2026-10-20-linux-cli-gnu-follow-up.md new file mode 100644 index 0000000000..b046213e07 --- /dev/null +++ b/scripts/release/qualification/2026-10-20-linux-cli-gnu-follow-up.md @@ -0,0 +1,153 @@ +# Task 9: Linux CLI candidate after the GNU snapshot and measurement fixes + +Status: ordinary PR CI passed the Linux `ts` candidate recipe again on +2026-10-20 after two hosted failures were fixed. The canonical and +reproduction builds were byte-identical and the exact accepted bytes started +in all three reviewed Linux images. This is candidate evidence with +`release_qualification: false`. It extends the +[2026-10-16 Linux CLI candidate report](2026-10-16-linux-cli-candidate.md) and +does not qualify production tools, image pins, acquisition or release +enablement. + +## Why the earlier candidate stopped passing + +After the 2026-10-16 pass, the candidate recipe gained source-owned GNU tool +selectors and a staged helper that records a bounded installed-image +inventory before and after Cargo. Two hosted runs then failed in the Linux CLI +candidate job; each failure was fixed by one commit. + +| Run | Head | Failed job | Recorded failure | Fix | +| ------------------------------------------------------------------------------------ | ----------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------- | +| [39371724671](https://github.com/IABTechLab/trusted-server/actions/runs/39371724671) | `7be86d935` | [118460869596](https://github.com/IABTechLab/trusted-server/actions/runs/39371724671/job/118460869596) | `cannot open image file: Permission denied (os error 13)` after 557.93 seconds | `e5a780d5c`: record three reviewed root-only helpers by metadata only | +| [39382624785](https://github.com/IABTechLab/trusted-server/actions/runs/39382624785) | `e5a780d5c` | [118494982595](https://github.com/IABTechLab/trusted-server/actions/runs/39382624785/job/118494982595) | `file .../release/build/prettyplease-.../build-script-build has 2 hard links; a link outside the root would bypass write denial` after 1,273.09 seconds | `bfab8ef20`: measure the image in a sibling sandbox without the target root | + +Both failures were in the real candidate test +`recipe::tests::cli::linux_cli_candidate_builds_inspects_and_starts_cleanly`; +other jobs in those runs are not summarised here. Run 39371724671 also had a +cancelled Intel macOS job, which is outside this report. + +### Reviewed root-only helpers (`e5a780d5c`) + +The confined GNU helper runs unprivileged and hashes every regular file under +the measured image roots. The UBI 9 builder ships three privileged helpers it +cannot open: `/usr/libexec/utempter/utempter` (setgid, mode 2711), +`/usr/sbin/userhelper` (setuid, 4711) and `/usr/sbin/unix_update` (0700), +observed identically on amd64 and arm64 by a local Podman build of the same +builder Dockerfile, listing files unreadable to a non-root user under the +measured roots (local diagnostic, not CI output). The snapshot now accepts +a permission-denied file only at those exact paths, owned by root, with no +group or other write bit, no other-read permission and unchanged since first +inspection. Each is recorded by mode, owner and size instead of content +(inode is excluded because it varies with host storage) and listed as +`content_unmeasured_files`, which the orchestrator requires to be a sorted +subset of the reviewed paths. Every other unreadable file, including an +execute-only compiler helper, still fails closed, and selected build tools +must remain readable and hashed. These three files' content is therefore +unmeasured; the SPDX candidate states the count as a coverage gap. + +### Sibling measurement sandbox (`bfab8ef20`) + +The second GNU image measurement ran in the build boundary, whose unchanged +pre-run writable scan rejects Cargo's in-root hard links in the target +directory. Both GNU measurements now run in a sibling Linux container +boundary derived from the build boundary: same image, settings, read-only and +tool roots, and every writable root except the last (the target directory). +The sibling has a fresh fingerprint, records the boundary it derived from and +proves denial with the staged probe. Cargo queries and the build keep the +build boundary and proof, receipts record both fingerprints, and completion +still binds the build boundary and plan. + +## Passing runs and jobs + +| Run | Head | Built source (PR merge commit) | Linux CLI job | Test duration | +| ------------------------------------------------------------------------------------ | ------------------------------------------ | ------------------------------------------ | ------------------------------------------------------------------------------------------------------ | ------------- | +| [39417433680](https://github.com/IABTechLab/trusted-server/actions/runs/39417433680) | `bfab8ef2009fa6f28b1258135e2445ee22775dc8` | `b05bd8a18f137b43f7542c797f0a5d7856363d23` | [118609761876](https://github.com/IABTechLab/trusted-server/actions/runs/39417433680/job/118609761876) | 2,124.97 s | +| [39420011152](https://github.com/IABTechLab/trusted-server/actions/runs/39420011152) | `1719cf15fd58fcf029f6b4ae5003215802bef405` | `b2273db76b8a2070f3c0d20d22788eed434d0029` | [118618316864](https://github.com/IABTechLab/trusted-server/actions/runs/39420011152/job/118618316864) | 2,089.82 s | + +Both runs concluded `success` for every job, including the Fastly confined +Cargo job, the native producer jobs on Ubuntu 24.04 and macOS 15, and the +Apple candidate jobs (see the [Apple CLI candidates report](2026-10-20-apple-cli-candidates.md)). +The run for the later documentation-and-SPDX head `073704cc7` (Run Tests 39452090186) was still in progress when this report was written and is not +recorded. + +## Measured results + +| Measurement | Run 39417433680 (`bfab8ef20`) | Run 39420011152 (`1719cf15f`) | +| ------------------------------------- | ------------------------------------------------------------------------- | ------------------------------------------------------------------------- | +| Asset | `ts-x86_64-unknown-linux-gnu`, variant `default`, package `0.1.0` | same | +| SHA-256, canonical and reproduction | `066000e58902ad9e8b2077498383c27f466fd45337320082eab20da83a28df7d` | `99991c0ae4a34b0db1630925df5e0ca1ef4aa0c3ca17f30d9a162ed746923245` | +| Size, both builds | 282,956,784 bytes | 282,956,920 bytes | +| Reproduction status | `identical`, no differing offset | `identical`, no differing offset | +| Canonical Cargo execution | 473 seconds | 454 seconds | +| Observed packages/units/build scripts | 486 / 649 / 62 | 486 / 649 / 62 | +| Lock SHA-256 | `82ef943339970e6399fe56f434beee484a05e7a0fe0db40383a794651ab19214` | `82ef943339970e6399fe56f434beee484a05e7a0fe0db40383a794651ab19214` | +| Descendant cleanup | `owned-by-container` | not tabulated; same boundary | +| Builder image | `registry.access.redhat.com/ubi9/ubi:9.6`, glibc 2.34 | same reference, glibc 2.34 | +| Observed builder image ID | `sha256:417c122225da0b6990c9f50c32a7b3908022e30df6d5480b572b45be9dd4d03c` | `sha256:b4eae457404e63633888ffaff3cd5020c6ffb4cc4fb495657a8945698ebdd5a0` | + +The builder image ID differs between the two runs for the same mutable tag +and both differ from the 2026-10-16 report, which is further reason these are +observations from ordinary tag acquisition and not authenticated production +digests. The executable bytes differ between runs because the embedded source +commit differs, and the size and digest of this binary also differ from the +2026-10-16 report because the source and lock changed. + +Final-byte ELF inspection of the canonical executable (run 39417433680; run +39420011152 reported the same ELF facts): x86-64, interpreter +`/lib64/ld-linux-x86-64.so.2`, highest required GLIBC version `2.34` across 27 +version requirements, needed libraries `libgcc_s.so.1`, `libm.so.6`, +`libc.so.6` and `ld-linux-x86-64.so.2`, `.debug_info` 87,947,690 bytes and +`.debug_line` 33,663,154 bytes. The path scan found only the remapped +prefixes `/trusted-server`, `/cargo-vendor` and `/cargo-target` and none of +`/boundary/`, host build roots, fixture roots or `RUNNER_TEMP`; the three +planted fictional operator variables were removed by preflight with zero +final-byte matches. `material_native_coverage_complete` stayed `false`. + +The builder filesystem controls again exited zero: `/tmp`, `/dev/shm`, `/dev`, +`/etc/hosts`, `/etc/hostname` and `/etc/resolv.conf` writes denied, and the +bound `TMPDIR` accepted write, read and cleanup. + +## Startup + +The copied read-only executable ran as `ts --version` without a checkout or +operator configuration in each reviewed image. All starts exited zero with an +empty stderr (SHA-256 of empty input) and, in run 39420011152: + +```text +ts package=0.1.0 source=b2273db76b8a2070f3c0d20d22788eed434d0029 target=x86_64-unknown-linux-gnu variant=default +``` + +| Startup image | Observed local image ID (both runs) | +| ------------------------------------------------- | ------------------------------------------------------------------------- | +| `registry.access.redhat.com/ubi9/ubi-minimal:9.6` | `sha256:a2c5a85865a585c3bc8b10f6c269358fdf89fe32be4232885166889d85c76421` | +| `docker.io/library/ubuntu:22.04` | `sha256:cee13550f97b8a595506e8e4ca74263ba44924248779a4ba4f942e197652acff` | +| `docker.io/library/debian:12` | `sha256:94926f0fd968cd68ac486502185a5dd78687874a9ec7727f64fe2af696e28311` | + +The `ubuntu:22.04` image ID differs from the 2026-10-16 report +(`sha256:e8df6c74...`), showing the tag moved between runs. Run +39417433680 recorded the same three IDs. The real startup timeout regression +(`startup_timeout_removes_only_its_owned_container`) also passed in run 39417433680. + +## Subsequent reproduction scheduling + +The equality observations above remain tied to their original runs. Since +`3e3e30996`, ordinary push/PR runs defer the optional native reproduction; +scheduled/dispatched or appropriately labelled PR runs measure it. Linux +canonical build, inspection and all three startup checks remain mandatory. +See the [dated schedule report](2026-10-21-native-reproduction-schedule.md) +for the selector, exact baseline jobs and the subsequent 45-minute ordinary-CI +per-build wall. Later equality claims need a measured run/job and built source, +not a successful deferred job. GNU helper coverage and qualification gaps remain. + +## Limits + +The GNU installed-file snapshot and the sibling measurement record which files +were present and, except for three root-only helpers recorded by metadata, their +content hashes. They do not establish which helpers executed, authenticated +acquisition of the UBI image or tools, or a complete native closure. The +candidate SPDX for this build must list the privileged-helper gap. Native +reproduction remains a measurement rather than a release gate. Task 9 and +milestone M4 remain open: qualified production ambient profiles, the closed +`build.sh` interface, independently authenticated acquisition/tool/image +selections and Task 8 hosted qualification are still required. This candidate +does not sign, publish, deploy or enable an official release. diff --git a/scripts/release/qualification/2026-10-21-candidate-release-index.md b/scripts/release/qualification/2026-10-21-candidate-release-index.md new file mode 100644 index 0000000000..85398500a5 --- /dev/null +++ b/scripts/release/qualification/2026-10-21-candidate-release-index.md @@ -0,0 +1,90 @@ +# Task 13 candidate release index evidence + +Status: partial candidate implementation, without signing, publication or +release authority. The production Task 13 exit remains open. + +## Implemented boundary + +`crates/trusted-server-release/src/distribution.rs` and its `entry`, `joins` +and test-only `summary` modules retain candidate entries and exact-cohort index +assembly. `candidate_entry_from_completed` takes a canonical completed owner +with its matching actual runtime/startup acceptance; Fastly additionally requires +owning canonical/reproduction equality. It renders candidate SPDX, build-evidence, +build-recipe and rebuild-check companions. The candidate entries bind final-byte +digests and the retained original JS inventory. + +The index assembler requires exactly the four fixed canonical assets with one +source commit, policy identity and JS inventory, and checks the retained v1 +schema and mirrored consumer joins. It uses tag `v0.0.0-unqualified-candidate` +and name `candidate-release-index.json`. Signature slots contain size zero and +the actual empty-file SHA-256 sentinel; `BundleSlot::Signed` requires the +uninhabited `SignedCompanions` type. No signature file is fabricated. Companions +retain `release_qualification: false`, explicit gaps and null workflow/transfer +identities. The CLI rejects zero-size signature bundles, so these fixtures cannot +pass full release verification. Consumer rejection of sentinel/unqualified +documents after future signing remains a separate tracked requirement. + +Golden fixtures in `tests/fixtures/index/candidate/` and CLI +`release/evidence/candidate_fixture_tests.rs` exercise unsigned structural joins +and rejection boundaries. `recipe/tests/candidate_index.rs` covers pure owner +wiring rejections. `candidate_entry.rs` checks each real canonical entry +independently, with reporting in `distribution/summary.rs`. Collecting and +assembling the four real entries across CI jobs is now implemented. Each +candidate job uploads its rechecked entry and final executable. The +`test-release-candidate-index` job rebuilds the four entries from the exact +uploads (`distribution::transfer` and `distribution::ordinary`, test-only) and +assembles them. It then runs the CLI consumer's unsigned joins over the +result. The first hosted all-four assembly passed in run 39577910126; see the +[four-asset candidate join report](2026-10-21-four-asset-candidate-join.md). + +## Real canonical-entry observations + +The initial real-entry wiring in `4483e8256dc52c89e615f1388daa873324a871f5` +has [run 39529922087](https://github.com/IABTechLab/trusted-server/actions/runs/39529922087). +REST job conclusions show Fastly +[118993209872](https://github.com/IABTechLab/trusted-server/actions/runs/39529922087/job/118993209872), +Linux CLI [118993210395](https://github.com/IABTechLab/trusted-server/actions/runs/39529922087/job/118993210395) +and Apple ARM [118998766555](https://github.com/IABTechLab/trusted-server/actions/runs/39529922087/job/118998766555) +succeeded; Intel [118998766733](https://github.com/IABTechLab/trusted-server/actions/runs/39529922087/job/118998766733) +and the overall run were cancelled. These conclusions establish three completed +jobs, not an all-four result. Their measurements are not transcribed here; +the completed baseline below supplies the audited entry identities and outcomes. + +[Run 39540057202](https://github.com/IABTechLab/trusted-server/actions/runs/39540057202) +concluded success for head `a9994b4c2b50265de4bbe5b8bb42cd8cab4e43ae`, building +source `d0cd53343fcbe1fb6f06eafc009e306e24b1245d`. Its logs retain: + +| Asset | Job | Candidate entry SHA-256 | Entry bytes | Entry rebuild outcome | +| --------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------ | ----------- | --------------------- | +| Fastly Wasm | [119025502524](https://github.com/IABTechLab/trusted-server/actions/runs/39540057202/job/119025502524) | `45ba885e8a210d6c1b6db92ff67fff9f4d3574370a118b7aa41be359831d5631` | 1,488 | `matched` | +| Linux GNU CLI | [119025502758](https://github.com/IABTechLab/trusted-server/actions/runs/39540057202/job/119025502758) | `668c22fd33c959c269289192671630275939a6f0c324de36e13547b1db701c47` | 1,492 | `unavailable` | +| Apple ARM CLI | [119029080050](https://github.com/IABTechLab/trusted-server/actions/runs/39540057202/job/119029080050) | `23d1b2beaeaeeae048b573afaaecd10be8c20d4b9f068cad6fb8f54d07d7a49e` | 1,450 | `unavailable` | +| Apple Intel CLI | [119029080038](https://github.com/IABTechLab/trusted-server/actions/runs/39540057202/job/119029080038) | `336b4b3876205dde71cfd99f1726455953e950636544a67d6193fa4f9633fa63` | 1,441 | `unavailable` | + +These are entry-document digests, not executable or SBOM digests. The native +entry outcome is deliberately conservative: separate fixture measurements show +Linux and ARM reproduction `identical`, while Intel reproduction exhausted its +wall budget. Those measurements do not become owning rebuild proofs in the +candidate native entry. A passing Intel canonical entry does not imply equality. + +All four jobs recorded JS inventory SHA-256 +`24efd45b403e955eb4cb98c0b5e9f31e7f878d0be2637d2f89447660383511f6` +for that same built source. This supports inventory compatibility for a future +join of these observations; it does not establish that a real four-job join ran. +An inventory digest includes source-specific metadata, so values from different +built commits are not interchangeable. Other runs need their own verified values. + +## Remaining exits + +Task 12's trusted workflow dependency and transfer binding is the next +prerequisite to a trusted cross-job candidate join. It is not the only remaining +release-attestation work. The logs explicitly retain missing authenticated +bootstrap, incomplete native material coverage, unresolved Fastly host tools, +three GNU privileged helpers with unmeasured content, incomplete Apple tool +closure, absent signatures and absent trusted workflow/run identities. + +Complete qualified SPDX generation, authentic provenance subjects, actual +signature bundles, SHA256SUMS, immutable publication and final qualification +remain required. Builder/startup images still acquired by mutable tags do not +become authenticated because their installed IDs were recorded. This report +does not implement the transfer adapter or change any production gate. diff --git a/scripts/release/qualification/2026-10-21-four-asset-candidate-join.md b/scripts/release/qualification/2026-10-21-four-asset-candidate-join.md new file mode 100644 index 0000000000..a65ab5fe6b --- /dev/null +++ b/scripts/release/qualification/2026-10-21-four-asset-candidate-join.md @@ -0,0 +1,122 @@ +# Tasks 12 and 13: first cross-job four-asset candidate index + +Status: ordinary PR CI assembled one unqualified candidate release index from +the four real candidate jobs on 2026-10-21. This is candidate integration +evidence with `release_qualification: false`. It is not a signed release, +trusted-workflow binding, authenticated acquisition or qualification, and it +extends the [candidate release index report](2026-10-21-candidate-release-index.md). + +## Run + +| Field | Value | +| ------------------------ | ------------------------------------------------------------------------------------ | +| Run | [39577910126](https://github.com/IABTechLab/trusted-server/actions/runs/39577910126) | +| Event | `pull_request` | +| PR head (REST head) | `d774ee47c274257fa0a088280f7df37ed530020c` | +| Built source (PR merge) | `fb42d8268af93c011959d032c81bfa581306c7c2` | +| Native reproduction mode | `defer` (recorded as `deferred-to-scheduled-run`) | +| Index evidence artifact | `unqualified-candidate-release-index-evidence-39577910126-1`, ID 12340882671, 7 days | + +The previous run, 39567710071 at `6ffebbd08`, completed all four producer jobs +and both macOS matrix outputs, but its collector rejected the first download: +it passed one 20-minute deadline while the bounded transport caps an attempt at +15 minutes. `d774ee47c` gives each download its own 10-minute attempt and +leaves the transport bound unchanged. + +## Jobs + +| Job | Name | Duration | +| ------------------------------------------------------------------------------------------------------ | --------------------------------------------------- | -------- | +| [119147931992](https://github.com/IABTechLab/trusted-server/actions/runs/39577910126/job/119147931992) | Confined release Cargo candidate (Fastly), Linux | 33 min | +| [119147931961](https://github.com/IABTechLab/trusted-server/actions/runs/39577910126/job/119147931961) | Linux `ts` CLI release candidate | 31 min | +| [119148070099](https://github.com/IABTechLab/trusted-server/actions/runs/39577910126/job/119148070099) | Linux JS handoff for macOS candidates | 14 min | +| [119154065068](https://github.com/IABTechLab/trusted-server/actions/runs/39577910126/job/119154065068) | macOS `ts` CLI release candidate, `macos-15` | 38 min | +| [119154065062](https://github.com/IABTechLab/trusted-server/actions/runs/39577910126/job/119154065062) | macOS `ts` CLI release candidate, `macos-15-intel` | 54 min | +| [119176658773](https://github.com/IABTechLab/trusted-server/actions/runs/39577910126/job/119176658773) | Assemble the unqualified four-asset candidate index | 15 min | + +## What the collector checked + +Each candidate job exported its rechecked entry companions, the shared JS +inventory and its final executable, uploaded them with the pinned upload +action and passed the exact artifact ID, name and digest through job outputs. +The collector, which runs only when all three candidate jobs succeed on a +same-repository event, then: + +- parsed four descriptors strictly and required distinct upload IDs, each + name recomputed from the fixed asset slot, run ID and producer attempt; +- downloaded each upload by exact ID through the bounded release-tool backend + and corroborated run, head, repository, name, digest and expiry in raw REST + metadata, then read the exact six flat ZIP members; +- authenticated a clean clone at the built source and read the consumer + policy from it, not from any producer; +- rebuilt each entry through the test-only `from_ordinary_ci_transfer`, which + reruns the inventory, build-evidence, SPDX, recipe and rebuild joins and + rehashes the executable against the recorded size and digest; +- required each rebuilt entry digest to equal the digest its producer + reported, and assembled the index with the unchanged assembler; +- reread the written index and its 17 sibling files and checked their + digests, the sentinel tag and the empty signature-bundle sentinels. + +The CLI consumer test then accepted the assembled bytes under its unsigned +joins and policy and rejected the size-zero signature bundles. + +## Entries + +| Asset | Producer entry SHA-256 | Executable SHA-256 | Executable bytes | +| ----------------------------- | ------------------------------------------------------------------ | ------------------------------------------------------------------ | ---------------- | +| `trusted-server-fastly.wasm` | `93468d7d8aa4dcd066fe3b42c38943f09a732d3bc858d4ae9ed3c584c2482dc3` | `d43c4412c9b01daf6c74add4cbde01a6a6699b7b1959f9c15f0426c84e08ba63` | 66,992,633 | +| `ts-x86_64-unknown-linux-gnu` | `46676743d02c252b8a373a9242a4baf4dec3344003af44c6c314240533319db2` | `21c55e583569a84f353181022efdfd43d8310caf5fdf3fc58eba18317f8acc61` | 283,190,976 | +| `ts-aarch64-apple-darwin` | `d6984e55e659999c9c5c0575ac1f95c89bf70b25c85157d98638edd49ed1d4d2` | `f715aa69b467db23b846efbede73d5acc3313fa605a431115ba75c854a88f088` | 43,244,624 | +| `ts-x86_64-apple-darwin` | `b9cf2e517615876d8417a8c01fa1891504bb4ea9154dda97c1fb1405f282e03a` | `6826bed7761f62c84587f45a08f4493bab67a587b9346570cdd73ecf48220959` | 46,159,016 | + +## Assembled index + +| Field | Value | +| ------------------------------ | --------------------------------------------------------------------------------- | +| `candidate-release-index.json` | `4872ef96086e93f720187f363e7262ae2921e663fbc9a531f2a42cc5d803b14d` | +| Tag | `v0.0.0-unqualified-candidate` | +| Cohort | `phase1-fastly-cli` | +| Source, recipe and cohort | `fb42d8268af93c011959d032c81bfa581306c7c2` | +| Policy | version 1, `f08ce10cd16ff2d1acc482db2b85eb53b667ec2b4753792f277d12f41d1f636c` | +| `tsjs-inventory.json` | `90c79b953ca112310707d52100ebaefe6c97718466242c34ade46b188a54b729`, 246,440 bytes | + +All four candidate jobs built the same JS inventory bytes for this source. The +inventory digest differs from the one in the earlier candidate index report +because the source differs. + +The collector printed these unqualified candidate gaps, carried unchanged in +the companions: + +```text +apple-coverage-excluded:toolchain_libraries +apple-tool-closure-unmeasured +authenticated-bootstrap-not-established +candidate-tag-sentinel +gnu-original-acquisition-unverified +gnu-privileged-helper-content-unmeasured +gnu-tool-closure-unmeasured +native-material-coverage-incomplete +native-reproduction-unavailable +native-tool-host-path-unresolved:archiver +native-tool-host-path-unresolved:assembler +native-tool-host-path-unresolved:build_tools +native-tool-host-path-unresolved:compiler +native-tool-host-path-unresolved:linker +signature-bundles-awaiting-signature +trusted-workflow-transfer-not-bound +workflow-run-identity-unavailable +``` + +## Limits + +The exact-ID transfer and REST corroboration prove which uploads of this run +were read, not which job or attempt produced them: the artifact API does not +identify the producing job, so trusted workflow dependency binding (Task 15) +remains open and companions keep null workflow and transfer IDs. The +collector's `GH_TOKEN` is read-only and visible to the same-repository test +binary. The index is unsigned, uses the sentinel tag and cannot pass full +release verification. Native reproduction was deferred for this run, so the +three native entries record rebuild outcome `unavailable`; Fastly records +`matched`. Tasks 12 and 13 remain partial; signing, publication, authenticated +acquisition, complete native coverage and qualification are separate open +exits. diff --git a/scripts/release/qualification/2026-10-21-native-reproduction-schedule.md b/scripts/release/qualification/2026-10-21-native-reproduction-schedule.md new file mode 100644 index 0000000000..c4f5efc214 --- /dev/null +++ b/scripts/release/qualification/2026-10-21-native-reproduction-schedule.md @@ -0,0 +1,77 @@ +# Ordinary CI native reproduction scheduling and build cache + +Status: candidate CI optimization, not release qualification. Commit +`3e3e30996` changes when native reproduction is measured; `b4c8e0c0f` +increases the ordinary-CI confined Cargo build wall from 25 to 45 minutes. +Neither change establishes Intel equality or authenticates tools or images. + +## Selection and triggers + +The Linux and Apple CLI fixtures require `TS_RELEASE_NATIVE_REPRODUCTION` +to be `measure` or `defer`. The workflow selects `measure` for `schedule`, +`workflow_dispatch`, or a pull request carrying `release-native-reproduction`; +other push and pull-request runs select `defer`. Deferred runs record +`status: deferred-to-scheduled-run` and `gate: measurement-only`. The fixtures +reject missing/unknown modes and reject `defer` on scheduled/dispatched runs. +Canonical native build, inspection, startup and evidence checks remain required. +Fastly canonical/reproduction equality and final-byte runtime checks are unchanged. + +The cron is `23 4 * * *` (04:23 UTC daily). GitHub runs scheduled workflows +from the default branch; dispatch also requires the workflow file to exist on +that branch, although a dispatch can select another ref. These triggers do not +by themselves validate this unmerged branch. The PR label must be present +before the next push or another configured PR event: this workflow does not +subscribe to the `labeled` event. See GitHub's +[schedule](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#schedule) +and [dispatch](https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#workflow_dispatch) +contracts. This report does not dispatch a run or change labels. + +## Cache scope and limits + +The pinned `actions/cache` v5.1.0 restore/save steps cache `target`, keyed by +runner OS, host target, pinned Rust version, lock/tool/manifest hashes and test +profile. The Linux Fastly producer job and Apple candidate jobs save only on +`refs/heads/main` and an exact-key miss, before ignored execution fixtures; +Linux CLI and JS handoff jobs restore without saving. Incremental directories +are removed before saving. This cache does not include `~/.cargo/registry` +and does not replace fresh confined application build roots. + +Measurements retain `ci_build_provenance`: producer/probe and test-binary +SHA-256 values plus `build_cache` (matched key, `miss`, or `unreported`). These +are observed identities, not independently authenticated provenance. Main-only +writes in this workflow and GitHub's branch-scoped cache access reduce sharing; +they do not qualify cached code or make a digest proof of trusted compilation. +See GitHub's [cache access restrictions](https://docs.github.com/en/actions/reference/workflows-and-actions/dependency-caching#restrictions-for-accessing-a-cache). +All measurements remain `release_qualification: false`. + +## Observed baseline and pending validation + +[Run 39540057202](https://github.com/IABTechLab/trusted-server/actions/runs/39540057202) +concluded success for head `a9994b4c2b50265de4bbe5b8bb42cd8cab4e43ae`, building +PR merge source `d0cd53343fcbe1fb6f06eafc009e306e24b1245d`. It predates both +optimizations and supplies the timing baseline, not evidence that they work. + +| Candidate | Job | Observed result | +| --------------- | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | +| Fastly | [119025502524](https://github.com/IABTechLab/trusted-server/actions/runs/39540057202/job/119025502524) | Candidate entry rebuild outcome `matched` | +| Linux CLI | [119025502758](https://github.com/IABTechLab/trusted-server/actions/runs/39540057202/job/119025502758) | Native reproduction `identical` | +| Apple ARM CLI | [119029080050](https://github.com/IABTechLab/trusted-server/actions/runs/39540057202/job/119029080050) | Native reproduction `identical`; complete fixture 3,144.22 seconds | +| Apple Intel CLI | [119029080038](https://github.com/IABTechLab/trusted-server/actions/runs/39540057202/job/119029080038) | Canonical Cargo execution 1,193 seconds; optional reproduction `build-failed` at its wall deadline; complete fixture 4,093.14 seconds | +| JS handoff | [119025605842](https://github.com/IABTechLab/trusted-server/actions/runs/39540057202/job/119025605842) | Fresh offline JS fixture passed in 474.23 seconds; this is fixture time, not total job time | + +The Intel reproduction timeout previously occurred in run 39273777887 and +recurred here, despite `identical` measurements in intervening historical runs +39417433680 and 39420011152 (see the [Apple report](2026-10-20-apple-cli-candidates.md)). +`ToolSelection::build_wall` now supplies 45 minutes for ordinary +CI instead of the fixed 25-minute wall. This is a per-confined-build budget, +not the complete fixture or job budget, and production selection remains closed. + +At this report's evidence check on 2026-10-21, the first run for +`b4c8e0c0fded27f02930edc6de990f17a63f5820`, +[39551207134](https://github.com/IABTechLab/trusted-server/actions/runs/39551207134), +was still in progress. Deferred reproduction, cache behavior, recorded producer +identities and unchanged mandatory Fastly equality require that run's completed +logs. A PR cache miss is expected when no eligible entry exists, not guaranteed; +main-only save steps must remain skipped on the PR. Intel equality under the +45-minute wall requires a later measured run; a deferred pass cannot establish it. +Every later equality claim must identify the measured run, job and built source. diff --git a/scripts/release/release-cohort.json b/scripts/release/release-cohort.json new file mode 100644 index 0000000000..d66e0b3f09 --- /dev/null +++ b/scripts/release/release-cohort.json @@ -0,0 +1,133 @@ +{ + "artifacts": [ + { + "assurance_profile": "provenance-sbom-repeatable", + "bin": "trusted-server-adapter-fastly", + "capabilities": ["fastly-production"], + "compatibility": { + "architecture": "wasm32", + "minimum_version": "wasip1", + "runtime": "fastly-compute" + }, + "default_features": true, + "features": [], + "kind": "wasm-core", + "name": "trusted-server-fastly.wasm", + "package": "trusted-server-adapter-fastly", + "role": "server", + "target": "wasm32-wasip1", + "variant": "default" + }, + { + "assurance_profile": "provenance-sbom", + "bin": "ts", + "capabilities": ["cli-execute"], + "compatibility": { + "architecture": "aarch64", + "minimum_version": "13.0", + "runtime": "macos" + }, + "default_features": true, + "features": [], + "kind": "native-executable", + "name": "ts-aarch64-apple-darwin", + "package": "trusted-server-cli", + "role": "operator-cli", + "target": "aarch64-apple-darwin", + "variant": "default" + }, + { + "assurance_profile": "provenance-sbom", + "bin": "ts", + "capabilities": ["cli-execute"], + "compatibility": { + "architecture": "x86_64", + "minimum_version": "13.0", + "runtime": "macos" + }, + "default_features": true, + "features": [], + "kind": "native-executable", + "name": "ts-x86_64-apple-darwin", + "package": "trusted-server-cli", + "role": "operator-cli", + "target": "x86_64-apple-darwin", + "variant": "default" + }, + { + "assurance_profile": "provenance-sbom", + "bin": "ts", + "capabilities": ["cli-execute"], + "compatibility": { + "architecture": "x86_64", + "minimum_version": "glibc-2.34", + "runtime": "linux-gnu" + }, + "default_features": true, + "features": [], + "kind": "native-executable", + "name": "ts-x86_64-unknown-linux-gnu", + "package": "trusted-server-cli", + "role": "operator-cli", + "target": "x86_64-unknown-linux-gnu", + "variant": "default" + }, + { + "assurance_profile": "provenance-sbom", + "bin": "trusted-server-axum", + "capabilities": ["axum-production"], + "compatibility": { + "architecture": "x86_64", + "minimum_version": "glibc-2.34", + "runtime": "linux-gnu" + }, + "default_features": true, + "features": [], + "kind": "native-executable", + "name": "trusted-server-axum-x86_64-unknown-linux-gnu", + "package": "trusted-server-adapter-axum", + "role": "server", + "target": "x86_64-unknown-linux-gnu", + "variant": "default" + }, + { + "assurance_profile": "provenance-sbom", + "bin": "trusted-server-axum", + "capabilities": ["axum-production"], + "compatibility": { + "architecture": "x86_64", + "minimum_version": "static", + "runtime": "linux-musl" + }, + "default_features": true, + "features": [], + "kind": "native-executable", + "name": "trusted-server-axum-x86_64-unknown-linux-musl", + "package": "trusted-server-adapter-axum", + "role": "server", + "target": "x86_64-unknown-linux-musl", + "variant": "default" + }, + { + "assurance_profile": "provenance-sbom", + "bin": "trusted-server-axum", + "capabilities": ["axum-production"], + "compatibility": { + "architecture": "aarch64", + "minimum_version": "glibc-2.34", + "runtime": "linux-gnu" + }, + "default_features": true, + "features": [], + "kind": "native-executable", + "name": "trusted-server-axum-aarch64-unknown-linux-gnu", + "package": "trusted-server-adapter-axum", + "role": "server", + "target": "aarch64-unknown-linux-gnu", + "variant": "default" + } + ], + "cohort": "phase1-fastly-cli", + "eligible_branches": ["main"], + "schema_version": 1 +} diff --git a/scripts/release/release-tools.json b/scripts/release/release-tools.json new file mode 100644 index 0000000000..5abaa8e07b --- /dev/null +++ b/scripts/release/release-tools.json @@ -0,0 +1,490 @@ +{ + "environments": [ + { + "acquisition": null, + "id": "debian12-x86_64", + "image_reference": null, + "kind": "oci-runtime", + "oci_index_sha256": null, + "oci_manifest_sha256": null, + "qualification": "pending", + "role": "compatibility-runtime" + }, + { + "acquisition": null, + "glibc_maximum": "2.34", + "id": "gnu-builder-x86_64", + "image_reference": null, + "kind": "oci-builder", + "oci_index_sha256": null, + "oci_manifest_sha256": null, + "qualification": "pending", + "role": "compilation", + "sdk_sha256": null + }, + { + "acquisition": null, + "id": "macos15-aarch64", + "kind": "github-hosted-macos", + "qualification": "pending", + "role": "compilation", + "runner_image_version": null, + "runner_label": "macos-15", + "sdk_sha256": null + }, + { + "acquisition": null, + "id": "macos15-x86_64", + "kind": "github-hosted-macos", + "qualification": "pending", + "role": "compilation", + "runner_image_version": null, + "runner_label": "macos-15-intel", + "sdk_sha256": null + }, + { + "acquisition": null, + "id": "ubi9-x86_64", + "image_reference": null, + "kind": "oci-runtime", + "oci_index_sha256": null, + "oci_manifest_sha256": null, + "qualification": "pending", + "role": "compatibility-runtime" + }, + { + "acquisition": null, + "id": "ubuntu22.04-x86_64", + "image_reference": null, + "kind": "oci-runtime", + "oci_index_sha256": null, + "oci_manifest_sha256": null, + "qualification": "pending", + "role": "compatibility-runtime" + } + ], + "recipes": [ + { + "environment_id": "gnu-builder-x86_64", + "id": "trusted-server-fastly.wasm", + "qualification": "pending", + "script": "scripts/release/build.sh", + "settings": { + "ambient_profiles": { + "A": { + "checkout_root": "external-release-A", + "execution_user": "qualification-selected", + "jobs": 4, + "locale": "C", + "timezone": "UTC", + "umask": "022" + }, + "B": { + "checkout_root": "external-release-B", + "execution_user": "qualified-alternate-or-unsupported", + "jobs": 2, + "locale": "qualification-selected-alternate", + "timezone": "qualification-selected-alternate", + "umask": "027" + } + }, + "aws_lc_target_selector": true, + "aws_lc_use_system": false, + "cargo_config": ".cargo/config.toml", + "cargo_frozen": true, + "cargo_home": "private-prepared", + "cargo_locked": true, + "cargo_message_format": "json-render-diagnostics", + "download_control": "actual-cargo-build-script-required", + "fresh_target": true, + "network_boundary": "source-owned-os-denial-v1", + "normalized_inputs": [ + "compiler-locale-C", + "compiler-timezone-UTC", + "source-commit-epoch", + "remapped-paths" + ], + "npm_lifecycle": "reviewed-preparation-only", + "release_debug": 1, + "release_strip": "none", + "rust_toolchain": "1.98.1", + "sqlite_features": ["bundled"], + "sqlite_use_pkg_config": false, + "sysroot": "authenticated-sdk-required", + "tool_paths": "authenticated-absolute-paths-required", + "vite_config_loading": false, + "vite_env_loading": false, + "vite_mode": "production" + }, + "sha256": null + }, + { + "environment_id": "macos15-aarch64", + "id": "ts-aarch64-apple-darwin", + "qualification": "pending", + "script": "scripts/release/build.sh", + "settings": { + "ambient_profiles": { + "A": { + "checkout_root": "external-release-A", + "execution_user": "qualification-selected", + "jobs": 4, + "locale": "C", + "timezone": "UTC", + "umask": "022" + }, + "B": { + "checkout_root": "external-release-B", + "execution_user": "qualified-alternate-or-unsupported", + "jobs": 2, + "locale": "qualification-selected-alternate", + "timezone": "qualification-selected-alternate", + "umask": "027" + } + }, + "aws_lc_target_selector": true, + "aws_lc_use_system": false, + "cargo_config": ".cargo/config.toml", + "cargo_frozen": true, + "cargo_home": "private-prepared", + "cargo_locked": true, + "cargo_message_format": "json-render-diagnostics", + "download_control": "actual-cargo-build-script-required", + "fresh_target": true, + "network_boundary": "source-owned-os-denial-v1", + "normalized_inputs": [ + "compiler-locale-C", + "compiler-timezone-UTC", + "source-commit-epoch", + "remapped-paths" + ], + "npm_lifecycle": "reviewed-preparation-only", + "release_debug": 1, + "release_strip": "none", + "rust_toolchain": "1.98.1", + "sqlite_features": ["bundled"], + "sqlite_use_pkg_config": false, + "sysroot": "authenticated-sdk-required", + "tool_paths": "authenticated-absolute-paths-required", + "vite_config_loading": false, + "vite_env_loading": false, + "vite_mode": "production" + }, + "sha256": null + }, + { + "environment_id": "macos15-x86_64", + "id": "ts-x86_64-apple-darwin", + "qualification": "pending", + "script": "scripts/release/build.sh", + "settings": { + "ambient_profiles": { + "A": { + "checkout_root": "external-release-A", + "execution_user": "qualification-selected", + "jobs": 4, + "locale": "C", + "timezone": "UTC", + "umask": "022" + }, + "B": { + "checkout_root": "external-release-B", + "execution_user": "qualified-alternate-or-unsupported", + "jobs": 2, + "locale": "qualification-selected-alternate", + "timezone": "qualification-selected-alternate", + "umask": "027" + } + }, + "aws_lc_target_selector": true, + "aws_lc_use_system": false, + "cargo_config": ".cargo/config.toml", + "cargo_frozen": true, + "cargo_home": "private-prepared", + "cargo_locked": true, + "cargo_message_format": "json-render-diagnostics", + "download_control": "actual-cargo-build-script-required", + "fresh_target": true, + "network_boundary": "source-owned-os-denial-v1", + "normalized_inputs": [ + "compiler-locale-C", + "compiler-timezone-UTC", + "source-commit-epoch", + "remapped-paths" + ], + "npm_lifecycle": "reviewed-preparation-only", + "release_debug": 1, + "release_strip": "none", + "rust_toolchain": "1.98.1", + "sqlite_features": ["bundled"], + "sqlite_use_pkg_config": false, + "sysroot": "authenticated-sdk-required", + "tool_paths": "authenticated-absolute-paths-required", + "vite_config_loading": false, + "vite_env_loading": false, + "vite_mode": "production" + }, + "sha256": null + }, + { + "environment_id": "gnu-builder-x86_64", + "id": "ts-x86_64-unknown-linux-gnu", + "qualification": "pending", + "script": "scripts/release/build.sh", + "settings": { + "ambient_profiles": { + "A": { + "checkout_root": "external-release-A", + "execution_user": "qualification-selected", + "jobs": 4, + "locale": "C", + "timezone": "UTC", + "umask": "022" + }, + "B": { + "checkout_root": "external-release-B", + "execution_user": "qualified-alternate-or-unsupported", + "jobs": 2, + "locale": "qualification-selected-alternate", + "timezone": "qualification-selected-alternate", + "umask": "027" + } + }, + "aws_lc_target_selector": true, + "aws_lc_use_system": false, + "cargo_config": ".cargo/config.toml", + "cargo_frozen": true, + "cargo_home": "private-prepared", + "cargo_locked": true, + "cargo_message_format": "json-render-diagnostics", + "download_control": "actual-cargo-build-script-required", + "fresh_target": true, + "network_boundary": "source-owned-os-denial-v1", + "normalized_inputs": [ + "compiler-locale-C", + "compiler-timezone-UTC", + "source-commit-epoch", + "remapped-paths" + ], + "npm_lifecycle": "reviewed-preparation-only", + "release_debug": 1, + "release_strip": "none", + "rust_toolchain": "1.98.1", + "sqlite_features": ["bundled"], + "sqlite_use_pkg_config": false, + "sysroot": "authenticated-sdk-required", + "tool_paths": "authenticated-absolute-paths-required", + "vite_config_loading": false, + "vite_env_loading": false, + "vite_mode": "production" + }, + "sha256": null + }, + { + "environment_id": "gnu-builder-x86_64", + "id": "tsjs", + "qualification": "pending", + "script": "scripts/release/build-js.sh", + "settings": { + "compare": "all-tracked-final-module-bytes", + "fresh_installations": 2, + "fresh_private_npm_caches": true, + "fresh_source": "tracked-objects-only", + "mandatory_tests": true, + "network_boundary": "source-owned-os-denial-v1", + "node_version": "24.20.0", + "npm_audit": false, + "npm_fund": false, + "npm_include_dev": true, + "npm_lifecycle": "reviewed-observed-foreground", + "npm_offline": true, + "npm_version": "11.19.0", + "publish": "A-only-after-equality", + "tool_paths": "authenticated-absolute-paths-required", + "vite_config_loading": false, + "vite_env_loading": false, + "vite_mode": "production" + }, + "sha256": null + } + ], + "schema_version": 1, + "tools": [ + { + "acquisition": null, + "name": "actions/attest:github-action", + "qualification": "pending", + "revision": "1e69f48acb82d1966a394da916b4c1698aa569d6", + "sha256": null, + "version": "4.2.2" + }, + { + "acquisition": null, + "name": "fastly:aarch64-apple-darwin", + "qualification": "pending", + "revision": null, + "sha256": null, + "version": "16.0.0" + }, + { + "acquisition": null, + "name": "fastly:x86_64-apple-darwin", + "qualification": "pending", + "revision": null, + "sha256": null, + "version": "16.0.0" + }, + { + "acquisition": null, + "name": "fastly:x86_64-unknown-linux-gnu", + "qualification": "pending", + "revision": null, + "sha256": null, + "version": "16.0.0" + }, + { + "acquisition": null, + "name": "gh:aarch64-apple-darwin", + "qualification": "pending", + "revision": null, + "sha256": null, + "version": "2.97.0" + }, + { + "acquisition": null, + "name": "gh:x86_64-apple-darwin", + "qualification": "pending", + "revision": null, + "sha256": null, + "version": "2.97.0" + }, + { + "acquisition": null, + "name": "gh:x86_64-unknown-linux-gnu", + "qualification": "pending", + "revision": null, + "sha256": null, + "version": "2.97.0" + }, + { + "acquisition": null, + "name": "node:aarch64-apple-darwin", + "qualification": "pending", + "revision": null, + "sha256": null, + "version": "24.20.0" + }, + { + "acquisition": null, + "name": "node:x86_64-apple-darwin", + "qualification": "pending", + "revision": null, + "sha256": null, + "version": "24.20.0" + }, + { + "acquisition": null, + "name": "node:x86_64-unknown-linux-gnu", + "qualification": "pending", + "revision": null, + "sha256": null, + "version": "24.20.0" + }, + { + "acquisition": null, + "name": "npm:aarch64-apple-darwin", + "qualification": "pending", + "revision": null, + "sha256": null, + "version": "11.19.0" + }, + { + "acquisition": null, + "name": "npm:x86_64-apple-darwin", + "qualification": "pending", + "revision": null, + "sha256": null, + "version": "11.19.0" + }, + { + "acquisition": null, + "name": "npm:x86_64-unknown-linux-gnu", + "qualification": "pending", + "revision": null, + "sha256": null, + "version": "11.19.0" + }, + { + "acquisition": null, + "name": "rust:aarch64-apple-darwin", + "qualification": "pending", + "revision": null, + "sha256": null, + "version": "1.98.1" + }, + { + "acquisition": null, + "name": "rust:x86_64-apple-darwin", + "qualification": "pending", + "revision": null, + "sha256": null, + "version": "1.98.1" + }, + { + "acquisition": null, + "name": "rust:x86_64-unknown-linux-gnu", + "qualification": "pending", + "revision": null, + "sha256": null, + "version": "1.98.1" + }, + { + "acquisition": null, + "name": "viceroy:aarch64-apple-darwin", + "qualification": "pending", + "revision": null, + "sha256": null, + "version": "0.21.0" + }, + { + "acquisition": null, + "name": "viceroy:x86_64-apple-darwin", + "qualification": "pending", + "revision": null, + "sha256": null, + "version": "0.21.0" + }, + { + "acquisition": null, + "name": "viceroy:x86_64-unknown-linux-gnu", + "qualification": "pending", + "revision": null, + "sha256": null, + "version": "0.21.0" + }, + { + "acquisition": null, + "name": "wasmtime:aarch64-apple-darwin", + "qualification": "pending", + "revision": null, + "sha256": null, + "version": "48.0.1" + }, + { + "acquisition": null, + "name": "wasmtime:x86_64-apple-darwin", + "qualification": "pending", + "revision": null, + "sha256": null, + "version": "48.0.1" + }, + { + "acquisition": null, + "name": "wasmtime:x86_64-unknown-linux-gnu", + "qualification": "pending", + "revision": null, + "sha256": null, + "version": "48.0.1" + } + ] +}