mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-18 05:23:39 +02:00
fix(hermes): admit verified generated assets without weakening source checks
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
a49b9c20b9
commit
1b31d77c17
5 files changed
+75
-11
No files matched your search
@@ -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.
|
||||
@@ -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");
|
||||
});
|
||||
});
|
||||
@@ -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:"));
|
||||
|
||||
Reference in new issue
Block a user