From 30ef14edd4e7290d9eac43ca7b7835611933cc74 Mon Sep 17 00:00:00 2001 From: Dotta <34892728+cryppadotta@users.noreply.github.com> Date: Sun, 30 Aug 2026 21:11:36 -0500 Subject: [PATCH] feat(runner): wire the Codex ACPX backend (#12406) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The runner package gives provider runtimes one normalized session boundary. > - The merged Codex ACPX driver implements that boundary and keeps its cleanup ownership bounded. > - The package backend factory still needs a narrow route from a persisted Codex ACPX execution input to that qualified driver. > - The route must validate the stored qualification snapshot and reject unsupported providers before runtime admission. > - This pull request wires only the qualified Codex ACPX driver into the package-local backend factory. > - The benefit is a fail-closed backend construction path without enabling a server adapter, changing execution selection, or adding another provider. ## Linked Issues or Issue Description Refs #12405 This pull request builds on the Codex ACPX harness driver merged in #12405. It adds only the package-local backend factory route for that driver. ## What Changed - Add an internal Codex ACPX native backend constructor. - Require provider kind `acpx` and agent `codex` at the provider-specific boundary. - Resolve the qualified Codex ACPX profile for the requested model. - Compare the persisted driver kind, protocol version, ACPX version, agent profile, package versions, runtime package fields, and command digest with the qualified profile. - Compose the existing native system instructions and task constraints for the Codex ACPX driver. - Route qualified Codex ACPX inputs through the native backend factory only when the caller supplies an explicit instance runtime directory. - Pass the scoped environment, managed credential source, dynamic tools, and ACPX tool handler through the factory boundary. - Keep Pi, Claude, OpenCode, managed Claude, AgentCore, and every other deferred ACPX agent unavailable. - Add factory tests for qualified construction, the explicit runtime-root requirement, unsupported ACPX agents, and qualification-snapshot drift. - Keep ACPX construction lazy. Reading the backend descriptor does not start ACPX transport or a provider process. ## Verification - Replay base: `74aabb7ea6c5cf373f2254e5f854a5233b745ddc` (`master` after #12405 merged). - Exact replay head: `827090a0cff870eedb99fa1b5cfda8bb87efa515`. - Stable patch ID for the exact replay delta: `e0f6733a5a124a4fb93155257358b966c354a176` (identical to the original narrow source patch). - The exact pull request delta contains exactly three files: - `packages/paperclip-runner/src/backends/codex-acpx-native-backend.ts` - `packages/paperclip-runner/src/backends/native-backend-factory.ts` - `packages/paperclip-runner/src/backends/native-backend-factory.test.ts` - The exact source delta is 193 additions and 10 deletions. - This delta does not change dependencies, `pnpm-lock.yaml`, workspace configuration, workflows, migrations, server selection, UI behavior, or public package exports. - Focused GitHub test coverage: **PASS**. The exact-head Build job passed all six native backend factory tests and all 720 TypeScript runner tests. - GitHub Actions: **PASS** for exact head `827090a0cff870eedb99fa1b5cfda8bb87efa515`. Every applicable job passed. Failed-job-only reruns cleared unrelated database timeout and server concurrency flakes without changing the patch. The Storybook visual regression skipped intentionally because this backend-only delta does not touch UI or Storybook paths. - Security checks: **PASS** for the exact head. Superagent, Snyk, both Socket checks, and contributor trust completed successfully. - Greptile: **5/5** for the exact head, with no open P1/P2 findings, recommendations, or follow-ups. - No local test result is claimed. GitHub Actions is the authoritative verification environment for the replayed revision. ## Risks This change has low package-local runtime risk. It adds a new branch to the native backend factory, but no server or runnerd factory selects an ACPX execution in this pull request. The route fails closed when the runtime directory is absent, the agent is not Codex, or the persisted qualification snapshot differs from the qualified Codex profile. The constructor repeats the provider-kind and agent checks as a second boundary. Existing direct adapters do not use this route. The change does not add a migration, dependency, lockfile update, workflow, UI surface, public export, or production rollout flag. > 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 — check the roadmap first. See `CONTRIBUTING.md`. ## Model Used OpenAI Codex with GPT-5.6, extended reasoning, repository tool use, and code execution. ## 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/...`) and contains no internal Paperclip ticket id or instance-derived details - [ ] I have run tests locally and they pass - [x] I have added or updated tests where applicable - [ ] 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 P1/P2 findings, recommendations, or follow-ups - [x] I will address all Greptile and reviewer comments before requesting merge --- .../src/backends/codex-acpx-native-backend.ts | 71 ++++++++++++++ .../backends/native-backend-factory.test.ts | 98 +++++++++++++++++-- .../src/backends/native-backend-factory.ts | 34 ++++++- 3 files changed, 193 insertions(+), 10 deletions(-) create mode 100644 packages/paperclip-runner/src/backends/codex-acpx-native-backend.ts diff --git a/packages/paperclip-runner/src/backends/codex-acpx-native-backend.ts b/packages/paperclip-runner/src/backends/codex-acpx-native-backend.ts new file mode 100644 index 0000000000..caedc8996e --- /dev/null +++ b/packages/paperclip-runner/src/backends/codex-acpx-native-backend.ts @@ -0,0 +1,71 @@ +import type { NativeExecutionInput } from "../contracts/native-execution.js"; +import type { NativeSessionBackend } from "../contracts/native-session-backend.js"; +import { + CodexAcpxDriver, + type CodexAcpxDriverOptions, +} from "../drivers/acpx/codex-acpx-driver.js"; +import { resolveQualifiedAcpxProfile } from "../drivers/acpx/qualified-profiles.js"; +import { HarnessDriverBackend } from "./harness-driver-backend.js"; +import { + nativeSystemInstructions, + nativeTaskConstraints, +} from "./runtime-context.js"; + +export interface CodexAcpxNativeSessionBackendOptions extends Omit< + CodexAcpxDriverOptions, + "model" | "permissionMode" | "systemInstructions" +> {} + +/** + * Constructs the qualified Codex ACPX backend. Other ACPX agents remain + * unavailable until their runtime, policy, and conformance slices ship. + */ +export function createCodexAcpxNativeSessionBackend( + input: NativeExecutionInput, + options: CodexAcpxNativeSessionBackendOptions, +): NativeSessionBackend { + if (input.provider.kind !== "acpx" || input.provider.agent !== "codex") { + throw new Error( + "Codex ACPX backend requires provider kind acpx with agent codex", + ); + } + const qualifiedProfile = resolveQualifiedAcpxProfile( + "codex", + input.provider.model, + ); + for (const field of [ + "driverKind", + "protocolVersion", + "acpxVersion", + "agent", + "agentProfileVersion", + "agentServerPackage", + "agentServerVersion", + "agentRuntimePackage", + "agentRuntimeVersion", + "commandDigest", + ] as const) { + if (input.provider.profile[field] !== qualifiedProfile[field]) { + throw new Error( + `Persisted Codex ACPX profile does not match the qualified ${field}`, + ); + } + } + + const constraints = nativeTaskConstraints(input); + const systemInstructions = [ + nativeSystemInstructions(input), + "", + "Paperclip Runner constraints:", + ...constraints.map((constraint) => `- ${constraint}`), + ].join("\n"); + + return new HarnessDriverBackend( + new CodexAcpxDriver({ + ...options, + model: input.provider.model, + permissionMode: input.provider.permissionMode ?? "approve-reads", + systemInstructions, + }), + ); +} diff --git a/packages/paperclip-runner/src/backends/native-backend-factory.test.ts b/packages/paperclip-runner/src/backends/native-backend-factory.test.ts index 57a644b341..004193d6b4 100644 --- a/packages/paperclip-runner/src/backends/native-backend-factory.test.ts +++ b/packages/paperclip-runner/src/backends/native-backend-factory.test.ts @@ -55,6 +55,44 @@ function execution( }; } +function acpxExecution(agent: "codex" | "pi" = "codex"): NativeExecutionInput { + return { + ...execution(), + session: { + normalizedSessionId: "session", + driverKind: "acpx_runtime", + protocolVersion: 1, + lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null }, + }, + provider: { + kind: "acpx", + agent, + model: + agent === "codex" + ? "gpt-5.6-sol" + : "openrouter/deepseek/deepseek-v4-flash-0731", + permissionPolicy: "interactive", + profile: { + driverKind: "acpx_runtime", + protocolVersion: 1, + acpxVersion: "0.13.1", + agent, + agentProfileVersion: 1, + agentServerPackage: + agent === "codex" ? "@agentclientprotocol/codex-acp" : "pi-acp", + agentServerVersion: agent === "codex" ? "1.6.2" : "0.0.33", + agentRuntimePackage: + agent === "codex" ? null : "@earendil-works/pi-coding-agent", + agentRuntimeVersion: agent === "codex" ? null : "0.84.2", + commandDigest: + agent === "codex" + ? "sha256:94049b3e3c3aee87de62703786e4fa81d031d7bd979f99bdf516d84f28791a79" + : "sha256:8c696f38296d53d0061fa11534570c5ddd951b63532aed30e0f1fcc676dc169f", + }, + }, + }; +} + describe("native backend factory", () => { it("constructs the Codex backend without starting its transport", async () => { const backend = createNativeSessionBackend(execution(), { @@ -75,21 +113,65 @@ describe("native backend factory", () => { it("fails closed when a deferred provider reaches the factory", () => { expect(() => - createNativeSessionBackend(execution({ - kind: "opencode", - model: "openrouter/model", - })), + createNativeSessionBackend( + execution({ + kind: "opencode", + model: "openrouter/model", + }), + ), ).toThrow( "Native backend for opencode is not included in the Codex-first runner", ); }); + it("constructs the qualified Codex ACPX backend without starting ACPX", async () => { + const backend = createNativeSessionBackend(acpxExecution(), { + acpxRuntimeDirectory: "/runtime", + }); + + await expect(backend.descriptor()).resolves.toMatchObject({ + kind: "runner", + name: "acpx_runtime", + version: "0.13.1", + capabilities: { + resume: false, + interruption: true, + dynamicTools: true, + }, + }); + }); + + it("requires an explicit runtime root and keeps other ACPX agents disabled", () => { + expect(() => createNativeSessionBackend(acpxExecution())).toThrow( + "requires an instance runtime directory", + ); + expect(() => + createNativeSessionBackend(acpxExecution("pi"), { + acpxRuntimeDirectory: "/runtime", + }), + ).toThrow("ACPX backend for pi is not included"); + }); + + it("rejects a Codex ACPX snapshot that drifts from its qualified profile", () => { + const input = acpxExecution(); + if (input.provider.kind !== "acpx") throw new Error("invalid fixture"); + input.provider.profile.commandDigest = `sha256:${"a".repeat(64)}`; + + expect(() => + createNativeSessionBackend(input, { + acpxRuntimeDirectory: "/runtime", + }), + ).toThrow("does not match the qualified commandDigest"); + }); + it("guards the provider-specific constructor as a second boundary", () => { expect(() => - createCodexNativeSessionBackend(execution({ - kind: "opencode", - model: "openrouter/model", - })), + 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 index 0ce8ebe458..8148034ade 100644 --- a/packages/paperclip-runner/src/backends/native-backend-factory.ts +++ b/packages/paperclip-runner/src/backends/native-backend-factory.ts @@ -8,12 +8,22 @@ import { createCodexNativeSessionBackend, type CodexNativeSessionBackendOptions, } from "./codex-native-backend.js"; +import { + createCodexAcpxNativeSessionBackend, + type CodexAcpxNativeSessionBackendOptions, +} from "./codex-acpx-native-backend.js"; -export interface NativeBackendFactoryOptions - extends Omit { +export interface NativeBackendFactoryOptions extends Omit< + CodexNativeSessionBackendOptions, + "transportFactory" +> { codexTransportFactory?: (context?: { providerRecoveryPolicy?: PersistedNativeSession["providerRecoveryPolicy"]; }) => CodexAppServerTransport; + acpxRuntimeDirectory?: string; + acpxEnvironment?: NodeJS.ProcessEnv; + acpxManagedCodexCredentialSourcePath?: string; + acpxDynamicToolHandler?: CodexAcpxNativeSessionBackendOptions["dynamicToolHandler"]; } /** @@ -25,6 +35,26 @@ export function createNativeSessionBackend( input: NativeExecutionInput, options: NativeBackendFactoryOptions = {}, ): NativeSessionBackend { + if (input.provider.kind === "acpx") { + if (input.provider.agent !== "codex") { + throw new Error( + `Native ACPX backend for ${input.provider.agent} is not included in the Codex-first runner`, + ); + } + if (!options.acpxRuntimeDirectory?.trim()) { + throw new Error( + "Codex ACPX backend requires an instance runtime directory", + ); + } + return createCodexAcpxNativeSessionBackend(input, { + runtimeDirectory: options.acpxRuntimeDirectory, + environment: options.acpxEnvironment, + managedCodexCredentialSourcePath: + options.acpxManagedCodexCredentialSourcePath, + dynamicTools: options.dynamicTools, + dynamicToolHandler: options.acpxDynamicToolHandler, + }); + } if (input.provider.kind !== "codex") { throw new Error( `Native backend for ${input.provider.kind} is not included in the Codex-first runner`,