mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-11 14:10:50 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - The runner E2E workflow executes paid everyday workflow stories on disposable CI hosts > - The everyday artifact oracle requires a pinned Python image and fails closed when it is absent > - Fresh CI hosts did not prepare this image before the paid cell, so project stories failed during preflight > - This pull request prepares and verifies the pinned image before the affected everyday cells > - The benefit is reliable artifact isolation checks on fresh trusted CI hosts ## Linked Issues or Issue Description **What happened?** Fresh trusted CI runners did not have the pinned Python artifact oracle image. **Expected behavior:** The workflow prepares and verifies the pinned image before an everyday project story starts. **Steps to reproduce:** Run an everyday project story on a fresh CI host without the image cached. The `everyday-artifact.py --preflight` check fails before task creation. **Paperclip version or commit:** `master` at `bd51f157e`. **Deployment mode:** Other: GitHub Actions trusted paid workflow. ## What Changed - Add a matrix-gated CI step for everyday project and recovery cells. - Check Docker, pull the fixed digest with bounded timeouts, and verify the exact repo digest. - Apply the existing Codex sandbox preparation to both native Codex profiles, including the mini profile. - Add workflow security assertions for ordering, condition, digest, timeouts, and secret isolation. - Document that CI prepares the pinned oracle image. - Check provisioning eligibility against every catalog cell, and scope the Daytona registry inspection assertion to the Daytona image job. ## Verification - `pnpm test:e2e:runner:unit` — 24 files and 222 tests passed. - `pnpm test:e2e:runner:typecheck` — passed. - `pnpm exec vitest run --config tests/runner-e2e/vitest.config.ts workflow-security.test.ts` — 10 tests passed. - Python artifact oracle calibration — 12/12 passed. - `git diff --check` — passed. ## Risks Low risk. The image step runs only for everyday cells that execute the artifact preflight. The Codex setup now covers both native Codex profiles. It uses a fixed public image digest and has no provider credentials. ## Model Used OpenAI gpt-5.6-luna (implementation subagent) and gpt-6-astra (review fixes and orchestration), using code execution and repository tools. Context window size is not exposed by 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 - [x] All Paperclip CI gates are green - [x] 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> Co-authored-by: Paperclip <paperclip@paperclip.ing>
93 lines
3.7 KiB
TypeScript
93 lines
3.7 KiB
TypeScript
import { readFile } from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
|
|
import { runnerMatrix } from "./catalog.js";
|
|
import { requiresCodexCiSandbox } from "./codex-ci-sandbox.js";
|
|
|
|
const root = path.resolve(import.meta.dirname, "../..");
|
|
|
|
describe("Codex CI sandbox trust boundary", () => {
|
|
it.each([
|
|
["everyday-workflows.runner-codex.local.build-revise", true],
|
|
["everyday-workflows.runner-codex-mini.local.build-revise", true],
|
|
["everyday-workflows.runner-acpx-claude.local.build-revise", false],
|
|
["everyday-workflows.runner-codex.daytona.build-revise", false],
|
|
["core-compatibility.legacy-codex.local.message-marker", false],
|
|
] as const)("qualifies the native local Codex sandbox for %s: %s", (id, expected) => {
|
|
const execution = runnerMatrix.find((cell) => cell.id === id);
|
|
expect(execution, id).toBeDefined();
|
|
expect(requiresCodexCiSandbox(execution!)).toBe(expected);
|
|
});
|
|
|
|
it("keeps trusted CI provisioning in sync with every catalog cell", async () => {
|
|
const workflow = await readFile(
|
|
path.join(root, ".github/workflows/runner-full-stack-e2e.yml"),
|
|
"utf8",
|
|
);
|
|
const condition = workflow.match(
|
|
/- name: Provision Codex sandbox on the disposable trusted runner\n\s+if: ([^\n]+)/,
|
|
)?.[1];
|
|
expect(condition).toMatch(
|
|
/^matrix\.environmentId == 'local' && \(matrix\.profileId == '[^']+'(?: \|\| matrix\.profileId == '[^']+')*\)$/,
|
|
);
|
|
const provisionedProfiles = new Set(
|
|
[...condition!.matchAll(/matrix\.profileId == '([^']+)'/g)].map((match) => match[1]),
|
|
);
|
|
expect([...provisionedProfiles].sort()).toEqual(
|
|
[...new Set(runnerMatrix.filter(requiresCodexCiSandbox).map((cell) => cell.profile.id))].sort(),
|
|
);
|
|
for (const cell of runnerMatrix) {
|
|
expect(
|
|
cell.environment.id === "local" && provisionedProfiles.has(cell.profile.id),
|
|
cell.id,
|
|
).toBe(requiresCodexCiSandbox(cell));
|
|
}
|
|
});
|
|
|
|
it("keeps privileged policy changes out of target-controlled tests", async () => {
|
|
const source = await readFile(
|
|
path.join(root, "tests/runner-e2e/codex-ci-sandbox.ts"),
|
|
"utf8",
|
|
);
|
|
expect(source).not.toMatch(
|
|
/execFileSync\(["']sudo|apparmor_parser|flags=\(unconfined\)/,
|
|
);
|
|
expect(source).toMatch(/"sandbox",\s*"--permission-profile",\s*"paperclip-e2e-probe"/);
|
|
expect(source).toContain(
|
|
"permissions.paperclip-e2e-probe.network.enabled=false",
|
|
);
|
|
expect(source).toContain(
|
|
'permissions.paperclip-e2e-probe.filesystem={":root"="read"}',
|
|
);
|
|
expect(source).toContain("Codex sandbox preflight failed");
|
|
expect(source).not.toContain("...process.env");
|
|
});
|
|
|
|
it("provisions the exact executable in trusted CI before provider credentials", async () => {
|
|
const workflow = await readFile(
|
|
path.join(root, ".github/workflows/runner-full-stack-e2e.yml"),
|
|
"utf8",
|
|
);
|
|
const setup = workflow.indexOf(
|
|
"- name: Provision Codex sandbox on the disposable trusted runner",
|
|
);
|
|
const paid = workflow.indexOf("- name: Run paid cell");
|
|
expect(setup).toBeGreaterThan(
|
|
workflow.indexOf(
|
|
"- name: Reauthorize paid execution before provider access",
|
|
),
|
|
);
|
|
expect(paid).toBeGreaterThan(setup);
|
|
const step = workflow.slice(setup, paid);
|
|
expect(step).toContain("matrix.profileId == 'runner-codex-mini'");
|
|
expect(step).toContain('binary.startsWith(root + "/node_modules/.pnpm/")');
|
|
expect(step).toContain("binary.endsWith(suffix)");
|
|
expect(step).toContain('"-n", "apparmor_parser", "-r", profilePath');
|
|
expect(step).toContain('flag:"wx"');
|
|
expect(step).not.toContain("secrets.");
|
|
expect(step).not.toContain("node scripts/");
|
|
expect(step).not.toContain("sysctl -w");
|
|
});
|
|
});
|