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>
135 lines
4.4 KiB
TypeScript
135 lines
4.4 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { Db } from "@paperclipai/db";
|
|
import { classifyNativeEvidence } from "./evidence-classifier.js";
|
|
|
|
const workProductId = "00000000-0000-4000-8000-000000000001";
|
|
const evidenceRef = `work_product:${workProductId}`;
|
|
|
|
function evidenceDb(row: Record<string, unknown> = {
|
|
id: workProductId,
|
|
status: "approved",
|
|
reviewState: "approved",
|
|
}): Db {
|
|
return {
|
|
select: () => ({
|
|
from: () => ({
|
|
where: () => ({
|
|
limit: () => Promise.resolve([row]),
|
|
}),
|
|
}),
|
|
}),
|
|
} as unknown as Db;
|
|
}
|
|
|
|
function result(status: "satisfied" | "not_satisfied", objectiveSatisfied: boolean) {
|
|
return {
|
|
reportedWorkDisposition: "done",
|
|
summary: "Classified",
|
|
completionClaim: {
|
|
contractRevision: "1",
|
|
objectiveSatisfied,
|
|
criteria: [{ criterionId: "objective", status, evidenceRefs: [evidenceRef] }],
|
|
remainingWork: [],
|
|
},
|
|
verification: [{ commandOrCheck: "test", status: "passed", artifactRef: evidenceRef }],
|
|
};
|
|
}
|
|
|
|
const input = {
|
|
db: evidenceDb(),
|
|
companyId: "company",
|
|
issueId: "issue",
|
|
runId: "run",
|
|
contract: {
|
|
revision: "1",
|
|
objective: "Classify evidence",
|
|
criteria: [{ id: "objective", requirement: "Use accepted durable evidence" }],
|
|
},
|
|
};
|
|
|
|
describe("classifyNativeEvidence", () => {
|
|
it("accepts a satisfied claim backed by an approved durable work product", async () => {
|
|
await expect(classifyNativeEvidence({
|
|
...input,
|
|
result: result("satisfied", true),
|
|
})).resolves.toEqual(expect.objectContaining({
|
|
objectiveSatisfied: true,
|
|
allCriteriaSatisfied: true,
|
|
verificationPassed: true,
|
|
acceptedEvidenceRefs: [evidenceRef],
|
|
}));
|
|
});
|
|
|
|
it("does not turn durable evidence into completion when the claim says the criterion is unsatisfied", async () => {
|
|
const assessment = await classifyNativeEvidence({
|
|
...input,
|
|
result: result("not_satisfied", false),
|
|
});
|
|
expect(assessment).toEqual(expect.objectContaining({
|
|
objectiveSatisfied: false,
|
|
allCriteriaSatisfied: false,
|
|
verificationPassed: true,
|
|
}));
|
|
expect(assessment.criterionAssessments).toEqual([
|
|
expect.objectContaining({ outcome: "rejected", reasonCode: "criterion_reported_not_satisfied" }),
|
|
]);
|
|
});
|
|
|
|
it("normalizes DOT-29-style environment attention into a non-blocking verification caveat", async () => {
|
|
const assessment = await classifyNativeEvidence({
|
|
...input,
|
|
result: {
|
|
...result("satisfied", true),
|
|
verification: [{ commandOrCheck: "Run npm test", status: "not_run" }],
|
|
attentionRequests: [{
|
|
kind: "environment_constraint",
|
|
summary: "Node and npm are unavailable in this sandbox.",
|
|
}],
|
|
},
|
|
});
|
|
|
|
expect(assessment.objectiveClaimSatisfied).toBe(true);
|
|
expect(assessment.hasFailedVerification).toBe(false);
|
|
expect(assessment.attentionRequests).toEqual([]);
|
|
expect(assessment.verificationCaveats).toEqual([{
|
|
commandOrCheck: "Run npm test",
|
|
reasonCode: "tool_unavailable",
|
|
detail: "Node and npm are unavailable in this sandbox.",
|
|
}]);
|
|
expect(assessment.ignoredAttentionRequests).toEqual([
|
|
expect.objectContaining({
|
|
sourceKind: "environment_constraint",
|
|
disposition: "verification_caveat",
|
|
}),
|
|
]);
|
|
});
|
|
|
|
it("accepts an annotated interaction UUID without sending the annotation to Postgres", async () => {
|
|
const interactionId = "00000000-0000-4000-8000-000000000029";
|
|
const annotatedRef = `interaction:${interactionId} (request_confirmation, status accepted)`;
|
|
const assessment = await classifyNativeEvidence({
|
|
...input,
|
|
db: evidenceDb({ id: interactionId, status: "accepted", result: { outcome: "accepted" } }),
|
|
result: {
|
|
reportedWorkDisposition: "done",
|
|
summary: "Plan accepted",
|
|
completionClaim: {
|
|
contractRevision: "1",
|
|
objectiveSatisfied: true,
|
|
criteria: [{ criterionId: "objective", status: "satisfied", evidenceRefs: [annotatedRef] }],
|
|
remainingWork: [],
|
|
},
|
|
verification: [],
|
|
},
|
|
});
|
|
|
|
expect(assessment.objectiveSatisfied).toBe(true);
|
|
expect(assessment.acceptedEvidenceRefs).toEqual([annotatedRef]);
|
|
expect(assessment.criterionAssessments[0]?.evidenceRefs[0]).toMatchObject({
|
|
durableRecordId: interactionId,
|
|
outcome: "accepted",
|
|
reasonCode: "interaction_resolved",
|
|
});
|
|
});
|
|
});
|