mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-21 00:05:49 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The runner package is useful only when the application can start, observe, and recover a native Codex run safely. > - Existing direct adapters must keep their current execution and finalization paths. > - The application boundary therefore needs additive persistence, authorization, coordination, and recovery behind an explicit experimental adapter. > - This pull request adds that Codex-only boundary without activating generalized providers, remote environments, or the later task/SDK surfaces. ## Linked Issues or Issue Description **Subsystem affected** Shared contracts, database persistence, adapter utilities, server native-runtime services, and the experimental Paperclip Runner adapter. **Problem or motivation** The already-landed runner package has a qualified Codex path, but the application needs durable native-run state, guarded runtime selection, authenticated coordination, tool security, finalization, and recovery before the experimental adapter can be exercised safely. **Proposed solution** Add a Codex-only `paperclip_runner` application path behind the existing default-off native-runner setting. Bind native state and coordination to company/run identity, preserve persisted-run recovery, and leave every direct adapter on its existing legacy execution path. **Alternatives considered** The earlier stack boundary introduced a generalized executor and remote-environment lifecycle here. That made this PR depend on implementations in higher PRs and changed reusable sandbox behavior globally. Those pieces are now deferred together to #12592. **Roadmap alignment** ROADMAP.md does not list a conflicting native-runner integration project. This change adds the application boundary for the existing Runner architecture. ## What Changed - Added native run/result/finalization/provider-trace persistence, shared validators, and idempotent migration/replay coverage. - Added guarded Codex-only runtime selection, authenticated PRP coordination, recovery, finalization, and interaction services. - Added run/company-bound tool-gateway authorization, credential redaction, SSRF protections, and replay-safe behavior. - Added the explicit `paperclip_runner` adapter behind the default-off rollout setting. - Preserved legacy answered-question wake projection and direct-adapter execution/finalization paths. - Hardened cancellation so only owned in-memory child processes are signaled; persisted recycled PIDs/process groups are never trusted. - Retained the narrow Claude ACPX isolated-context security follow-up discovered after #12590. - Deferred the generalized executor, provider ingress, remote lifecycle, SDK/lab/eval work, release-process changes, and lockfile. ## Verification - Changed-file delta against `master`: 133 files. - GitHub Actions is the authoritative verification environment for this PR. - Full CI, security, and Greptile review will run on this lowest unmerged stack PR. - Local tests/build/typecheck were not run because this checkout is resource constrained. - Static diff/reference checks pass, and `pnpm-lock.yaml` is unchanged. ## Risks - This touches central heartbeat and agent-route code, so legacy compatibility is the primary risk. - Runtime selection remains Codex-only and explicit; direct Codex, Claude, OpenCode, process, HTTP, and plugin adapters remain on their existing paths. - Fresh native starts fail closed while the rollout flag is off; persisted native records remain readable and recoverable. - Cancellation, company/run binding, tool calls, status decisions, and completion writes are guarded or replay-safe. > For core feature work, check [ROADMAP.md](ROADMAP.md) first and discuss it in #dev before opening the PR. Feature PRs that overlap with planned core work may need to be redirected. ## Model Used OpenAI Codex, GPT-5.6, with repository tools, code execution, and parallel agent review. ## 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 linked existing issues or described the issue in-PR following the relevant issue template - [x] I have not referenced internal or instance-local Paperclip issues or links - [x] My branch name describes the change and contains no internal Paperclip ticket id - [ ] I have run tests locally and they pass — GitHub Actions is authoritative for this resource-constrained checkout - [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 risks above - [ ] All Paperclip CI and security gates are green - [ ] Greptile is 5/5 with no open actionable findings - [x] I will address all Greptile and reviewer comments before merge ## Stack - Position: 3 of 5 overall; lowest of 3 currently unmerged - Base: `master` - Previous: [#12590](https://github.com/paperclipai/paperclip/pull/12590), qualified Claude ACPX runtime — merged - Next: [#12592](https://github.com/paperclipai/paperclip/pull/12592), generalized Codex executor, task experience, and developer SDKs --------- Co-authored-by: Dev Agent <dev@paperclip.ing>
89 lines
3.0 KiB
TypeScript
89 lines
3.0 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import { projectCodexWorkspaceDiffsFromTrace } from "./provider-trace-workspace-diff-reprojection.js";
|
|
|
|
function frame(frameId: number, payload: unknown) {
|
|
return {
|
|
kind: "frame",
|
|
direction: "provider_to_client",
|
|
frameId,
|
|
rawBase64: Buffer.from(JSON.stringify(payload)).toString("base64"),
|
|
};
|
|
}
|
|
|
|
function createdFile(path: string, additions: number) {
|
|
return [
|
|
`diff --git a/${path} b/${path}`,
|
|
"new file mode 100644",
|
|
"--- /dev/null",
|
|
`+++ b/${path}`,
|
|
`@@ -0,0 +1,${additions} @@`,
|
|
...Array.from({ length: additions }, (_, index) => `+line ${index + 1}`),
|
|
].join("\n");
|
|
}
|
|
|
|
describe("provider trace workspace diff reprojection", () => {
|
|
it("replays the minimized DOT-220 final snapshot as one seven-file event", () => {
|
|
const turnId = "01a03964-9fa6-7550-a87e-b6acc72e372a";
|
|
const first = createdFile("README.md", 9);
|
|
const final = [
|
|
createdFile("README.md", 9),
|
|
createdFile("package.json", 1),
|
|
createdFile("public/game.js", 17),
|
|
createdFile("public/index.html", 2),
|
|
createdFile("public/style.css", 1),
|
|
createdFile("server.js", 6),
|
|
createdFile("test/server.test.js", 5),
|
|
].join("\n");
|
|
const projection = projectCodexWorkspaceDiffsFromTrace([
|
|
frame(1, { method: "turn/diff/updated", params: { turnId, diff: first } }),
|
|
frame(2, { method: "turn/diff/updated", params: { turnId, diff: final } }),
|
|
]);
|
|
|
|
expect(projection.skipReasons).toEqual([]);
|
|
expect(projection.turns).toHaveLength(1);
|
|
expect(projection.turns[0]?.payload).toMatchObject({
|
|
changeSetId: `${turnId}:workspace`,
|
|
revision: 2,
|
|
source: "runner_verified",
|
|
complete: true,
|
|
totals: { files: 7, additions: 41, deletions: 0 },
|
|
});
|
|
expect(projection.turns[0]?.payload.files.map((file) => file.path)).toEqual([
|
|
"README.md",
|
|
"package.json",
|
|
"public/game.js",
|
|
"public/index.html",
|
|
"public/style.css",
|
|
"server.js",
|
|
"test/server.test.js",
|
|
]);
|
|
});
|
|
|
|
it("keeps prior valid turns while reporting malformed snapshots", () => {
|
|
const projection = projectCodexWorkspaceDiffsFromTrace([
|
|
frame(1, {
|
|
method: "turn/diff/updated",
|
|
params: { turnId: "turn-1", diff: createdFile("src/a.ts", 1) },
|
|
}),
|
|
frame(2, {
|
|
method: "turn/diff/updated",
|
|
params: { turnId: "../../unsafe", diff: createdFile("src/b.ts", 1) },
|
|
}),
|
|
frame(3, {
|
|
method: "turn/diff/updated",
|
|
params: { turnId: "turn-2", diff: "not a unified patch" },
|
|
}),
|
|
frame(4, {
|
|
method: "turn/diff/updated",
|
|
params: { turnId: "turn-3", diff: "" },
|
|
}),
|
|
]);
|
|
|
|
expect(projection.turns.map((turn) => turn.turnId)).toEqual(["turn-1", "turn-3"]);
|
|
expect(projection.skipReasons).toEqual([
|
|
expect.objectContaining({ reason: "missing_turn_id", frameId: 2 }),
|
|
expect.objectContaining({ reason: "malformed_diff", turnId: "turn-2", frameId: 3 }),
|
|
]);
|
|
});
|
|
});
|