mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-28 20:21:50 +01:00
<!-- entire-trail-link-start --> https://entire.com/gh/paperclipai/-/trails/98/changes/paperclip/98 <!-- entire-trail-link-end --> ## Thinking Path > - Paperclip manages agents and records their work. > - Product E2E campaigns measure real agent behavior and model spend. > - A paid hosted run completed both cases and all ten model receipts. > - Its report job could not install dependencies from the committed lock. > - The trusted checkout needs a reviewed dependency correction. > - This change applies a reviewed lock correction before the frozen report install. ## Linked Issues or Issue Description Refs: #17928 **Description** The report in [run 40576181788](https://github.com/paperclipai/paperclip/actions/runs/40576181788) failed with `ERR_PNPM_LOCKFILE_CONFIG_MISMATCH`. Both paid cases had passed. The worker and build jobs already restored the resolved source lock, but the report did not. **Steps to reproduce** Dispatch the bounded daily campaign on the default branch with a stale committed lock configuration. Workers use the resolved lock. The report uses the stale lock and fails before report generation. **Expected behavior** The report installs frozen dependencies from a verified lock for the exact trusted source. Evidence can be reported without repeating paid work. ## What Changed - Apply a reviewed lock correction from the trusted report checkout before its frozen install. Use the same correction for the qualification image build, which also requires a compatible frozen lock. - Pin the original lock, correction and output SHA-256. The trusted authorization job rejects unknown inputs before any paid work. - Apply the same correction to trusted manual history publication. Qualification image builds apply it only when their target equals the workflow revision; other targets keep their own lock. - Update existing Claude ACP and postgres patch identities and link the declared yaml dependency. Existing package tarball integrity entries stay unchanged; the declared yaml dependency adds exactly yaml@2.9.1 with pinned integrity. - Keep runtime registry resolution and worker target locks out of the credential-bearing report. - Test valid correction, idempotence and changed-lock, changed-patch and wrong-output rejection. ## Verification - Focused workflow and lock-correction tests: 36 passed. - `actionlint .github/workflows/runner-full-stack-e2e.yml` passed. - Original worker evidence: 2/2 passed, ten final model receipts, zero unresolved model requests. - The original report was regenerated from retained evidence with zero provider calls and saved privately with replay provenance. - Broader repository checks run in CI. Prior full Product checks passed for #17928. ## Risks The correction is tied to the reviewed committed lock. Dependency input changes need a reviewed correction update. No new registry version or lifecycle script can enter the privileged report through this fix. The original failed workflow stays failed. No root lockfile is committed. ## Model Used OpenAI Codex, GPT-6 family, with reasoning, repository inspection and code execution. The exact serving model ID and context window are not exposed to this session. ## Checklist - [x] I have included a thinking path that traces from project context to this change - [x] I have specified the model used (with version and capability details) - [x] I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work - [x] I have searched GitHub for duplicate or related PRs and linked them above - [x] I have either (a) linked existing issues with `Fixes: #` / `Closes #` / `Refs #` OR (b) described the issue in-PR following the relevant issue template - [x] I have not referenced internal/instance-local Paperclip issues or links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip` URLs) - [x] My branch name describes the change (e.g. `docs/...`, `fix/...`) and contains no internal Paperclip ticket id or instance-derived details - [x] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [x] I have updated relevant documentation to reflect my changes - [x] I have considered and documented any risks above - [ ] All Paperclip CI gates are green - [ ] Greptile is 5/5 with no open P2s, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --------- Co-authored-by: Paperclip <noreply@paperclip.ing>
68 lines
3.6 KiB
TypeScript
68 lines
3.6 KiB
TypeScript
import { spawnSync } from "node:child_process";
|
|
import { mkdtemp, readFile, rm, access } from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
const queuedSha = "a".repeat(40);
|
|
const advancedSha = "b".repeat(40);
|
|
|
|
async function resolveTarget(event: string, cadence: string, branch = "master") {
|
|
const workflow = await readFile(new URL("../../.github/workflows/runner-full-stack-e2e.yml", import.meta.url), "utf8");
|
|
const step = workflow.split(" - name: Resolve requested repository branch to an immutable commit\n")[1]!.split("\n - name:")[0]!;
|
|
const script = step.split(" run: |\n")[1]!.replace(/^ {10}/gm, "");
|
|
const directory = await mkdtemp(path.join(os.tmpdir(), "eval-workflow-target-"));
|
|
try {
|
|
const output = path.join(directory, "output");
|
|
const result = spawnSync("bash", ["-c", `gh() { printf '%s\\n' "$MOCK_BRANCH_SHA"; }\n${script}`], {
|
|
encoding: "utf8", env: { ...process.env, EVENT_NAME: event, EVAL_CADENCE: cadence,
|
|
TARGET_BRANCH: branch, DEFAULT_BRANCH: "master", GITHUB_SHA: queuedSha,
|
|
MOCK_BRANCH_SHA: advancedSha, GITHUB_OUTPUT: output, REPOSITORY: "paperclipai/paperclip" },
|
|
});
|
|
return { status: result.status, stderr: result.stderr,
|
|
output: await readFile(output, "utf8").catch(() => "") };
|
|
} finally { await rm(directory, { recursive: true, force: true }); }
|
|
}
|
|
|
|
describe("paid workflow target authorization", () => {
|
|
it.each([["schedule", "manual"], ["workflow_dispatch", "daily"], ["workflow_dispatch", "weekly"]])(
|
|
"pins %s/%s to the queued revision when the branch advances", async (event, cadence) => {
|
|
const result = await resolveTarget(event, cadence);
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(result.output).toContain(`sha=${queuedSha}\n`);
|
|
expect(result.output).not.toContain(advancedSha);
|
|
});
|
|
|
|
it("rejects custom bounded targets before emitting a commit", async () => {
|
|
const result = await resolveTarget("workflow_dispatch", "daily", "development");
|
|
expect(result.status).toBe(1);
|
|
expect(result.output).toBe("");
|
|
expect(result.stderr).toContain("default-branch commit");
|
|
});
|
|
|
|
it("still resolves explicit manual targets at authorization time", async () => {
|
|
const result = await resolveTarget("workflow_dispatch", "manual", "development");
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(result.output).toContain(`sha=${advancedSha}\n`);
|
|
expect(result.output).toContain("ref=refs/heads/development\n");
|
|
});
|
|
});
|
|
|
|
describe("qualification image dependency correction", () => {
|
|
it.each([true, false])("applies the workflow correction only to its own source: %s", async sameSource => {
|
|
const workflow = await readFile(new URL("../../.github/workflows/docker-runner-check.yml", import.meta.url), "utf8");
|
|
const step = workflow.split(" - name: Apply reviewed qualification lock corrections\n")[1]!.split("\n - ")[0]!;
|
|
const script = step.split(" run: |\n")[1]!.replace(/^ {10}/gm, "");
|
|
const directory = await mkdtemp(path.join(os.tmpdir(), "eval-image-lock-target-"));
|
|
try {
|
|
const marker = path.join(directory, "called");
|
|
const result = spawnSync("bash", ["-c", `node() { touch "$CALLED"; }\n${script}`], {
|
|
cwd: directory, encoding: "utf8", env: { ...process.env, GITHUB_SHA: queuedSha,
|
|
TARGET_SHA: sameSource ? queuedSha : advancedSha, CALLED: marker },
|
|
});
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(await access(marker).then(() => true, () => false)).toBe(sameSource);
|
|
} finally { await rm(directory, { recursive: true, force: true }); }
|
|
});
|
|
});
|