From 6d358e646dc3fb76bfe3e56260f9edf2f3273b36 Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Sun, 30 Aug 2026 01:49:41 -0500 Subject: [PATCH] feat(runner): add Codex native backend (#12374) ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work > - The runner package now has a complete Codex session driver > - The driver needs a package-owned construction boundary before server code can use it > - Persisted provider contracts must not make deferred providers executable > - Provider selection must fail closed when an implementation is not included > - This pull request adds a Codex-only native backend and factory > - It does not expose or enable the Paperclip Runner adapter ## Linked Issues or Issue Description **Subsystem affected** `packages/paperclip-runner` native backend construction. **Problem or motivation** The runner needs one normalized backend seam that constructs the reviewed Codex driver. The seam must not route OpenCode, ACPX, Claude Managed, or AWS AgentCore through an incomplete fallback. **Proposed solution** Add a Codex backend constructor and a Codex-first factory. Reject every deferred provider at the factory and provider-specific constructor boundaries. **Alternatives considered** Routing all provider contracts through the Codex protocol facade would give deferred providers runtime behavior before their implementations are reviewed. Including all provider backends would also broaden this pull request beyond the Codex-first series. **Roadmap alignment** This connects reviewed runner package layers. It does not enable a new adapter or change an existing direct adapter path. ## What Changed - Added the Codex native backend constructor. - Added the Codex-first native backend factory. - Preserved the execution contract, runtime instructions, plan constraints, dynamic tools, transport injection, and durable identity requirements. - Rejected every deferred provider with an explicit error. - Added tests for lazy transport construction and both fail-closed boundaries. ## Verification - `pnpm --filter @paperclipai/paperclip-runner test:typescript` (35 files, 340 tests) - `pnpm -r typecheck` - `pnpm build` - The focused native backend factory suite has 3 passing cases. ## Risks The main risk is starting the wrong provider or starting a provider before its runtime is ready. The factory and Codex constructor both reject non-Codex inputs. Existing direct adapters do not call this package-local factory. ## Model Used OpenAI Codex with GPT-5 and repository tool use. ## 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 --- .../src/backends/codex-native-backend.ts | 84 ++++++++++++++++ .../backends/native-backend-factory.test.ts | 95 +++++++++++++++++++ .../src/backends/native-backend-factory.ts | 41 ++++++++ packages/paperclip-runner/src/index.ts | 4 + 4 files changed, 224 insertions(+) create mode 100644 packages/paperclip-runner/src/backends/codex-native-backend.ts create mode 100644 packages/paperclip-runner/src/backends/native-backend-factory.test.ts create mode 100644 packages/paperclip-runner/src/backends/native-backend-factory.ts diff --git a/packages/paperclip-runner/src/backends/codex-native-backend.ts b/packages/paperclip-runner/src/backends/codex-native-backend.ts new file mode 100644 index 0000000000..ee137a7c85 --- /dev/null +++ b/packages/paperclip-runner/src/backends/codex-native-backend.ts @@ -0,0 +1,84 @@ +import { createCodexTaskEnvelope } from "../contracts/codex.js"; +import type { NativeExecutionInput } from "../contracts/native-execution.js"; +import type { + NativeSessionBackend, + PersistedNativeSession, +} from "../contracts/native-session-backend.js"; +import type { CodexAppServerTransport } from "../drivers/codex/app-server-transport.js"; +import { CodexAppServerDriver } from "../drivers/codex/codex-app-server-driver.js"; +import { HarnessDriverBackend } from "./harness-driver-backend.js"; +import { nativeSystemInstructions, nativeTaskConstraints } from "./runtime-context.js"; + +export interface CodexNativeSessionBackendOptions { + runnerInstanceId?: string; + onSpawn?: (meta: { + pid: number; + processGroupId: number | null; + startedAt: string; + }) => Promise; + transportFactory?: (context?: { + providerRecoveryPolicy?: PersistedNativeSession["providerRecoveryPolicy"]; + }) => CodexAppServerTransport; + dynamicTools?: readonly Readonly>[]; + dynamicToolHandler?: (call: { + tool: string; + callId: string; + threadId: string; + turnId: string; + arguments: unknown; + }) => Promise; +} + +/** + * Constructs the first production-native provider boundary. Other provider + * contracts may already be persisted, but their runtime implementations are + * deliberately shipped in separate provider slices. + */ +export function createCodexNativeSessionBackend( + input: NativeExecutionInput, + options: CodexNativeSessionBackendOptions = {}, +): NativeSessionBackend { + if (input.provider.kind !== "codex") { + throw new Error("Codex native backend requires provider kind codex"); + } + + return new HarnessDriverBackend(new CodexAppServerDriver({ + ...(input.provider.model ? { model: input.provider.model } : {}), + approvalPolicy: input.provider.approvalPolicy ?? "never", + baseInstructions: nativeSystemInstructions(input), + includeSkillInstructions: "runtimeContext" in input, + requestedCollaborationMode: + "executionMode" in input ? input.executionMode : "default", + taskEnvelope: createCodexTaskEnvelope({ + objective: input.completionContract.contract.objective, + contractRevision: input.completionContract.contract.revision, + criteria: input.completionContract.contract.criteria, + constraints: [ + "Work only inside the supplied working directory.", + ...("executionMode" in input && input.executionMode === "plan" + ? [ + "Use native plan collaboration mode and do not modify workspace files.", + "Treat the supplied Paperclip planning context as the canonical pinned base revision.", + "Complete one structured provider plan item; Paperclip will synchronize it after completion.", + "Keep the final response to a short synchronization summary instead of repeating the full plan.", + ] + : []), + ...nativeTaskConstraints(input), + "Return one semantic completion result.", + ], + }), + runnerInstanceId: + options.runnerInstanceId ?? `paperclip-native-${input.binding.runId}`, + onSpawn: options.onSpawn, + transportFactory: options.transportFactory, + dynamicTools: options.dynamicTools, + dynamicToolHandler: options.dynamicToolHandler, + driverIdentity: { + kind: "codex_app_server", + displayName: "Codex app-server", + version: "codex-v2", + }, + collaborationModes: ["default", "plan"], + requireProviderSessionIdentity: options.transportFactory !== undefined, + })); +} diff --git a/packages/paperclip-runner/src/backends/native-backend-factory.test.ts b/packages/paperclip-runner/src/backends/native-backend-factory.test.ts new file mode 100644 index 0000000000..57a644b341 --- /dev/null +++ b/packages/paperclip-runner/src/backends/native-backend-factory.test.ts @@ -0,0 +1,95 @@ +import { describe, expect, it } from "vitest"; + +import type { NativeExecutionInput } from "../contracts/native-execution.js"; +import { createNativeSessionBackend } from "../index.js"; +import { createCodexNativeSessionBackend } from "./codex-native-backend.js"; + +function execution( + provider: NativeExecutionInput["provider"] = { + kind: "codex", + model: null, + approvalPolicy: "never", + }, +): NativeExecutionInput { + return { + schema: "paperclip.native-execution-input.v1", + binding: { + companyId: "company", + runId: "run", + issueId: "issue", + agentId: "agent", + executionWorkspaceId: "workspace", + }, + task: { + identifier: "PAP-1", + title: "Exercise Codex native routing", + description: null, + prompt: "Complete the task.", + workMode: "standard", + }, + workspace: { + cwd: "/workspace", + repoUrl: null, + repoRef: null, + branchName: null, + }, + session: { + normalizedSessionId: "session", + driverKind: "codex_app_server", + protocolVersion: 1, + lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null }, + }, + provider, + completionContract: { + id: "contract", + sha256: "sha256", + schemaVersion: "1", + contract: { + revision: "revision", + objective: "Complete the task.", + criteria: [], + }, + }, + interactionResponses: [], + credentialBindings: [], + }; +} + +describe("native backend factory", () => { + it("constructs the Codex backend without starting its transport", async () => { + const backend = createNativeSessionBackend(execution(), { + codexTransportFactory: () => { + throw new Error("descriptor must not launch the transport"); + }, + }); + + await expect(backend.descriptor()).resolves.toMatchObject({ + kind: "runner", + name: "codex_app_server", + version: "codex-v2", + capabilities: { + collaborationModes: ["default", "plan"], + }, + }); + }); + + it("fails closed when a deferred provider reaches the factory", () => { + expect(() => + createNativeSessionBackend(execution({ + kind: "opencode", + model: "openrouter/model", + })), + ).toThrow( + "Native backend for opencode is not included in the Codex-first runner", + ); + }); + + it("guards the provider-specific constructor as a second boundary", () => { + expect(() => + createCodexNativeSessionBackend(execution({ + kind: "opencode", + model: "openrouter/model", + })), + ).toThrow("Codex native backend requires provider kind codex"); + }); +}); diff --git a/packages/paperclip-runner/src/backends/native-backend-factory.ts b/packages/paperclip-runner/src/backends/native-backend-factory.ts new file mode 100644 index 0000000000..0ce8ebe458 --- /dev/null +++ b/packages/paperclip-runner/src/backends/native-backend-factory.ts @@ -0,0 +1,41 @@ +import type { NativeExecutionInput } from "../contracts/native-execution.js"; +import type { + NativeSessionBackend, + PersistedNativeSession, +} from "../contracts/native-session-backend.js"; +import type { CodexAppServerTransport } from "../drivers/codex/app-server-transport.js"; +import { + createCodexNativeSessionBackend, + type CodexNativeSessionBackendOptions, +} from "./codex-native-backend.js"; + +export interface NativeBackendFactoryOptions + extends Omit { + codexTransportFactory?: (context?: { + providerRecoveryPolicy?: PersistedNativeSession["providerRecoveryPolicy"]; + }) => CodexAppServerTransport; +} + +/** + * Selects only provider implementations included in this release slice. + * Persisted contracts for future providers do not make those providers + * executable before their independently reviewed runtime ships. + */ +export function createNativeSessionBackend( + input: NativeExecutionInput, + options: NativeBackendFactoryOptions = {}, +): NativeSessionBackend { + if (input.provider.kind !== "codex") { + throw new Error( + `Native backend for ${input.provider.kind} is not included in the Codex-first runner`, + ); + } + + return createCodexNativeSessionBackend(input, { + runnerInstanceId: options.runnerInstanceId, + onSpawn: options.onSpawn, + dynamicTools: options.dynamicTools, + dynamicToolHandler: options.dynamicToolHandler, + transportFactory: options.codexTransportFactory, + }); +} diff --git a/packages/paperclip-runner/src/index.ts b/packages/paperclip-runner/src/index.ts index a25d3f32ac..c7c9d706e3 100644 --- a/packages/paperclip-runner/src/index.ts +++ b/packages/paperclip-runner/src/index.ts @@ -11,6 +11,10 @@ export * from "./contracts/question-set.js"; export * from "./contracts/runtime-context.js"; export * from "./contracts/types.js"; export * from "./backends/harness-driver-backend.js"; +export { + createNativeSessionBackend, + type NativeBackendFactoryOptions, +} from "./backends/native-backend-factory.js"; export * from "./native-session-runtime.js"; export { DurablePrpControlPlane,