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
This commit is contained in:
Dotta 2026-08-30 01:49:41 -05:00 committed by GitHub
parent 06f8773097
commit 6d358e646d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 224 additions and 0 deletions

View File

@ -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<void>;
transportFactory?: (context?: {
providerRecoveryPolicy?: PersistedNativeSession["providerRecoveryPolicy"];
}) => CodexAppServerTransport;
dynamicTools?: readonly Readonly<Record<string, unknown>>[];
dynamicToolHandler?: (call: {
tool: string;
callId: string;
threadId: string;
turnId: string;
arguments: unknown;
}) => Promise<unknown>;
}
/**
* 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,
}));
}

View File

@ -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");
});
});

View File

@ -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<CodexNativeSessionBackendOptions, "transportFactory"> {
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,
});
}

View File

@ -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,