diff --git a/.github/workflows/runner-full-stack-e2e.yml b/.github/workflows/runner-full-stack-e2e.yml index fc3c18f31d..c1bf863766 100644 --- a/.github/workflows/runner-full-stack-e2e.yml +++ b/.github/workflows/runner-full-stack-e2e.yml @@ -553,33 +553,48 @@ jobs: -f "ref=$GITHUB_WORKFLOW_SHA" --jq .sha)" [[ "$workflow_blob" =~ ^[0-9a-f]{40}$ ]] + package_set_manifest="$RUNNER_TEMP/runner-e2e-build-packages" + dpkg-query --show --showformat='${binary:Package}=${Version}\n' \ + | LC_ALL=C sort > "$package_set_manifest" + test -s "$package_set_manifest" + package_set_id="$(sha256sum "$package_set_manifest" | cut -d ' ' -f 1)" + [[ "$package_set_id" =~ ^[0-9a-f]{64}$ ]] + toolchain_id="$({ - printf '%s\n' 'paperclip-runner/e2e-build-toolchain/v1' + printf '%s\n' 'paperclip-runner/e2e-build-toolchain/v2' printf 'runner=%s\n' "$TEST_RUNNER" printf 'runner-os=%s\n' "$RUNNER_OS" printf 'runner-arch=%s\n' "$RUNNER_ARCH" + printf 'runner-image-os=%s\n' "${ImageOS-}" + printf 'runner-image-version=%s\n' "${ImageVersion-}" + printf 'package-set=%s\n' "$package_set_id" for variable in \ - CC CFLAGS CI CARGO_BUILD_TARGET CARGO_ENCODED_RUSTFLAGS \ + AR CC CFLAGS CI CMAKE CARGO_BUILD_TARGET CARGO_ENCODED_RUSTFLAGS \ CARGO_TARGET_DIR LANG LC_ALL LDFLAGS NODE_ENV NODE_OPTIONS \ - RUSTC RUSTC_WRAPPER RUSTFLAGS SOURCE_DATE_EPOCH TZ + CXX PKG_CONFIG RANLIB RUSTC RUSTC_WRAPPER RUSTFLAGS \ + SOURCE_DATE_EPOCH TZ do printf '%s=%s\n' "$variable" "${!variable-}" done uname -srm - sha256sum /etc/os-release "$(command -v cc)" "$(command -v ld)" "$(command -v ldd)" + sha256sum /etc/os-release node --version pnpm --version (cd packages/paperclip-runner && rustc -vV) (cd packages/paperclip-runner && cargo -Vv) - cc --version - ld --version - ldd --version + for tool in cc c++ ld ldd ar ranlib cmake pkg-config; do + tool_path="$(command -v "$tool")" + test -f "$tool_path" + printf 'tool=%s path=%s\n' "$tool" "$tool_path" + sha256sum "$tool_path" + "$tool" --version + done } | sha256sum | cut -d ' ' -f 1)" [[ "$toolchain_id" =~ ^[0-9a-f]{64}$ ]] manifest="$RUNNER_TEMP/runner-e2e-build-inputs" { - printf '%s\n' 'paperclip-runner/e2e-build-inputs/v1' + printf '%s\n' 'paperclip-runner/e2e-build-inputs/v2' printf 'workflow=%s\n' "$workflow_blob" printf 'lock=%s\n' "$EXPECTED_LOCK_SHA256" printf 'toolchain=%s\n' "$toolchain_id" @@ -612,7 +627,7 @@ jobs: { echo "content_id=$content_id" echo "toolchain_id=$toolchain_id" - echo "cache_key=runner-e2e-build-v1-$ref_scope-$content_id" + echo "cache_key=runner-e2e-build-v2-$ref_scope-$content_id" } >> "$GITHUB_OUTPUT" - name: Restore exact reusable build outputs diff --git a/tests/runner-e2e/README.md b/tests/runner-e2e/README.md index ad62738f60..f10c8114e5 100644 --- a/tests/runner-e2e/README.md +++ b/tests/runner-e2e/README.md @@ -341,6 +341,14 @@ default of 32. Multi-turn steps are sequential inside their cell while independent cells overlap. Artifacts and merged HTML/JUnit/normalized reports are retained for 30 days. +The campaign builds shared TypeScript and native runner outputs once and then +fans that verified bundle out to selected cells. A later campaign on the same +target branch may reuse the bundle only when the trusted workflow, target +source closure and lockfile, requested output shape, runner image package set, +and native toolchain identity are exact matches. Changes to native build tools +such as CMake, pkg-config, the compiler, archiver, or ranlib deliberately force +a cold rebuild. + Restrict the RunsOn fleet to this repository and independently trusted workflows. Do not let untrusted pull-request or fork-triggered workflows target it, and require a fresh ephemeral instance for each job so one paid cell cannot diff --git a/tests/runner-e2e/SECURITY.md b/tests/runner-e2e/SECURITY.md index 518e9f1a47..87b0a4aafc 100644 --- a/tests/runner-e2e/SECURITY.md +++ b/tests/runner-e2e/SECURITY.md @@ -38,6 +38,15 @@ history publication explicitly use the trusted workflow commit and do not consume the target lockfile. Never run the workflow definition from the target branch. +Reusable executable build outputs are keyed by the trusted workflow blob, the +resolved target lockfile, a conservative source closure, requested output +shape, target branch scope, and the build environment. The environment identity +includes the literal runner selector, OS and architecture, the sorted installed +Debian package set, and the resolved path, digest, and version output of the C, +C++, linker, archiver, ranlib, CMake, and pkg-config tools. This is intentionally +conservative: a runner image or native build-package update must produce a cold +cache miss rather than reusing binaries from an under-specified toolchain. + The workflows verify both the original actor and triggering actor for every scheduled or manual attempt, including human reruns. Every secret-bearing job repeats this check as its first step so GitHub's partial-job diff --git a/tests/runner-e2e/workflow-security.test.ts b/tests/runner-e2e/workflow-security.test.ts index 850b245394..561a28b29e 100644 --- a/tests/runner-e2e/workflow-security.test.ts +++ b/tests/runner-e2e/workflow-security.test.ts @@ -549,10 +549,10 @@ describe("public repository paid workflow security", () => { ); expect(runnerBuildJob).not.toContain("restore-keys:"); expect(runnerBuildJob).toContain( - "cache_key=runner-e2e-build-v1-$ref_scope-$content_id", + "cache_key=runner-e2e-build-v2-$ref_scope-$content_id", ); expect(runnerBuildJob).not.toContain( - "cache_key=runner-e2e-build-v1-$TARGET_SHA", + "cache_key=runner-e2e-build-v2-$TARGET_SHA", ); expect(runnerBuildJob).toContain( '"repos/$REPOSITORY/contents/.github/workflows/runner-full-stack-e2e.yml"', @@ -593,21 +593,38 @@ describe("public repository paid workflow security", () => { "@paperclipai/paperclip-eval-kernel", ]); for (const toolchainInput of [ + "paperclip-runner/e2e-build-toolchain/v2", + "paperclip-runner/e2e-build-inputs/v2", + "AR CC CFLAGS", "CARGO_ENCODED_RUSTFLAGS", + "CMAKE", + "CXX", "NODE_OPTIONS", + "PKG_CONFIG", + "RANLIB", "RUSTFLAGS", "uname -srm", - 'sha256sum /etc/os-release "$(command -v cc)"', + "runner-image-os=", + "runner-image-version=", + "dpkg-query --show --showformat=", + "package-set=%s", + 'for tool in cc c++ ld ldd ar ranlib cmake pkg-config; do', + 'tool_path="$(command -v "$tool")"', + 'sha256sum "$tool_path"', "node --version", "pnpm --version", "rustc -vV", "cargo -Vv", - "cc --version", - "ld --version", - "ldd --version", + '"$tool" --version', ]) { expect(runnerBuildJob).toContain(toolchainInput); } + expect(runnerBuildJob).not.toContain( + "paperclip-runner/e2e-build-toolchain/v1", + ); + expect(runnerBuildJob).not.toContain( + "paperclip-runner/e2e-build-inputs/v1", + ); expect( runnerBuildJob.match( /if: steps\.restore_build_cache\.outputs\.cache-hit != 'true'/gu,