fix(hermes): verify approved qualification lockfile

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
DottaandPaperclip committed 2026-10-07 21:49:17 -05:00
1 parent 12098ba3fd
commit 805d787b16
5 files changed
+82 -8

No files matched your search

@@ -1085,6 +1085,7 @@ jobs:
PAPERCLIP_E2E_DAYTONA_IMAGE: ${{ needs.daytona_image.outputs.image }}
PAPERCLIP_RUNNER_REMOTE_PROVIDER_PACK_PATH: ${{ github.workspace }}/packages/paperclip-runner/provider-pack
PAPERCLIP_E2E_CAMPAIGN_ID: gha-${{ github.run_id }}-${{ github.run_attempt }}-${{ matrix.executionId }}
PAPERCLIP_RUNNER_E2E_LOCK_SHA256: ${{ needs.target_lock.outputs.lock_sha256 }}
PAPERCLIP_RUNNER_E2E_SOURCE_SHA: ${{ needs.authorize.outputs.target_sha }}
PAPERCLIP_RUNNER_E2E_SOURCE_REF: ${{ needs.authorize.outputs.target_ref }}
PAPERCLIP_PLAYWRIGHT_CHANNEL: ${{ needs.authorize.outputs.playwright_channel }}
@@ -855,3 +855,12 @@ billing. The initial sandbox socket denial is a separate infrastructure outcome.
Hermes remains pending the full original Mac/Daytona, subscription, connection,
attachment/control/state/routine and public-consumer gates. The ledger still
retains $23 against the $25 cap; unknown charges are not counted as free.
Review of `12098ba3` found that the source check rejected the trusted paid
workflow's resolved target lock. Admission now allows only its unstaged tracked
`pnpm-lock.yaml` replacement after independently hashing the complete regular
file against the workflow's approved SHA-256. The campaign receipt preserves
that digest and reports the actual dirty state instead of calling the working
tree clean. Other edits, staged replacements, missing/mismatched approval,
deletions and symlinks remain rejected. All eighteen source-admission tests and
Product E2E TypeScript pass. The root lockfile itself remains unchanged.
+4
View File
@@ -1489,6 +1489,10 @@ Detached checkouts record `HEAD` unless an explicitly matched target SHA supplie
its ref. Read-only `--list` and `--matrix-json` discovery require no source
admission or credentials. This receipt identifies the controller; runtime/image
provenance and live behavior still require their independent checks.
The paid workflow may restore its reviewed target lockfile: only an unstaged
modification of the tracked `pnpm-lock.yaml` is allowed, and its complete bytes
must match `PAPERCLIP_RUNNER_E2E_LOCK_SHA256`. The receipt records that approved
digest and the actual working-tree cleanliness. Other edits remain rejected.
When a Hermes cell is selected, the trusted workflow provisions its pinned
Python closure on Linux before exposing credentials, and selects the same
+42 -2
View File
@@ -1,5 +1,6 @@
import { execFileSync } from "node:child_process";
import { mkdtempSync, realpathSync, rmSync, writeFileSync } from "node:fs";
import { createHash } from "node:crypto";
import { 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";
@@ -16,7 +17,8 @@ function checkout() {
env: { PATH: process.env.PATH, GIT_CONFIG_GLOBAL: devNull, GIT_CONFIG_NOSYSTEM: "1" } }).trim();
git("init", "-b", "qualification");
writeFileSync(join(root, "source.txt"), "original\n");
git("add", "source.txt");
writeFileSync(join(root, "pnpm-lock.yaml"), "original lock\n");
git("add", "source.txt", "pnpm-lock.yaml");
git("-c", "user.name=Fixture", "-c", "user.email=fixture@example.test", "-c", "commit.gpgsign=false", "-c", `core.hooksPath=${devNull}`, "commit", "-m", "fixture");
return { root, git, sha: git("rev-parse", "HEAD") };
}
@@ -62,3 +64,41 @@ describe("Hermes qualification controller source admission", () => {
expect(() => prepareHermesQualificationSource(selected, "/does-not-exist", env)).toThrow("readable Git source before credentials");
});
});
describe("approved qualification dependency lock", () => {
const resolved = "reviewed resolved lock\n";
const digest = createHash("sha256").update(resolved).digest("hex");
it("admits only the independently verified lock replacement and records its digest and dirty state", () => {
const f = checkout(); writeFileSync(join(f.root, "pnpm-lock.yaml"), resolved);
const env = { PATH: process.env.PATH, PAPERCLIP_RUNNER_E2E_LOCK_SHA256: digest };
expect(prepareHermesQualificationSource(selected, f.root, env)).toMatchObject({
sha: f.sha, workingTreeClean: false, approvedLockSha256: digest,
});
expect(resolveRunnerE2ESource(null, env).sha).toBe(f.sha);
});
it("still records a verified lock when it already matches the commit", () => {
const f = checkout();
const originalDigest = createHash("sha256").update("original lock\n").digest("hex");
expect(prepareHermesQualificationSource(selected, f.root, {
PATH: process.env.PATH, PAPERCLIP_RUNNER_E2E_LOCK_SHA256: originalDigest,
})).toMatchObject({ workingTreeClean: true, approvedLockSha256: originalDigest });
});
it.each(["missing", "incorrect", "malformed"])("rejects a replacement with %s approval before recording source", approval => {
const f = checkout(); writeFileSync(join(f.root, "pnpm-lock.yaml"), resolved);
const env: NodeJS.ProcessEnv = { PATH: process.env.PATH,
...(approval === "missing" ? {} : { PAPERCLIP_RUNNER_E2E_LOCK_SHA256: approval === "incorrect" ? "a".repeat(64) : "not-a-digest" }) };
expect(() => prepareHermesQualificationSource(selected, f.root, env)).toThrow();
expect(env.PAPERCLIP_RUNNER_E2E_SOURCE_SHA).toBeUndefined();
});
it.each(["source", "untracked", "staged", "deleted", "symlink"])("rejects %s changes even alongside an approved digest", kind => {
const f = checkout(); const lock = join(f.root, "pnpm-lock.yaml"); writeFileSync(lock, resolved);
if (kind === "source") writeFileSync(join(f.root, "source.txt"), "changed\n");
if (kind === "untracked") writeFileSync(join(f.root, "extra.txt"), "changed\n");
if (kind === "staged") f.git("add", "pnpm-lock.yaml");
if (kind === "deleted" || kind === "symlink") rmSync(lock);
if (kind === "symlink") { writeFileSync(join(f.root, "resolved.txt"), resolved); symlinkSync("resolved.txt", lock); }
expect(() => prepareHermesQualificationSource(selected, f.root, {
PATH: process.env.PATH, PAPERCLIP_RUNNER_E2E_LOCK_SHA256: digest,
})).toThrow();
});
});
+26 -6
View File
@@ -1,5 +1,8 @@
import { execFileSync } from "node:child_process";
import { createHash } from "node:crypto";
import { closeSync, constants, fstatSync, openSync, readFileSync } from "node:fs";
import { devNull } from "node:os";
import { join } from "node:path";
import type { MatrixExecution } from "./types.js";
import { resolveRunnerE2ESource } from "./source.js";
@@ -20,27 +23,44 @@ export function prepareHermesQualificationSource(
return execFileSync("git", ["-c", "core.fsmonitor=false", ...args], {
cwd: repositoryRoot, env: gitEnvironment, encoding: "utf8",
timeout: 10_000, maxBuffer: 1024 * 1024, stdio: ["ignore", "pipe", "pipe"],
}).trim();
});
} catch {
throw new Error("Hermes qualification needs readable Git source before credentials; use a clean checkout.");
}
};
const sha = git(["rev-parse", "HEAD"]);
const sha = git(["rev-parse", "HEAD"]).trim();
if (!/^[a-f0-9]{40}$/.test(sha)) throw new Error("Hermes qualification source SHA is invalid.");
if (git(["status", "--porcelain", "--untracked-files=normal"])) {
const changes = git(["status", "--porcelain", "-z", "--untracked-files=normal"]).split("\0").filter(Boolean);
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)) {
throw new Error("Hermes qualification needs a clean checkout before credentials; commit or remove source changes.");
}
if (approvedLock) {
let fd: number | undefined;
try {
fd = openSync(join(repositoryRoot, "pnpm-lock.yaml"), constants.O_RDONLY | constants.O_NOFOLLOW);
const stat = fstatSync(fd);
if (!stat.isFile() || stat.size > 16 * 1024 * 1024
|| createHash("sha256").update(readFileSync(fd)).digest("hex") !== approvedLock) throw new Error();
} catch {
throw new Error("Hermes qualification lockfile differs from the approved digest before credentials.");
} finally {
if (fd !== undefined) closeSync(fd);
}
}
const requestedSha = environment.PAPERCLIP_RUNNER_E2E_SOURCE_SHA?.trim();
if (requestedSha && requestedSha !== sha) {
throw new Error("Hermes qualification source SHA differs from the checked-out controller before credentials.");
}
const ref = git(["rev-parse", "--symbolic-full-name", "HEAD"]);
if (git(["rev-parse", "HEAD"]) !== sha) throw new Error("Hermes qualification source changed during admission.");
const ref = git(["rev-parse", "--symbolic-full-name", "HEAD"]).trim();
if (git(["rev-parse", "HEAD"]).trim() !== sha) throw new Error("Hermes qualification source changed during admission.");
// These explicit values survive local-env loading and reach every child and
// synthetic failure. A workflow's own GITHUB_SHA may identify another commit.
environment.PAPERCLIP_RUNNER_E2E_SOURCE_SHA = sha;
environment.PAPERCLIP_RUNNER_E2E_SOURCE_REF =
(requestedSha && environment.PAPERCLIP_RUNNER_E2E_SOURCE_REF?.trim()) || ref;
return { schema: "paperclip.hermes.e2e-source/v1", workingTreeClean: true,
return { schema: "paperclip.hermes.e2e-source/v1", workingTreeClean: changes.length === 0,
...(approvedLock ? { approvedLockSha256: approvedLock } : {}),
...resolveRunnerE2ESource(null, environment) };
}