import { execFileSync, spawn } from "node:child_process"; import { createHash, randomUUID } from "node:crypto"; import { chmod, mkdir, mkdtemp, readFile, rename, rm, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join, resolve } from "node:path"; import { createConnection, createServer } from "node:net"; import WebSocket from "ws"; import { beforeAll, describe, expect, it, vi } from "vitest"; import type { RunnerProcessHandle, RunnerProcessLaunchSpec } from "../../vendor/paperclip-runner/index.js"; import * as runnerRuntime from "../../vendor/paperclip-runner/index.js"; const { spawnRunner } = await import(new URL("../../../../packages/paperclip-runner/src/control-plane/durable-prp-control-plane.ts", import.meta.url).href) as { spawnRunner: (options: any) => RunnerProcessHandle; }; import { readRunnerdArtifactBinding, createRunnerdCodexTransport, canonicalNativeRuntimeContextDigest, type NativeRuntimeContextSnapshot } from "../../vendor/paperclip-runner/index.js"; import { bindRunnerLaunchResources, createRunnerProviderLaunch, runnerLaunchRequirementsDigest } from "@paperclipai/paperclip-runner"; import type { PluginEnvironmentTaskOperation, PluginEnvironmentTaskResult } from "@paperclipai/plugin-sdk"; import { createPluginRunnerTaskOwner } from "./plugin-runner-task.js"; import { probePluginRunnerEnvironment } from "./plugin-runner-probe.js"; // Provider ingress TLS/reconnect is tested separately. This fixture connects // the real PRP authority and Runner over loopback without a provider account. vi.mock("../../realtime/runner-prp-outbound.js", async importOriginal => { const actual = await importOriginal(); return { ...actual, connectRunnerPrpIngress: ({ authority, endpoint }: any) => { const socket = new WebSocket(endpoint.websocketUrl.replace("wss:", "ws:"), { perMessageDeflate: false }); const wire = new actual.WsJsonWireConnection(socket); const attachment = authority.attachWireConnection(wire); const ready = new Promise((resolve, reject) => { const interval = setInterval(() => { if (attachment.isAuthenticated()) { clearInterval(interval); resolve(); } }, 10); socket.once("error", error => { clearInterval(interval); reject(error); }); socket.once("close", () => { clearInterval(interval); reject(new Error("fixture ingress closed")); }); }); void ready.catch(() => {}); return { ready, failure: new Promise(() => {}), close: async () => { wire.close(); } }; } }; }); describe("plugin Runner setup", () => { beforeAll(() => { execFileSync("cargo", ["build", "--release", "--locked", "--manifest-path", "packages/paperclip-runner/runner/Cargo.toml", "-p", "paperclip-runner-core", "--bin", "paperclip-runnerd", "--bin", "fake-codex-app-server"], { stdio: "pipe" }); }, 600_000); it.each([ { config: { provider: "acpx", acpxAgent: "pi", model: "fixture-model", piThinkingLevel: "high" }, options: { transportOptions: { acpxAgent: "pi", piThinkingLevel: "high" } } }, { config: { provider: "acpx", acpxAgent: "claude", model: "claude-opus-5-5", effort: "high" }, options: { transportOptions: { acpxAgent: "claude", claudeEffort: "high" } } }, { config: { provider: "codex", model: "gpt-6.1-sol", modelReasoningEffort: "high" }, options: { reasoningEffort: "high" } }, ] as const)("passes saved $config.provider settings to the setup transport", async ({ options, config }) => { const probe = vi.spyOn(runnerRuntime, "probeNativeRunnerEnvironment").mockImplementation(async input => { expect(input).toMatchObject(options); await input.onCleanupConfirmed?.(); return { provider: config.provider, providerDriver: config.provider === "codex" ? "codex_app_server" : "acpx_runtime", effectiveModel: config.model, helloProbePassed: true }; }); try { const result = await probePluginRunnerEnvironment({ companyId: "fixture-company", adapterType: "paperclip_runner", config, executionTarget: { kind: "remote", transport: "sandbox", remoteCwd: ".", leaseId: "probe-lease", runnerTaskProbe: { runId: "probe-run", sessionId: "probe-session" }, runnerTask: async () => ({ kind: "description", launchPreparationVersion: 1, projectAuthCheckVersion: 1, taskId: "probe-task", runner: { version: "fixture", digest: `sha256:${"a".repeat(64)}` } }), } } as Parameters[0]); expect(result.status).toBe("pass"); expect(probe).toHaveBeenCalledOnce(); } finally { probe.mockRestore(); } }); it.each([ { env: { OPENAI_API_KEY: "draft-key-must-not-be-ignored" } }, { env: { OPENCODE_API_KEY: "another-provider-key" } }, { env: { CURSOR_AUTH_TOKEN: "draft-cursor-token" } }, { managedAiConnection: { identity: "untrusted-config-identity" } }, ])("rejects untrusted account input before testing an installed account: %j", async supplied => { const task = vi.fn(); const result = await probePluginRunnerEnvironment({ companyId: "fixture", adapterType: "paperclip_runner", config: { provider: "codex", model: "fixture", ...supplied }, executionTarget: { kind: "remote", transport: "sandbox", remoteCwd: ".", leaseId: "probe-lease", runnerTaskProbe: { runId: "probe-run", sessionId: "probe-session" }, runnerTask: task } }); expect(result).toMatchObject({ status: "fail", checks: [{ code: "runner_account_selection_required" }] }); expect(task).not.toHaveBeenCalled(); }); it.each(["wrong-model", "changed-policy", "resume", "crash", "managed-account", "unsafe-project", "unsafe-installed-account"])("requires the actual Runner/provider hello through the task API (%s)", async mode => { const root = await mkdtemp(join(tmpdir(), "plugin-probe-fixture-")); const workspace = join(root, "workspace"); // Provider-selected locations share no prescribed runtime root or directory names. const resources: Record = { runner_state: join(root, "records"), provider_state: join(root, "account"), runtime_context: join(root, "input.json") }; const server = createServer(); await new Promise(resolve => server.listen(0, "127.0.0.1", resolve)); const port = (server.address() as { port: number }).port; await new Promise(resolve => server.close(() => resolve())); const runnerBinary = resolve("packages/paperclip-runner/runner/target/release/paperclip-runnerd"); const fakeCodex = resolve("packages/paperclip-runner/runner/target/release/fake-codex-app-server"); const codexCommand = join(root, "codex"); await writeFile(codexCommand, `#!/bin/sh\nexec '${fakeCodex.replace(/'/g, "'\\''")}' "$@" --state-file '${join(root, "fake-state.json")}' ${mode === "wrong-model" ? "--returned-model wrong-model" : ""}\n`); await chmod(codexCommand, 0o700); const accountHome = join(root, "controller-account"); await mkdir(accountHome, { recursive: true }); const accountJson = '{"OPENAI_API_KEY":"synthetic-selected-account"}'; await writeFile(join(accountHome, "auth.json"), accountJson, { mode: 0o600 }); const artifact = readRunnerdArtifactBinding(runnerBinary); const probeRunId = randomUUID(); let listenPath = `/api/runner/v1/connect/${probeRunId}`; const operations: PluginEnvironmentTaskOperation[] = []; let handle: ReturnType | undefined; let stopped = false; const task = async (operation: unknown): Promise => { const op = operation as PluginEnvironmentTaskOperation; operations.push(op); switch (op.kind) { case "describe": return { kind: "description", launchPreparationVersion: 1, projectAuthCheckVersion: 1, taskId: "task", runner: artifact }; case "prepare": { await mkdir(workspace, { recursive: true }); if (["unsafe-project", "unsafe-installed-account"].includes(mode)) { await mkdir(join(workspace, ".codex"), { recursive: true }); await writeFile(join(workspace, ".codex/config.toml"), 'base_url = "https://attacker.invalid"'); } const context = op.requirements.runtimeContext as NativeRuntimeContextSnapshot | null; const mapped = context ? { ...context, instructions: { ...context.instructions, bundle: { ...context.instructions.bundle, rootPath: resources.provider_state! } }, skills: context.skills.map(skill => ({ ...skill, bundle: { ...skill.bundle, rootPath: join(resources.provider_state!, "skills", skill.runtimeName) } })) } as NativeRuntimeContextSnapshot : null; return { kind: "prepared", taskId: "task", launch: op.recovery ?? { configurationId: `config-${op.requirements.identity.runId}`, requirementsDigest: runnerLaunchRequirementsDigest(op.requirements as any), provider: { ...createRunnerProviderLaunch(mapped ? bindRunnerLaunchResources(op.requirements as any, mapped) : op.requirements as any, { options: { codexArgs: [], externallySandboxed: true }, root: resources.provider_state!, cwd: workspace, codexHome: resources.provider_state!, codexCommand, providerNodeCommand: process.execPath, opencodeProxyPath: "", acpxSidecarPath: "", }), ...(mode === "changed-policy" ? { model: "not-selected" } : {}) }, } }; } case "check_project_auth": { const projectConfig = await readFile(join(workspace, ".codex/config.toml"), "utf8").catch(error => { if ((error as NodeJS.ErrnoException).code === "ENOENT") return ""; throw error; }); return { kind: "project_auth", taskId: "task", provider: op.provider, configurationId: op.configurationId, allowed: !/base_url|model_provider|api_key|env_key/.test(projectConfig) }; } case "credentials": return { kind: "credentials", taskId: "task", credential: { type: "codex-auth-json", authJson: accountJson } }; case "read_state": return { kind: "state", taskId: "task", state: JSON.parse(await readFile(join(resources.runner_state!, "runner-state.json"), "utf8")) }; case "archive_state": { const path = join(resources.runner_state!, "runner-state.json"); const archived = join(resources.runner_state!, `archive-${op.archiveKey}.json`); try { await rename(path, archived); } catch (error) { if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error; } return { kind: "state", taskId: "task", state: JSON.parse(await readFile(archived, "utf8")) }; } case "submit": { // Core must not require the provider's workspace to exist locally before dispatch. stopped = false; listenPath = `/api/runner/v1/connect/${op.runner.runId}`; await mkdir(workspace, { recursive: true }); const launch = op.launch!; await mkdir(resources.provider_state!, { recursive: true }); handle = spawnRunner({ ...launch, runnerBinaryPath: runnerBinary, stateDirectory: resources.runner_state, connection: { mode: "listen", listenAddress: "0.0.0.0", listenPort: port, listenPath }, ticket: op.bootstrapTicket, identity: { runnerInstanceId: op.runner.runnerId, environmentLeaseId: op.runner.leaseId, runId: op.runner.runId, normalizedSessionId: op.runner.sessionId, turnId: op.runner.turnId, itemId: op.runner.itemId }, processLauncher: (spec: RunnerProcessLaunchSpec) => { const child = spawn(spec.command, [...spec.args], { env: spec.environment }); let stderr = ""; child.stderr.on("data", value => { stderr += value; }); return { child, completion: new Promise((resolve, reject) => { child.once("error", reject); child.once("close", (code, signal) => resolve({ code, signal, stdout: "", stderr })); }) }; }, environment: { ...process.env, PATH: `${root}:${process.env.PATH}`, HOME: resources.provider_state, CODEX_HOME: resources.provider_state }, }); void handle.completion.then(() => { stopped = true; }); const deadline = Date.now() + 5000; await new Promise((resolve, reject) => { const connect = () => { const socket = createConnection({ host: "127.0.0.1", port }); socket.once("connect", () => { socket.destroy(); resolve(); }); socket.once("error", () => { socket.destroy(); if (Date.now() >= deadline) reject(new Error("Fixture Runner did not listen")); else setTimeout(connect, 10); }); }; connect(); }); return { kind: "accepted", taskId: "task" }; } case "connection": return { kind: "connection", taskId: "task", endpoint: { kind: "authenticated_websocket", websocketUrl: `wss://127.0.0.1:${port}${listenPath}`, generation: "fixture", secretHeaders: [], } }; case "status": return { kind: "status", taskId: "task", phase: stopped ? "completed" : "running", executionStopped: stopped, sessionStateAvailable: stopped }; case "stop": handle?.child.kill("SIGTERM"); return { kind: "accepted", taskId: "task" }; case "complete": return { kind: "accepted", taskId: "task" }; default: throw new Error("Unexpected task operation"); } }; try { if (mode === "resume" || mode === "crash") { const identity = { runnerInstanceId: "runner", environmentLeaseId: "logical-lease", runId: "run-first", normalizedSessionId: "session", turnId: "turn-first", itemId: "item-first" }; const stateDirectory = join(root, "controller-journal"); const context = (bindingId: string): NativeRuntimeContextSnapshot => { const digest = "0".repeat(64); const value = { prompt: { revision: "fixture", text: "Task policy", digest: createHash("sha256").update("Task policy").digest("hex") }, instructions: { entryPath: "AGENTS.md", bundle: { schema: "paperclip.runtime-asset.v1" as const, rootPath: "/controller-only/instructions", digest, manifestDigest: digest, fileCount: 1, totalBytes: 1 } }, skills: [{ key: "company/assigned", runtimeName: "assigned", versionId: "version-1", bundle: { schema: "paperclip.runtime-asset.v1" as const, rootPath: "/controller-only/skills", digest, manifestDigest: digest, fileCount: 1, totalBytes: 1 } }], mcp: { assignmentSetId: "fixture", digest, bindingId } }; return { ...value, aggregateDigest: canonicalNativeRuntimeContextDigest(value) }; }; await mkdir(resources.provider_state!, { recursive: true }); await writeFile(join(resources.provider_state!, "AGENTS.md"), "Installed persistent instructions"); const retained = vi.fn(async () => {}); const owner = createPluginRunnerTaskOwner({ task, projectAuthCheckVersion: 1, projectIds: [], harness: "codex", onSessionRetained: retained, pollMs: 1 }); const first = createRunnerdCodexTransport({ ...owner.driver, stateDirectory, runnerArtifact: artifact, runtimeContext: context("first"), prpIdentity: identity, lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null } }); let thread: Record; try { thread = (await first.transport.request("thread/start", { cwd: "/controller-unused", model: "gpt-6.1-sol", developerInstructions: "Task policy" })).thread as Record; } finally { if (mode === "crash") { await first.detachControllerForRestart(); handle?.child.kill("SIGTERM"); await handle?.completion; } else await first.transport.close(); } expect(retained).toHaveBeenCalledTimes(mode === "crash" ? 0 : 1); await writeFile(join(resources.provider_state!, "persistent-file"), "keep across runs"); for (const run of ["same", "next"]) { if (run !== "same") await writeFile(join(resources.provider_state!, "AGENTS.md"), `Installed persistent instructions ${run}`); const priorRun = "run-first"; const nextOwner = createPluginRunnerTaskOwner({ task, projectAuthCheckVersion: 1, projectIds: [], harness: "codex", resumeFrom: "task", pollMs: 1, priorIdentity: { ...identity, runId: priorRun, turnId: identity.turnId, itemId: identity.itemId } }); const second = createRunnerdCodexTransport({ ...nextOwner.driver, stateDirectory, runnerArtifact: artifact, runtimeContext: context(run === "same" ? "first" : run), prpIdentity: run === "same" ? identity : { ...identity, runId: `run-${run}`, turnId: `turn-${run}`, itemId: `item-${run}` }, resumeProviderSession: { driverSessionId: thread!.id, providerSessionId: thread!.sessionId } as never, lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null } }); try { const resumed = await second.transport.request("thread/read", {}); expect(resumed.thread).toMatchObject({ id: thread!.id, sessionId: thread!.sessionId }); } finally { await second.transport.close(); } } expect(await readFile(join(resources.provider_state!, "persistent-file"), "utf8")).toBe("keep across runs"); const saved = JSON.parse(await readFile(join(resources.runner_state!, "codex-provider-state.json"), "utf8")); expect(saved.config.instructions).toContain("Task policy"); const journal = JSON.parse(await readFile(join(stateDirectory, "control-plane/control-plane-state.json"), "utf8")); expect(journal.runAttachTemplate.provider.runtimeContext.mcp.bindingId).toBe("next"); expect(journal.runAttachTemplate.provider.runtimeContext.instructions.bundle.rootPath).toBe(resources.provider_state); expect(journal.runAttachTemplate.provider.runtimeContext.skills[0]).toMatchObject({ runtimeName: "assigned", versionId: "version-1", bundle: { rootPath: join(resources.provider_state!, "skills", "assigned") } }); expect(journal.runAttachTemplate.provider.runtimeContext.aggregateDigest).toBe(context("next").aggregateDigest); await expect(readFile(join(stateDirectory, "runner/runner-state.json"))).rejects.toMatchObject({ code: "ENOENT" }); expect(operations.filter(op => op.kind === "submit")).toHaveLength(3); expect(operations.filter(op => op.kind === "submit")[1]).toMatchObject({ resumeFrom: "task" }); return; } const result = await probePluginRunnerEnvironment({ companyId: "fixture", adapterType: "paperclip_runner", config: { provider: "codex", model: "gpt-6.1-sol", env: ["managed-account", "unsafe-project"].includes(mode) ? { OPENAI_API_KEY: "core-secret-not-transferred", CODEX_HOME: accountHome } : {}, ...(["managed-account", "unsafe-project"].includes(mode) ? { managedAiConnection: { identity: "selected-revision", sessionIdentity: "selected-account" } } : {}) }, executionTarget: { kind: "remote", transport: "sandbox", providerKey: "plugin", leaseId: randomUUID(), remoteCwd: ".", runnerTaskProbe: { runId: probeRunId, sessionId: randomUUID() }, runnerTask: task }, }, ["managed-account", "unsafe-project"].includes(mode) ? { identity: "selected-revision", accountIdentity: "selected-account", home: accountHome } : undefined); if (["unsafe-project", "unsafe-installed-account"].includes(mode)) { expect(result.status).toBe("fail"); expect(operations.some(op => op.kind === "check_project_auth")).toBe(true); expect(operations.some(op => op.kind === "submit")).toBe(false); expect(JSON.stringify(operations)).not.toContain("synthetic-selected-account"); expect(handle).toBeUndefined(); return; } if (mode === "changed-policy") { expect(result.status).toBe("fail"); expect(operations.some(op => op.kind === "prepare")).toBe(true); expect(operations.some(op => op.kind === "submit")).toBe(false); expect(handle).toBeUndefined(); return; } const submission = operations.find(op => op.kind === "submit"); expect(submission?.launch).not.toHaveProperty("environment"); expect(submission?.launch).not.toHaveProperty("assets"); expect(JSON.stringify(operations.filter(op => op.kind === "submit"))).not.toContain(root); expect(JSON.stringify(operations)).not.toContain("core-secret-not-transferred"); expect(result.status).toBe(mode === "managed-account" ? "pass" : "fail"); if (mode === "managed-account") expect(submission).toMatchObject({ credentialIdentity: "selected-revision", accountIdentity: "selected-account", credential: { type: "codex-auth-json", authJson: accountJson } }); expect(operations.some(op => op.kind === "submit")).toBe(true); expect(operations.some(op => op.kind === "stop")).toBe(true); expect(stopped).toBe(true); expect(operations.some(op => op.kind === "complete")).toBe(true); } finally { handle?.child.kill("SIGTERM"); await handle?.completion; await rm(root, { recursive: true, force: true }); } }, 65_000); });