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,