diff --git a/.github/tests/runner-controller-artifact-transport.test.mjs b/.github/tests/runner-controller-artifact-transport.test.mjs new file mode 100644 index 00000000000..2c144cae80a --- /dev/null +++ b/.github/tests/runner-controller-artifact-transport.test.mjs @@ -0,0 +1,12 @@ +import test from 'node:test'; +import { verifyControllerRuntime } from '../../packages/paperclip-runner/scripts/controlled-evals/verify-runtime.mjs'; + +test('the uploaded controller preserves every inventoried runtime file', { + skip: !process.env.PAPERCLIP_CONTROLLER_TRANSPORT_DIRECTORY, +}, async () => { + await verifyControllerRuntime({ + directory: process.env.PAPERCLIP_CONTROLLER_TRANSPORT_DIRECTORY, + sourceRevision: process.env.GITHUB_SHA, + receiptSha256: process.env.PAPERCLIP_CONTROLLER_RECEIPT_SHA256, + }); +}); diff --git a/.github/workflows/runner-protocol-live-evals.yml b/.github/workflows/runner-protocol-live-evals.yml index 3fd5f41d0d2..ba3bd0d14c0 100644 --- a/.github/workflows/runner-protocol-live-evals.yml +++ b/.github/workflows/runner-protocol-live-evals.yml @@ -513,10 +513,25 @@ jobs: uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: name: runner-protocol-runtime-${{ github.run_id }}-${{ github.run_attempt }} + include-hidden-files: true path: ${{ runner.temp }}/runner-protocol-runtime/ retention-days: 30 if-no-files-found: error + - name: Download controller artifact for a credential-free transport test + if: needs.authorize.outputs.accounting_mode == 'controlled' + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 + with: + artifact-ids: ${{ steps.upload.outputs.artifact-id }} + path: ${{ runner.temp }}/runner-controller-roundtrip + + - name: Verify the real uploaded controller inventory before private preparation + if: needs.authorize.outputs.accounting_mode == 'controlled' + env: + PAPERCLIP_CONTROLLER_TRANSPORT_DIRECTORY: ${{ runner.temp }}/runner-controller-roundtrip + PAPERCLIP_CONTROLLER_RECEIPT_SHA256: ${{ steps.build.outputs.runtime_receipt_sha256 }} + run: node --test .github/tests/runner-controller-artifact-transport.test.mjs + prepare_controller: name: Prepare one shared cost and trace manifest needs: [authorize, catalog, build_runner, build_controller] diff --git a/packages/paperclip-runner/docs/runner-protocol-live-evals.md b/packages/paperclip-runner/docs/runner-protocol-live-evals.md index 478eaf869cc..94801d17540 100644 --- a/packages/paperclip-runner/docs/runner-protocol-live-evals.md +++ b/packages/paperclip-runner/docs/runner-protocol-live-evals.md @@ -952,3 +952,9 @@ For this policy version, `operatingReconciliation` is always `pending`; the immutable controller summary has no operating invoice settlement step. Mixed histories retain that warning when any campaign uses the policy. A shared `accountingReadiness` label appears only when every campaign uses it. + + +The trusted controller artifact preserves hidden dependency metadata as well as +code. Its inventory verifier requires every recorded byte, including package +`.npmignore` files. Only the fresh provider-credential-free controller build uses +this upload setting; private attempts still use the protected archive path. diff --git a/tests/runner-e2e/workflow-security.test.ts b/tests/runner-e2e/workflow-security.test.ts index d68df923c6a..698f7e71575 100644 --- a/tests/runner-e2e/workflow-security.test.ts +++ b/tests/runner-e2e/workflow-security.test.ts @@ -981,6 +981,12 @@ it("keeps dependency resolution and shared caches out of Runner protocol archive } const builder = jobs.find(job => job.startsWith(" build_controller:"))!; expect(builder).not.toMatch(/id-token: write|environment:|secrets\./u); + const upload = builder.split(" - name: Retain the trusted controller code for every worker\n")[1]!.split("\n - ")[0]!; + expect(upload).toContain("include-hidden-files: true"); + expect(upload).toContain("id: upload"); + expect(builder).toContain("artifact-ids: ${{ steps.upload.outputs.artifact-id }}"); + expect(builder).toContain("run: node --test .github/tests/runner-controller-artifact-transport.test.mjs"); + expect(builder).toContain("PAPERCLIP_CONTROLLER_RECEIPT_SHA256: ${{ steps.build.outputs.runtime_receipt_sha256 }}"); const preparer = jobs.find(job => job.startsWith(" prepare_controller:"))!; expect(preparer).not.toMatch(/pnpm install/u); expect(preparer.indexOf("verify-runtime.mjs")).toBeLessThan(preparer.indexOf('hosted.mjs" prepare'));