diff --git a/.github/scripts/check-workflow-policy.mjs b/.github/scripts/check-workflow-policy.mjs index 9b6cfdd24..5029225cb 100644 --- a/.github/scripts/check-workflow-policy.mjs +++ b/.github/scripts/check-workflow-policy.mjs @@ -414,6 +414,19 @@ function requireExactStepScript(violations, file, job, name, expectedDigest, sub ); } +// A line that begins with `#` is not necessarily a shell comment when the +// preceding line opened a quote. Hash raw text for scripts with multiline +// quoted programs so quote-context rewrites cannot disappear from the digest. +function requireExactRawStepScript(violations, file, job, name, expectedDigest, subject) { + const run = exactResolverRunText(stepRun(job, name)); + const digest = createHash("sha256").update(run).digest("hex"); + add( + violations, + run.length > 0 && digest === expectedDigest, + `${file} step ${name} must match the reviewed ${subject} script exactly`, + ); +} + function stepIndex(job, name) { return list(job?.steps).map(object).findIndex(step => step.name === name); } @@ -526,6 +539,23 @@ const marketplaceGuardDigest = "6380c916a1b3566b4b9d6545b63fbc9c7db12b54fb328b5c // executable text as well as its reader-facing invariants. const packagedPlatformCloseoutDigest = "ce7a7f5aa99f5fcbc037d4c1f06de5d841e4a4d114208820592a84c41c797b1a"; +// This workflow builds release archives on three operating systems and carries +// state between many shell steps through GITHUB_ENV and GITHUB_PATH. Pin its +// parsed executable structure so an unreviewed earlier step cannot replace an +// owner binary while leaving the locally digested finalizer unchanged. +const packagedPlatformWorkflowDigest = + "6cb226d673bdbe5679b9f7010aad18c2c2a956129b51cff69ba457ed91962132"; +// Linux owns its compiler server inside Docker, while macOS and Windows own one +// in the host shell. Pin both executable programs so a swallowed stop or a +// dead-code copy cannot satisfy the ownership fragments below. +const packagedSccacheIdentityDigest = + "35f1976fd420c0ca6f2213c49ec3879dfa136d649529bbe3fe175f6b5ca633c6"; +const packagedLinuxBuildDigest = + "fc02f682c294983989d5f63151f8af9b31febda2da6fa0933aa9b1f7c221b4aa"; +const packagedCompileClockStopDigest = + "ef9f7ee4636c3466830447e2ed8a10c2030ca3949bca082652d9262848d258a5"; +const packagedHostCompilerFinalizerDigest = + "b77d8bb12c2748bfe016ab65ccb2f4581356f3ccf1d666e747306caffd6c0c46"; const draftProofCommands = [ "cargo test --locked -p codestory-llama-sys --test native_staging", "cargo test --locked -p codestory-llama-sys --test model_staging", @@ -2733,6 +2763,12 @@ function validatePackagedProof(workflows, violations, graph) { violations.push(`${file} must exist`); return; } + add( + violations, + createHash("sha256").update(JSON.stringify(workflow)).digest("hex") + === packagedPlatformWorkflowDigest, + `${file} must match the reviewed canonical workflow structure`, + ); add(violations, trigger(workflow, "workflow_call") !== undefined, `${file} must be reusable`); const refInput = object(at(workflow, "on", "workflow_call", "inputs", "ref")); add( @@ -2827,6 +2863,25 @@ function validatePackagedProof(workflows, violations, graph) { && object(sccacheSetup?.with).version === "${{ env.SCCACHE_VERSION }}", `${file} must install the pinned sccache action and binary`, ); + const sccacheIdentity = namedStep(job, "Capture pinned sccache identity"); + add( + violations, + sccacheIdentity?.id === "sccache-identity" + && sccacheIdentity?.shell === "bash" + && sccacheIdentity?.env === undefined + && sccacheIdentity?.["continue-on-error"] === undefined + && stepIndex(job, "Capture pinned sccache identity") + === stepIndex(job, "Install pinned sccache") + 1, + `${file} must capture the pinned sccache identity immediately after installation`, + ); + requireExactRawStepScript( + violations, + file, + job, + "Capture pinned sccache identity", + packagedSccacheIdentityDigest, + "pinned sccache identity capture", + ); requireStepRun(violations, file, job, "Configure short Windows Cargo target", [ '$workspaceTarget = Join-Path $env:GITHUB_WORKSPACE "target"', '$runnerRoot = [System.IO.Path]::GetPathRoot($workspaceTarget)', @@ -2986,24 +3041,90 @@ function validatePackagedProof(workflows, violations, graph) { requireStepRun(violations, file, job, "Compile native workspace path regression on Windows", [ "cargo test --locked -p codestory-workspace repository_identity --no-run", ]); + const packageBuild = namedStep(job, "Build codestory-cli"); + const linuxBuild = namedStep(job, "Build Linux x64 at the glibc 2.31 baseline"); + const expectedSccacheIdentityEnv = { + SCCACHE_BINARY: "${{ steps.sccache-identity.outputs.path }}", + SCCACHE_SHA256: "${{ steps.sccache-identity.outputs.sha256 }}", + }; requireStepRun(violations, file, job, "Build Linux x64 at the glibc 2.31 baseline", [ - 'mkdir -p "$CARGO_HOME" "$SCCACHE_DIR"', + 'test -x "$SCCACHE_BINARY"', + 'test "$actual_sccache_sha256" = "$SCCACHE_SHA256"', "RUSTC_WRAPPER=/sccache/sccache", "SCCACHE_DIR=/sccache/cache", "CMAKE_C_COMPILER_LAUNCHER=/sccache/sccache", "CMAKE_CXX_COMPILER_LAUNCHER=/sccache/sccache", - "$SCCACHE_PATH:/sccache/sccache:ro", + "$SCCACHE_BINARY:/sccache/sccache:ro", "$SCCACHE_DIR:/sccache/cache", - "/sccache/sccache --stop-server", ]); + add( + violations, + linuxBuild?.if === "matrix.asset_target == 'linux-x64'" + && linuxBuild?.shell === "bash" + && hasExactKeys(object(linuxBuild?.env), Object.keys(expectedSccacheIdentityEnv)) + && Object.entries(expectedSccacheIdentityEnv).every( + ([key, value]) => object(linuxBuild?.env)[key] === value, + ) + && linuxBuild?.["continue-on-error"] === undefined, + `${file} Linux container must strictly report and stop its owned compiler server`, + ); + requireExactRawStepScript( + violations, + file, + job, + "Build Linux x64 at the glibc 2.31 baseline", + packagedLinuxBuildDigest, + "Linux container build and compiler-server ownership", + ); + const stopCompilationClock = namedStep(job, "Stop compilation clock"); + add( + violations, + stopCompilationClock?.id === "compile-clock-stop" + && stopCompilationClock?.shell === "bash" + && stopCompilationClock?.env === undefined + && stopCompilationClock?.["continue-on-error"] === undefined, + `${file} compiler clock stop must remain a strict telemetry-only boundary`, + ); + requireExactRawStepScript( + violations, + file, + job, + "Stop compilation clock", + packagedCompileClockStopDigest, + "compiler clock stop", + ); const finalizeCompilerObjects = namedStep(job, "Finalize compiler objects"); add( violations, - String(finalizeCompilerObjects?.if ?? "") - .includes("steps.linux-build.outcome == 'success'") - && String(finalizeCompilerObjects?.if ?? "") - .includes("steps.package-build.outcome == 'success'"), - `${file} must stop the compiler server that performed each selected build`, + String(finalizeCompilerObjects?.if ?? "").trim() + === "always() && steps.package-build.outcome == 'success'" + && finalizeCompilerObjects?.shell === "bash" + && hasExactKeys( + object(finalizeCompilerObjects?.env), + Object.keys(expectedSccacheIdentityEnv), + ) + && Object.entries(expectedSccacheIdentityEnv).every( + ([key, value]) => object(finalizeCompilerObjects?.env)[key] === value, + ) + && finalizeCompilerObjects?.["continue-on-error"] === undefined + && packageBuild?.if === "matrix.asset_target != 'linux-x64'", + `${file} host finalizer must strictly stop only the host package-build compiler server`, + ); + requireExactRawStepScript( + violations, + file, + job, + "Finalize compiler objects", + packagedHostCompilerFinalizerDigest, + "host compiler-server finalizer", + ); + add( + violations, + stepIndex(job, "Stop compilation clock") + === stepIndex(job, "Build Linux x64 at the glibc 2.31 baseline") + 1 + && stepIndex(job, "Finalize compiler objects") + === stepIndex(job, "Stop compilation clock") + 1, + `${file} compiler owner build, clock stop, and finalizer must remain adjacent`, ); add( violations, @@ -3041,7 +3162,6 @@ function validatePackagedProof(workflows, violations, graph) { `${file} Bullseye native build must preserve compiler contract ${fragment}`, ); } - const packageBuild = namedStep(job, "Build codestory-cli"); add( violations, packageBuild?.env === undefined, diff --git a/.github/scripts/check-workflow-policy.test.mjs b/.github/scripts/check-workflow-policy.test.mjs index 855c75c56..a465126c9 100644 --- a/.github/scripts/check-workflow-policy.test.mjs +++ b/.github/scripts/check-workflow-policy.test.mjs @@ -1935,6 +1935,9 @@ test("reusable compiler caches and proof modes reject hostile downgrades", async draftStep(packagedJob(workflow), "Capture reusable build cache contract"); const mutations = [ + ["packaged workflow injects an earlier Node preload", packagedFile, workflow => { + workflow.env.NODE_OPTIONS = "--require ./fake-hash.cjs"; + }, /packaged-platform-proof\.yml must match the reviewed canonical workflow structure/u], ["release workflow policy loses its full history", releaseFile, workflow => { delete workflow.jobs["workflow-policy"].steps[0].with; }, /workflow-policy must check out full history for the reuse-binding contracts/u], @@ -2051,6 +2054,20 @@ test("reusable compiler caches and proof modes reject hostile downgrades", async packagedIdentity(workflow).run = packagedIdentity(workflow).run .replace("--identity qualification_driver=disabled", "--workload ignored"); }, /packaged-platform-proof\.yml must compute one complete reusable compiler compatibility contract/u], + ["pinned sccache identity capture moves away from installation", packagedFile, workflow => { + moveNamedStepAfter( + packagedJob(workflow), + "Capture pinned sccache identity", + "Configure bounded compiler cache", + ); + }, /must capture the pinned sccache identity immediately after installation/u], + ["pinned sccache identity capture stops hashing the binary", packagedFile, workflow => { + const capture = draftStep(packagedJob(workflow), "Capture pinned sccache identity"); + capture.run = capture.run.replace( + 'createHash("sha256").update(readFileSync(process.argv[1])).digest("hex")', + '"unverified"', + ); + }, /pinned sccache identity capture script exactly/u], ["source compiler cache waits for tests", sourceFile, workflow => { moveNamedStepAfter( sourceJob(workflow), @@ -2173,10 +2190,149 @@ test("reusable compiler caches and proof modes reject hostile downgrades", async }, }); }, /frozen Linux fresh-target qualification must not restore compiler output/u], - ["Linux compiler cache exits with an active server", packagedFile, workflow => { + ["Linux compiler cache omits server shutdown", packagedFile, workflow => { const build = draftStep(packagedJob(workflow), "Build Linux x64 at the glibc 2.31 baseline"); build.run = build.run.replace("/sccache/sccache --stop-server", "true"); - }, /step Build Linux x64 at the glibc 2\.31 baseline must run \/sccache\/sccache --stop-server/u], + }, /Linux container build and compiler-server ownership script exactly/u], + ["Linux compiler cache makes statistics advisory", packagedFile, workflow => { + const build = draftStep(packagedJob(workflow), "Build Linux x64 at the glibc 2.31 baseline"); + build.run = build.run.replace( + "/sccache/sccache --show-stats", + "/sccache/sccache --show-stats || true", + ); + }, /Linux container build and compiler-server ownership script exactly/u], + ["Linux compiler cache makes shutdown advisory", packagedFile, workflow => { + const build = draftStep(packagedJob(workflow), "Build Linux x64 at the glibc 2.31 baseline"); + build.run = build.run.replace( + "/sccache/sccache --stop-server", + "/sccache/sccache --stop-server || true", + ); + }, /Linux container build and compiler-server ownership script exactly/u], + ["Linux compiler shutdown is parked in dead code", packagedFile, workflow => { + const build = draftStep(packagedJob(workflow), "Build Linux x64 at the glibc 2.31 baseline"); + build.run = `if false; then\n${build.run}\nfi\n`; + }, /Linux container build and compiler-server ownership script exactly/u], + ["Linux compiler shutdown hides behind an exact dead-code decoy", packagedFile, workflow => { + const build = draftStep(packagedJob(workflow), "Build Linux x64 at the glibc 2.31 baseline"); + build.run = build.run.replace( + "/sccache/sccache --stop-server", + "/sccache/sccache --stop-server || true", + ); + build.run += "\nif false; then\n /sccache/sccache --stop-server\nfi\n"; + }, /Linux container build and compiler-server ownership script exactly/u], + ["Linux compiler shutdown escapes through a stripped quote-context comment", packagedFile, workflow => { + const build = draftStep(packagedJob(workflow), "Build Linux x64 at the glibc 2.31 baseline"); + build.run = build.run.replace( + " /sccache/sccache --show-stats", + " # '; exit 0; : '\n /sccache/sccache --show-stats", + ); + }, /Linux container build and compiler-server ownership script exactly/u], + ["Linux compiler shutdown is inverted", packagedFile, workflow => { + const build = draftStep(packagedJob(workflow), "Build Linux x64 at the glibc 2.31 baseline"); + build.run = build.run.replace("docker run --rm", "! docker run --rm"); + }, /Linux container build and compiler-server ownership script exactly/u], + ["Linux compiler shutdown is bypassed by an early exit", packagedFile, workflow => { + const build = draftStep(packagedJob(workflow), "Build Linux x64 at the glibc 2.31 baseline"); + build.run = `exit 0\n${build.run}`; + }, /Linux container build and compiler-server ownership script exactly/u], + ["Linux compiler build shell absorbs failure", packagedFile, workflow => { + draftStep( + packagedJob(workflow), + "Build Linux x64 at the glibc 2.31 baseline", + ).shell = "bash {0} || true"; + }, /Linux container must strictly report and stop its owned compiler server/u], + ["Linux compiler cache step becomes advisory", packagedFile, workflow => { + draftStep( + packagedJob(workflow), + "Build Linux x64 at the glibc 2.31 baseline", + )["continue-on-error"] = true; + }, /Linux container must strictly report and stop its owned compiler server/u], + ["Linux compiler cache rebinds the pinned binary", packagedFile, workflow => { + draftStep( + packagedJob(workflow), + "Build Linux x64 at the glibc 2.31 baseline", + ).env.SCCACHE_BINARY = "sccache"; + }, /Linux container must strictly report and stop its owned compiler server/u], + ["host compiler finalizer is restored on Linux", packagedFile, workflow => { + draftStep(packagedJob(workflow), "Finalize compiler objects").if = + "always() && ((matrix.asset_target == 'linux-x64' && steps.linux-build.outcome == 'success') || (matrix.asset_target != 'linux-x64' && steps.package-build.outcome == 'success'))"; + }, /host finalizer must strictly stop only the host package-build compiler server/u], + ["host package build becomes Linux-reachable", packagedFile, workflow => { + draftStep(packagedJob(workflow), "Build codestory-cli").if = "always()"; + }, /host finalizer must strictly stop only the host package-build compiler server/u], + ["clock stop prepends a fake compiler cache binary", packagedFile, workflow => { + const stop = draftStep(packagedJob(workflow), "Stop compilation clock"); + stop.run += [ + "", + 'fake_dir="$RUNNER_TEMP/fake-sccache"', + 'mkdir -p "$fake_dir"', + "printf '#!/usr/bin/env bash\\nexit 0\\n' > \"$fake_dir/sccache\"", + 'chmod +x "$fake_dir/sccache"', + 'echo "$fake_dir" >> "$GITHUB_PATH"', + ].join("\n"); + }, /compiler clock stop script exactly/u], + ["clock stop shell absorbs failure", packagedFile, workflow => { + draftStep(packagedJob(workflow), "Stop compilation clock").shell = "bash {0} || true"; + }, /compiler clock stop must remain a strict telemetry-only boundary/u], + ["a prep step is inserted before compiler finalization", packagedFile, workflow => { + const steps = packagedJob(workflow).steps; + const finalizeIndex = steps.findIndex(step => step.name === "Finalize compiler objects"); + steps.splice(finalizeIndex, 0, { + name: "Shadow compiler cache", + shell: "bash", + run: 'echo "$RUNNER_TEMP/fake-sccache" >> "$GITHUB_PATH"', + }); + }, /compiler owner build, clock stop, and finalizer must remain adjacent/u], + ["host compiler statistics become advisory", packagedFile, workflow => { + const finalize = draftStep(packagedJob(workflow), "Finalize compiler objects"); + finalize.run = finalize.run.replace( + '"$SCCACHE_BINARY" --show-stats', + '"$SCCACHE_BINARY" --show-stats || true', + ); + }, /host compiler-server finalizer script exactly/u], + ["host compiler shutdown becomes advisory", packagedFile, workflow => { + const finalize = draftStep(packagedJob(workflow), "Finalize compiler objects"); + finalize.run = finalize.run.replace( + '"$SCCACHE_BINARY" --stop-server', + '"$SCCACHE_BINARY" --stop-server || true', + ); + }, /host compiler-server finalizer script exactly/u], + ["host compiler shutdown hides behind exact dead-code decoys", packagedFile, workflow => { + const finalize = draftStep(packagedJob(workflow), "Finalize compiler objects"); + finalize.run = [ + "sccache --show-stats || true", + "sccache --stop-server || true", + "if false; then", + " sccache --show-stats", + " sccache --stop-server", + "fi", + ].join("\n"); + }, /host compiler-server finalizer script exactly/u], + ["host compiler finalizer shell absorbs failure", packagedFile, workflow => { + draftStep( + packagedJob(workflow), + "Finalize compiler objects", + ).shell = "bash {0} || true"; + }, /host finalizer must strictly stop only the host package-build compiler server/u], + ["host compiler finalizer step becomes advisory", packagedFile, workflow => { + draftStep( + packagedJob(workflow), + "Finalize compiler objects", + )["continue-on-error"] = true; + }, /host finalizer must strictly stop only the host package-build compiler server/u], + ["host compiler finalizer rebinds the pinned binary", packagedFile, workflow => { + draftStep( + packagedJob(workflow), + "Finalize compiler objects", + ).env.SCCACHE_BINARY = "sccache"; + }, /host finalizer must strictly stop only the host package-build compiler server/u], + ["host compiler finalizer resolves through PATH again", packagedFile, workflow => { + const finalize = draftStep(packagedJob(workflow), "Finalize compiler objects"); + finalize.run = finalize.run.replace( + '"$SCCACHE_BINARY" --show-stats', + "sccache --show-stats", + ); + }, /host compiler-server finalizer script exactly/u], ["package checkout accepts a fallback SHA", packagedFile, workflow => { draftStep(packagedJob(workflow), "Checkout").with.ref = "${{ inputs.ref || github.sha }}"; }, /package jobs must checkout only the requested exact SHA/u], diff --git a/.github/workflows/packaged-platform-proof.yml b/.github/workflows/packaged-platform-proof.yml index 2e23fc4df..93cb1188f 100644 --- a/.github/workflows/packaged-platform-proof.yml +++ b/.github/workflows/packaged-platform-proof.yml @@ -160,6 +160,26 @@ jobs: version: ${{ env.SCCACHE_VERSION }} disable_annotations: true + - name: Capture pinned sccache identity + id: sccache-identity + shell: bash + run: | + sccache_path="$(command -v sccache)" + test -x "$sccache_path" + sccache_sha256="$( + node --input-type=module -e ' + import { createHash } from "node:crypto"; + import { readFileSync } from "node:fs"; + process.stdout.write( + createHash("sha256").update(readFileSync(process.argv[1])).digest("hex"), + ); + ' "$sccache_path" + )" + { + echo "path=$sccache_path" + echo "sha256=$sccache_sha256" + } >> "$GITHUB_OUTPUT" + - name: Configure bounded compiler cache shell: bash run: | @@ -342,8 +362,21 @@ jobs: id: linux-build if: matrix.asset_target == 'linux-x64' shell: bash + env: + SCCACHE_BINARY: ${{ steps.sccache-identity.outputs.path }} + SCCACHE_SHA256: ${{ steps.sccache-identity.outputs.sha256 }} run: | - test -x "$SCCACHE_PATH" + test -x "$SCCACHE_BINARY" + actual_sccache_sha256="$( + node --input-type=module -e ' + import { createHash } from "node:crypto"; + import { readFileSync } from "node:fs"; + process.stdout.write( + createHash("sha256").update(readFileSync(process.argv[1])).digest("hex"), + ); + ' "$SCCACHE_BINARY" + )" + test "$actual_sccache_sha256" = "$SCCACHE_SHA256" mkdir -p "$CARGO_HOME" "$SCCACHE_DIR" model_relative="${CODESTORY_EMBED_MODEL_SOURCE#"$PWD/"}" docker run --rm --platform linux/amd64 \ @@ -359,7 +392,7 @@ jobs: --env CMAKE_CXX_COMPILER_LAUNCHER=/sccache/sccache \ --volume "$CARGO_HOME:/cargo" \ --volume "$PWD:/workspace" \ - --volume "$SCCACHE_PATH:/sccache/sccache:ro" \ + --volume "$SCCACHE_BINARY:/sccache/sccache:ro" \ --volume "$SCCACHE_DIR:/sccache/cache" \ --workdir /workspace \ codestory-linux-glibc-build \ @@ -394,19 +427,29 @@ jobs: (matrix.asset_target == 'linux-x64' && steps.linux-build.outcome == 'success') || (matrix.asset_target != 'linux-x64' && steps.package-build.outcome == 'success') ) + shell: bash run: node .github/scripts/cargo-cache-contract.mjs stop - name: Finalize compiler objects - if: >- - always() && - ( - (matrix.asset_target == 'linux-x64' && steps.linux-build.outcome == 'success') || - (matrix.asset_target != 'linux-x64' && steps.package-build.outcome == 'success') - ) + if: always() && steps.package-build.outcome == 'success' shell: bash + env: + SCCACHE_BINARY: ${{ steps.sccache-identity.outputs.path }} + SCCACHE_SHA256: ${{ steps.sccache-identity.outputs.sha256 }} run: | - sccache --show-stats - sccache --stop-server + test -x "$SCCACHE_BINARY" + actual_sccache_sha256="$( + node --input-type=module -e ' + import { createHash } from "node:crypto"; + import { readFileSync } from "node:fs"; + process.stdout.write( + createHash("sha256").update(readFileSync(process.argv[1])).digest("hex"), + ); + ' "$SCCACHE_BINARY" + )" + test "$actual_sccache_sha256" = "$SCCACHE_SHA256" + "$SCCACHE_BINARY" --show-stats + "$SCCACHE_BINARY" --stop-server - name: Bound Cargo dependency cache id: cargo-dependency-cache-size