fix(hermes): admit verified generated assets without weakening source checks

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-08 12:28:13 -05:00
1 parent a49b9c20b9
commit 1b31d77c17
5 files changed
+75 -11

No files matched your search

+9 -9
View File
@@ -711,18 +711,18 @@ jobs:
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: ${{ needs.build_runner_artifacts.outputs.build_artifact_name }}
path: runner-e2e-build
path: ${{ runner.temp }}/runner-e2e-build
- name: Verify and restore shared TypeScript outputs
if: needs.catalog.outputs.needs_remote_provider_pack == 'true'
run: |
set -euo pipefail
(
cd runner-e2e-build
cd "$RUNNER_TEMP/runner-e2e-build"
sha256sum --check runner-e2e-build-bundle.tar.gz.sha256
)
tar --extract --gzip \
--file runner-e2e-build/runner-e2e-build-bundle.tar.gz \
--file "$RUNNER_TEMP/runner-e2e-build/runner-e2e-build-bundle.tar.gz" \
--directory "$GITHUB_WORKSPACE"
test -d packages/paperclip-eval-kernel/dist
test -d packages/paperclip-runner/dist
@@ -920,14 +920,14 @@ jobs:
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: ${{ needs.build_runner_artifacts.outputs.build_artifact_name }}
path: runner-e2e-build
path: ${{ runner.temp }}/runner-e2e-build
- name: Download immutable remote provider pack
if: matrix.environmentId == 'daytona' && (matrix.profileId == 'runner-opencode' || startsWith(matrix.profileId, 'runner-acpx-'))
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
name: ${{ needs.build_remote_provider_pack.outputs.provider_pack_artifact_name }}
path: runner-e2e-provider-pack
path: ${{ runner.temp }}/runner-e2e-provider-pack
- name: Verify and restore campaign outputs
if: startsWith(matrix.profileId, 'runner-') || matrix.suiteId == 'openrouter-model-breadth'
@@ -937,11 +937,11 @@ jobs:
run: |
set -euo pipefail
(
cd runner-e2e-build
cd "$RUNNER_TEMP/runner-e2e-build"
sha256sum --check runner-e2e-build-bundle.tar.gz.sha256
)
tar --extract --gzip \
--file runner-e2e-build/runner-e2e-build-bundle.tar.gz \
--file "$RUNNER_TEMP/runner-e2e-build/runner-e2e-build-bundle.tar.gz" \
--directory "$GITHUB_WORKSPACE"
test -d packages/paperclip-eval-kernel/dist
if [ "$NEEDS_RUNNER_TYPESCRIPT" = true ]; then
@@ -958,11 +958,11 @@ jobs:
run: |
set -euo pipefail
(
cd runner-e2e-provider-pack
cd "$RUNNER_TEMP/runner-e2e-provider-pack"
sha256sum --check runner-e2e-provider-pack.tar.gz.sha256
)
tar --extract --gzip \
--file runner-e2e-provider-pack/runner-e2e-provider-pack.tar.gz \
--file "$RUNNER_TEMP/runner-e2e-provider-pack/runner-e2e-provider-pack.tar.gz" \
--directory "$GITHUB_WORKSPACE"
jq -e \
--arg revision "$IMAGE_SOURCE_REVISION" \
@@ -1125,3 +1125,12 @@ runner binary. Master changed Rust inputs, so the retained Mac binary cannot
qualify the synchronized source. Fresh Rust builds run in cloud CI. A new Mac
binary, current image, and the outstanding live matrix remain qualification
requirements. No local Docker or Rust build is used for this synchronization.
The trusted E2E workflow now downloads its checksum-bound build and provider-pack
archives under `RUNNER_TEMP`, outside the controller checkout. Source admission
checks every tracked change and all other untracked files. It excludes only
untracked content inside the generated Mac/Linux Hermes assets and provider-pack
roots; separate verified runtime admission still checks their manifests and
bytes before credentials. A 43-test source/workflow selection passes, including
tracked asset edits, staged/deleted files, root symlinks, neighboring unmanaged
assets, and archive placement. These are fixture checks, not paid workflow proof.
+33 -1
View File
@@ -1,6 +1,6 @@
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import { mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { mkdirSync, mkdtempSync, realpathSync, rmSync, symlinkSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { devNull, tmpdir } from "node:os";
import { afterEach, describe, expect, it } from "vitest";
@@ -102,3 +102,35 @@ describe("approved qualification dependency lock", () => {
})).toThrow();
});
});
describe("generated runtime source admission", () => {
const assetRoot = "packages/paperclip-runner/provider-assets/hermes/linux-x64";
function generated(root: string, relative: string) {
const directory = join(root, relative); mkdirSync(directory, { recursive: true });
writeFileSync(join(directory, "manifest.json"), "fixture runtime bytes\n");
}
it("admits untracked Mac/Linux runtime assets and a verified pack without hiding source changes", () => {
const f = checkout();
for (const root of [assetRoot, "packages/paperclip-runner/provider-assets/hermes/darwin-arm64", "packages/paperclip-runner/provider-pack"]) generated(f.root, root);
expect(prepareHermesQualificationSource(selected, f.root, { PATH: process.env.PATH })).toMatchObject({ sha: f.sha, workingTreeClean: true });
writeFileSync(join(f.root, "source.txt"), "changed source\n");
expect(() => prepareHermesQualificationSource(selected, f.root, { PATH: process.env.PATH })).toThrow("clean checkout before credentials");
});
it.each(["runner-e2e-build", "runner-e2e-provider-pack", "packages/paperclip-runner/provider-assets/hermes/win32-x64", "packages/paperclip-runner/provider-assets/unmanaged"])("rejects untracked files in %s", root => {
const f = checkout(); generated(f.root, assetRoot); generated(f.root, root);
expect(() => prepareHermesQualificationSource(selected, f.root, { PATH: process.env.PATH })).toThrow("clean checkout before credentials");
});
it.each(["modified", "staged", "deleted"])("rejects %s tracked files inside a generated asset root", kind => {
const f = checkout(); generated(f.root, assetRoot); f.git("add", assetRoot);
f.git("-c", "user.name=Fixture", "-c", "user.email=fixture@example.test", "-c", "commit.gpgsign=false", "-c", `core.hooksPath=${devNull}`, "commit", "-m", "tracked asset fixture");
const file = join(f.root, assetRoot, "manifest.json");
if (kind === "deleted") rmSync(file); else writeFileSync(file, "changed tracked asset\n");
if (kind === "staged") f.git("add", assetRoot);
expect(() => prepareHermesQualificationSource(selected, f.root, { PATH: process.env.PATH })).toThrow("clean checkout before credentials");
});
it("rejects a runtime-root symlink instead of treating it as generated content", () => {
const f = checkout(); const parent = join(f.root, "packages/paperclip-runner/provider-assets/hermes"); mkdirSync(parent, { recursive: true });
symlinkSync(f.root, join(parent, "linux-x64"));
expect(() => prepareHermesQualificationSource(selected, f.root, { PATH: process.env.PATH })).toThrow("clean checkout before credentials");
});
});
+13 -1
View File
@@ -30,7 +30,19 @@ export function prepareHermesQualificationSource(
};
const sha = git(["rev-parse", "HEAD"]).trim();
if (!/^[a-f0-9]{40}$/.test(sha)) throw new Error("Hermes qualification source SHA is invalid.");
const changes = git(["status", "--porcelain", "-z", "--untracked-files=normal"]).split("\0").filter(Boolean);
// Check every tracked source change, including anything tracked in an asset
// directory. Only untracked files in these generated runtime roots are omitted;
// their manifests and bytes are verified separately before credentials arrive.
const generatedRuntimeRoots = [
"packages/paperclip-runner/provider-assets/hermes/linux-x64",
"packages/paperclip-runner/provider-assets/hermes/darwin-arm64",
"packages/paperclip-runner/provider-pack",
];
const trackedChanges = git(["status", "--porcelain", "-z", "--untracked-files=no"]).split("\0").filter(Boolean);
const untrackedSource = git(["ls-files", "--others", "--exclude-standard", "-z", "--", ".",
...generatedRuntimeRoots.map(root => `:(exclude)${root}/**`),
]).split("\0").filter(Boolean);
const changes = [...trackedChanges, ...untrackedSource.map(file => `?? ${file}`)];
const approvedLock = environment.PAPERCLIP_RUNNER_E2E_LOCK_SHA256?.trim();
if (approvedLock && !/^[a-f0-9]{64}$/.test(approvedLock)) throw new Error("Hermes qualification approved lock digest is invalid.");
if (changes.some(change => change !== " M pnpm-lock.yaml" || !approvedLock)) {
@@ -79,6 +79,17 @@ describe("public repository paid workflow security", () => {
expect(provision).toContain("uv==0.12.17");
expect(provision).toContain("scripts/provision-hermes.mjs");
});
it("downloads and verifies qualification archives outside controller source", async () => {
const workflow = await readFile(path.join(repositoryRoot, ".github/workflows/runner-full-stack-e2e.yml"), "utf8");
expect(workflow.match(/path: \$\{\{ runner.temp \}\}\/runner-e2e-build/gu)).toHaveLength(2);
expect(workflow.match(/path: \$\{\{ runner.temp \}\}\/runner-e2e-provider-pack/gu)).toHaveLength(1);
expect(workflow.match(/cd "\$RUNNER_TEMP\/runner-e2e-build"/gu)).toHaveLength(2);
expect(workflow).toContain('cd "$RUNNER_TEMP/runner-e2e-provider-pack"');
expect(workflow.match(/--file "\$RUNNER_TEMP\/runner-e2e-build\/runner-e2e-build-bundle.tar.gz"/gu)).toHaveLength(2);
expect(workflow).toContain('--file "$RUNNER_TEMP/runner-e2e-provider-pack/runner-e2e-provider-pack.tar.gz"');
expect(workflow).not.toMatch(/path: runner-e2e-(?:build|provider-pack)\n/u);
expect(workflow).not.toMatch(/--file runner-e2e-(?:build|provider-pack)\//u);
});
it("keeps the manual EC2 image build credential-free and pins the authorized target", async () => {
const workflow = await readFile(path.join(repositoryRoot, ".github/workflows/docker-runner-check.yml"), "utf8");
const manual = workflow.slice(workflow.indexOf(" authorize_manual:"));