mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-27 19:35:13 +01: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>
319 lines
11 KiB
TypeScript
319 lines
11 KiB
TypeScript
import { describe, expect, it } from "vitest";
|
|
import type { NativeEvidenceAssessment } from "./evidence-classifier.js";
|
|
import { arbitrateNativeStatus } from "./status-arbiter.js";
|
|
|
|
function assessment(overrides: Partial<NativeEvidenceAssessment> = {}): NativeEvidenceAssessment {
|
|
return {
|
|
objectiveClaimSatisfied: true,
|
|
objectiveSatisfied: true,
|
|
allCriteriaSatisfied: true,
|
|
verificationPassed: true,
|
|
hasFailedVerification: false,
|
|
hasBlockingRemainingWork: false,
|
|
reportedDisposition: "done",
|
|
summary: "Complete",
|
|
contractRevisionMatches: true,
|
|
criterionAssessments: [],
|
|
verificationAssessments: [],
|
|
verificationCaveats: [],
|
|
acceptedEvidenceRefs: ["event:2"],
|
|
missingRequirements: [],
|
|
rejectedEvidence: [],
|
|
unverifiableEvidence: [],
|
|
blocker: null,
|
|
continuation: null,
|
|
attentionRequests: [],
|
|
ignoredAttentionRequests: [],
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
function arbitrate(overrides: Partial<Parameters<typeof arbitrateNativeStatus>[0]> = {}) {
|
|
return arbitrateNativeStatus({
|
|
assessment: assessment(),
|
|
terminalState: "succeeded",
|
|
workspaceFinalizeStatus: "succeeded",
|
|
agentId: "agent",
|
|
priorIssueStatus: "in_progress",
|
|
...overrides,
|
|
});
|
|
}
|
|
|
|
describe("native status authority", () => {
|
|
it("marks done only from successful finalization and complete durable evidence", () => {
|
|
expect(arbitrate()).toEqual(expect.objectContaining({
|
|
statusAction: "done",
|
|
toStatus: "done",
|
|
reasonCode: "completion_contract_satisfied",
|
|
effects: [{ kind: "release_checkout" }],
|
|
}));
|
|
expect(arbitrate({ completionClaimPolicyAccepted: true })).toEqual(expect.objectContaining({
|
|
statusAction: "done",
|
|
reasonCode: "completion_claim_policy_accepted",
|
|
}));
|
|
expect(arbitrate({
|
|
assessment: assessment({ verificationPassed: false, missingRequirements: ["test"] }),
|
|
})).toEqual(expect.objectContaining({
|
|
statusAction: "in_review",
|
|
toStatus: "in_review",
|
|
reasonCode: "external_verification_required",
|
|
effects: [expect.objectContaining({ kind: "bind_reviewer" })],
|
|
}));
|
|
const claimOnly = assessment({
|
|
objectiveSatisfied: false,
|
|
allCriteriaSatisfied: false,
|
|
verificationPassed: false,
|
|
criterionAssessments: [{
|
|
criterionId: "objective",
|
|
claimStatus: "satisfied",
|
|
outcome: "missing",
|
|
evidenceRefs: [],
|
|
reasonCode: "criterion_evidence_missing",
|
|
}],
|
|
verificationAssessments: [{
|
|
commandOrCheck: "Answered the question",
|
|
claimStatus: "passed",
|
|
outcome: "unverifiable",
|
|
evidenceRef: null,
|
|
reasonCode: "verification_has_no_durable_reference",
|
|
reportedReasonCode: null,
|
|
detail: null,
|
|
}],
|
|
acceptedEvidenceRefs: [],
|
|
missingRequirements: ["objective"],
|
|
});
|
|
expect(arbitrate({ assessment: claimOnly })).toEqual(expect.objectContaining({
|
|
toStatus: "in_review",
|
|
reasonCode: "external_verification_required",
|
|
}));
|
|
expect(arbitrate({ assessment: claimOnly, completionClaimPolicyAccepted: true })).toEqual(expect.objectContaining({
|
|
toStatus: "done",
|
|
reasonCode: "completion_claim_policy_accepted",
|
|
}));
|
|
});
|
|
|
|
it("creates explicit liveness paths for review, continuation, cancellation, and governance", () => {
|
|
expect(arbitrate({
|
|
assessment: assessment({ reportedDisposition: "needs_review" }),
|
|
})).toEqual(expect.objectContaining({
|
|
toStatus: "in_review",
|
|
effects: [expect.objectContaining({ kind: "bind_reviewer" })],
|
|
}));
|
|
expect(arbitrate({
|
|
assessment: assessment({
|
|
reportedDisposition: "yielded",
|
|
continuation: { kind: "retry", summary: "Retry the task", idempotencyKey: "retry-task" },
|
|
}),
|
|
})).toEqual(expect.objectContaining({
|
|
toStatus: "in_progress",
|
|
effects: [expect.objectContaining({ kind: "enqueue_continuation", continuationKind: "retry" })],
|
|
}));
|
|
expect(arbitrate({ terminalState: "cancelled" })).toEqual(expect.objectContaining({
|
|
toStatus: "in_progress",
|
|
effects: [expect.objectContaining({ kind: "release_run_resources" })],
|
|
}));
|
|
expect(arbitrate({ governanceGate: { kind: "interaction", id: "interaction" } })).toEqual(expect.objectContaining({
|
|
toStatus: "in_review",
|
|
reasonCode: "governed_gate_pending",
|
|
effects: [
|
|
{ kind: "create_interaction", gate: { kind: "interaction", id: "interaction" } },
|
|
{ kind: "notify_owner", agentId: "agent", reason: "governed_gate_pending" },
|
|
],
|
|
}));
|
|
expect(arbitrate({
|
|
governanceGate: { kind: "interaction", id: "interaction" },
|
|
assessment: assessment({
|
|
reportedDisposition: "yielded",
|
|
continuation: {
|
|
kind: "response_wake",
|
|
summary: "Resume from the response",
|
|
idempotencyKey: "interaction-response:interaction",
|
|
},
|
|
}),
|
|
})).toEqual(expect.objectContaining({
|
|
toStatus: "in_review",
|
|
reasonCode: "governed_response_waiting",
|
|
effects: [{ kind: "create_interaction", gate: { kind: "interaction", id: "interaction" } }],
|
|
}));
|
|
});
|
|
|
|
it("accepts low-risk completion claims with unrun verification caveats", () => {
|
|
const withCaveat = assessment({
|
|
verificationPassed: false,
|
|
criterionAssessments: [{
|
|
criterionId: "objective",
|
|
claimStatus: "satisfied",
|
|
outcome: "missing",
|
|
evidenceRefs: [],
|
|
reasonCode: "criterion_evidence_missing",
|
|
}],
|
|
verificationAssessments: [{
|
|
commandOrCheck: "Run npm test",
|
|
claimStatus: "not_run",
|
|
outcome: "missing",
|
|
evidenceRef: null,
|
|
reasonCode: "verification_not_run",
|
|
reportedReasonCode: "tool_unavailable",
|
|
detail: "Node and npm are unavailable in this environment.",
|
|
}],
|
|
verificationCaveats: [{
|
|
commandOrCheck: "Run npm test",
|
|
reasonCode: "tool_unavailable",
|
|
detail: "Node and npm are unavailable in this environment.",
|
|
}],
|
|
});
|
|
|
|
expect(arbitrate({ assessment: withCaveat, completionClaimPolicyAccepted: true }))
|
|
.toEqual(expect.objectContaining({
|
|
toStatus: "done",
|
|
reasonCode: "completion_claim_policy_accepted",
|
|
}));
|
|
});
|
|
|
|
it("sends failed verification and actionable attention to owned review without retrying", () => {
|
|
const failed = assessment({
|
|
verificationPassed: false,
|
|
hasFailedVerification: true,
|
|
verificationAssessments: [{
|
|
commandOrCheck: "Run npm test",
|
|
claimStatus: "failed",
|
|
outcome: "rejected",
|
|
evidenceRef: null,
|
|
reasonCode: "verification_reported_failed",
|
|
reportedReasonCode: null,
|
|
detail: "One test failed.",
|
|
}],
|
|
});
|
|
expect(arbitrate({
|
|
assessment: failed,
|
|
completionClaimPolicyAccepted: true,
|
|
reviewOwnerUserId: "user-1",
|
|
})).toEqual(expect.objectContaining({
|
|
toStatus: "in_review",
|
|
reasonCode: "completion_claim_conflict",
|
|
effects: [expect.objectContaining({ kind: "bind_reviewer", ownerUserId: "user-1" })],
|
|
}));
|
|
|
|
const withAttention = assessment({
|
|
attentionRequests: [{
|
|
kind: "approval",
|
|
summary: "Approve publication",
|
|
ownerClass: "human",
|
|
targetAgentId: null,
|
|
sourceIndex: 0,
|
|
sourceKind: "approval",
|
|
legacy: false,
|
|
}],
|
|
});
|
|
expect(arbitrate({ assessment: withAttention, completionClaimPolicyAccepted: true }))
|
|
.toEqual(expect.objectContaining({
|
|
toStatus: "in_review",
|
|
reasonCode: "actionable_attention_pending",
|
|
}));
|
|
});
|
|
|
|
it("blocks only for a task-wide blocker with a named owner and action", () => {
|
|
expect(arbitrate({
|
|
assessment: assessment({
|
|
reportedDisposition: "blocked",
|
|
blocker: { boardOwned: true, scope: "task_wide", unblockAction: "Approve access" },
|
|
}),
|
|
})).toEqual(expect.objectContaining({
|
|
toStatus: "blocked",
|
|
unblockDescriptor: { owner: "board", action: "Approve access" },
|
|
effects: [
|
|
{ kind: "bind_blocker", owner: "board", action: "Approve access" },
|
|
{ kind: "notify_owner", agentId: "agent", reason: "task_wide_blocker_bound" },
|
|
],
|
|
}));
|
|
});
|
|
|
|
it("blocks a current-track result when a durable dependency already gates the issue", () => {
|
|
expect(arbitrate({
|
|
assessment: assessment({
|
|
reportedDisposition: "blocked",
|
|
blocker: {
|
|
boardOwned: false,
|
|
scope: "current_track",
|
|
unblockAction: "Wait for child task DOT-52",
|
|
},
|
|
}),
|
|
hasUnresolvedIssueBlockers: true,
|
|
})).toEqual(expect.objectContaining({
|
|
statusAction: "blocked",
|
|
toStatus: "blocked",
|
|
reasonCode: "durable_dependency_blocker_bound",
|
|
unblockDescriptor: {
|
|
owner: { agentId: "agent" },
|
|
action: "Wait for child task DOT-52",
|
|
},
|
|
effects: [],
|
|
}));
|
|
});
|
|
|
|
it("lets a durable dependency override an optimistic done result", () => {
|
|
expect(arbitrate({
|
|
assessment: assessment({ reportedDisposition: "done", blocker: null }),
|
|
completionClaimPolicyAccepted: true,
|
|
hasUnresolvedIssueBlockers: true,
|
|
})).toEqual(expect.objectContaining({
|
|
statusAction: "blocked",
|
|
toStatus: "blocked",
|
|
reasonCode: "durable_dependency_blocker_bound",
|
|
effects: [],
|
|
}));
|
|
});
|
|
|
|
it("does not create a duplicate review after this run's review was already accepted", () => {
|
|
expect(arbitrate({
|
|
assessment: assessment({
|
|
reportedDisposition: "needs_review",
|
|
attentionRequests: [{
|
|
kind: "review",
|
|
summary: "Accept the plan",
|
|
ownerClass: "human",
|
|
targetAgentId: null,
|
|
sourceIndex: 0,
|
|
sourceKind: "review",
|
|
legacy: false,
|
|
}],
|
|
}),
|
|
governanceResolvedForRun: true,
|
|
})).toEqual(expect.objectContaining({
|
|
statusAction: "in_review",
|
|
reasonCode: "governance_response_continuation_queued",
|
|
effects: [],
|
|
}));
|
|
});
|
|
|
|
it("does not create a duplicate review after this run's planning confirmation was already accepted", () => {
|
|
expect(arbitrate({
|
|
assessment: assessment({
|
|
reportedDisposition: "blocked",
|
|
attentionRequests: [{
|
|
kind: "approval",
|
|
summary: "Accept the pinned plan revision.",
|
|
ownerClass: "human",
|
|
targetAgentId: null,
|
|
sourceIndex: 0,
|
|
sourceKind: "approval",
|
|
legacy: false,
|
|
}],
|
|
}),
|
|
governanceResolvedForRun: true,
|
|
})).toEqual(expect.objectContaining({
|
|
statusAction: "in_review",
|
|
reasonCode: "governance_response_continuation_queued",
|
|
effects: [],
|
|
}));
|
|
});
|
|
|
|
it("preserves authoritative terminal statuses", () => {
|
|
expect(arbitrate({ priorIssueStatus: "done" })).toEqual(expect.objectContaining({
|
|
toStatus: "done",
|
|
reasonCode: "terminal_status_preserved",
|
|
effects: [],
|
|
}));
|
|
});
|
|
});
|