Files
PaperClipAI/server/src/services/native-runtime/plugin-runner-task.test.ts
T
Nicky LeachandPaperclip a3e8fa568a Reduce repeated work during plugin Runner admission (#17377)
## Thinking Path

> - Paperclip manages agents and their execution authority.
> - A plugin can host Runner tasks on a persistent execution host.
> - Admission checks must observe current lifecycle state and
credentials.
> - The current runtime reader also resolves files, skills, and
connections for each check.
> - Some providers also repeat the same project authentication check
during submit.
> - This pull request adds a compact fresh reader and an explicit
submit-time validation contract.
> - Providers can remove repeated work while Core keeps lease and scope
checks.

## Linked Issues or Issue Description

**What existing behavior does this improve?**

Plugin Runner startup before task submission.

**Current behavior**

One measured startup made 30 full runtime reads during bootstrap. These
reads took 5.759 seconds. Each guard can resolve instructions, skills,
and connection assignments even when it only needs current identity and
lifecycle state. Providers that already enforce project authentication
in submit must also perform a separate Core preflight.

**Proposed behavior**

Use a lifecycle-owned runtime identity reader for guards. Keep the full
runtime reader for synchronization inputs. A trusted provider can
declare `projectAuthCheckAtSubmit: true` in its prepared result. Core
pins the exact configuration, provider, and project set on the lease.
The provider must reject authentication conflicts before credential
installation or task start. Providers that omit the flag retain the
existing preflight.

**Reason and benefit**

Estimated savings with a paired consumer: 4–5 seconds from compact
runtime checks, plus roughly 0.5–0.9 seconds from removing the duplicate
authentication operation. These estimates overlap and are not a measured
deployed result. Refs #17367 for the separate run-log HEAD optimization;
this PR does not duplicate it.

**Breaking changes**

No migration. The SDK adds a host method and an optional prepared-result
field. Consumers that use them require this Core host version. Older
providers retain their existing flow.

## What Changed

- Add `agents.getRuntimeIdentity` across SDK, RPC, host service, and
test fixtures.
- Reuse the existing company, plugin ownership, readiness, and
capability checks. Read lifecycle state and protected instance secrets
fresh on every call.
- Skip instruction, organization, skill, and connection hydration in the
compact reader.
- Allow explicit submit-time authentication enforcement, with prepared
scope pinned and checked again at dispatch.
- Emit fixed preparation, project-auth, submit, and connection timing
phases.

## Verification

- 72 targeted SDK capability, plugin task-owner, and environment-task
mediator tests passed.
- 33 real embedded-Postgres lifecycle and secret tests passed, including
pause, content revision, company isolation, plugin disablement, and
secret rotation.
- SDK and server TypeScript checks passed. Local Runner/eval TypeScript
outputs were built to satisfy server type resolution.
- Local Postgres required missing library aliases in ignored
dependencies; no product source was changed for that repair.
- Full repository suites are delegated to GitHub CI. No live rollout or
speedup claim.

## Risks

The submit-time flag delegates enforcement to a trusted provider. It is
not proof that prepare already checked authentication. The provider must
check before VM credential installation or task admission. Core still
checks exact prepared scope, active lease, run identity, credential
type, and dispatch fencing. Existing providers keep the separate check.
Roll back a paired consumer before rolling back this host.

## Model Used

OpenAI GPT-6 (Codex), with code execution and repository tools. Exact
deployment build and context-window size are not exposed.

## 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 described the issue in-PR following the relevant issue
template
- [x] I have not referenced internal/instance-local Paperclip issues or
links
- [x] My branch name describes the change and contains no internal
ticket id
- [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
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-10-23 14:54:25 -07:00

325 lines
20 KiB
TypeScript

import { createHash } from "node:crypto";
import { describe, expect, it, vi } from "vitest";
import type { RunnerProviderLaunchRequirements } from "@paperclipai/paperclip-runner";
import type { RunnerProcessLaunchSpec } from "../../vendor/paperclip-runner/index.js";
import { createPluginRunnerTaskOwner } from "./plugin-runner-task.js";
const ingress = vi.hoisted(() => ({ connect: vi.fn() }));
vi.mock("../../realtime/runner-prp-outbound.js", () => ({ connectRunnerPrpIngress: ingress.connect }));
function launchSpec(): RunnerProcessLaunchSpec {
return { command: "unused", args: [], cwd: "/unused", environment: {}, nativeRunner: {
identity: { runnerInstanceId: "runner", environmentLeaseId: "lease", runId: "run",
normalizedSessionId: "session", turnId: "turn", itemId: "item" }, ticket: "synthetic-ticket",
diagnosticsDirectory: "/controller/private/diagnostics",
stateDirectory: "/state", runnerVersion: "fixture", runnerDigest: `sha256:${"a".repeat(64)}`,
maxOutboxBytes: 262144, p0ReserveBytes: 65536, lifecyclePolicy: { mode: "per_turn", idleTimeoutMs: null },
} };
}
const requirements: RunnerProviderLaunchRequirements = {
schema: "paperclip.runner.launch-requirements.v1", identity: launchSpec().nativeRunner!.identity,
provider: "codex", model: "fixture", instructions: "Core policy", runtimeContext: null,
settings: {}, approvalPolicy: "never", readOnly: false, preparedConversation: false,
includeCollaborationModeInstructions: false,
};
async function prepare(owner: ReturnType<typeof createPluginRunnerTaskOwner>) {
await owner.driver.prepareLaunch!(requirements);
}
function provider() {
let stopped = false;
const task = vi.fn(async (operation: any): Promise<unknown> => {
if (operation.kind === "check_project_auth") return { kind: "project_auth", taskId: "task", allowed: true,
provider: operation.provider, configurationId: operation.configurationId };
if (operation.kind === "prepare") return { kind: "prepared", taskId: "task", launch: {
configurationId: "configuration", requirementsDigest: `sha256:${"a".repeat(64)}`, provider: { kind: "codex" },
} };
if (operation.kind === "read_state") return { kind: "state", taskId: "task", state: {
schema: "paperclip.runner.durable.state.v1", ...operation.identity, lifecycle: "suspended",
} };
if (operation.kind === "stop") stopped = true;
if (operation.kind === "status") return { kind: "status", taskId: "task", phase: stopped ? "cancelled" : "running", executionStopped: stopped, sessionStateAvailable: stopped };
if (operation.kind === "connection") return { kind: "connection", taskId: "task", endpoint: {
kind: "authenticated_websocket", websocketUrl: "wss://fixture.example.test/connect", generation: "fixture", secretHeaders: [],
} };
return { kind: "accepted", taskId: "task" };
});
return { task };
}
describe("plugin native Runner ownership", () => {
it("submits only a prepared identity and configuration ID", async () => {
const { task } = provider();
const owner = createPluginRunnerTaskOwner({ projectAuthCheckVersion: 1, task, projectIds: [], harness: "codex", pollMs: 1 });
expect(() => owner.launcher(launchSpec())).toThrow("requires preparation");
await prepare(owner);
expect(task.mock.calls[0][0]).toEqual({ kind: "prepare", requirements, projectIds: [] });
expect(task.mock.calls[1][0]).toMatchObject({ kind: "check_project_auth", configurationId: "configuration", provider: "openai" });
expect(() => owner.launcher({ ...launchSpec(), nativeRunner: { ...launchSpec().nativeRunner!,
identity: { ...requirements.identity, runId: "other" } } })).toThrow("structured per-turn");
ingress.connect.mockReturnValue({ ready: Promise.resolve(), close: async () => {} });
const registration = await owner.registration({} as never, "run");
const handle = owner.launcher(launchSpec());
registration.activate(); await registration.ready();
await registration.release(); await registration.checkpoint("unsettled"); await handle.completion;
expect(task.mock.calls.find(([op]) => op.kind === "submit")?.[0]).toMatchObject({ preparedLaunchId: "configuration" });
});
it("omits duplicate project validation only when the provider enforces it at submission", async () => {
const { task } = provider();
const perform = task.getMockImplementation()!;
task.mockImplementation(async op => op.kind === "prepare"
? { ...await perform(op) as object, projectAuthCheckAtSubmit: true } : perform(op));
const owner = createPluginRunnerTaskOwner({ projectAuthCheckVersion: 1,
task, projectIds: [], harness: "codex", pollMs: 1 });
await prepare(owner);
expect(task.mock.calls.map(([op]) => op.kind)).toEqual(["prepare"]);
ingress.connect.mockReturnValue({ ready: Promise.resolve(), close: async () => {} });
const registration = await owner.registration({} as never, "run");
const handle = owner.launcher(launchSpec());
registration.activate(); await registration.ready();
await registration.release(); await registration.checkpoint("unsettled"); await handle.completion;
expect(task.mock.calls.filter(([op]) => op.kind === "submit")).toHaveLength(1);
const unsupported = createPluginRunnerTaskOwner({ task, projectIds: [], harness: "codex" });
await expect(prepare(unsupported)).rejects.toThrow("validation unavailable");
});
it("does not accept a late preparation after task release", async () => {
let finish!: () => void;
const gate = new Promise<void>(resolve => { finish = resolve; });
const { task } = provider(); const perform = task.getMockImplementation()!;
task.mockImplementation(async op => { if (op.kind === "prepare") await gate; return perform(op); });
const owner = createPluginRunnerTaskOwner({ projectAuthCheckVersion: 1, task, projectIds: [], harness: "codex" });
const prepared = prepare(owner);
const registration = await owner.registration({} as never, "run");
await registration.release(); finish();
await expect(prepared).rejects.toThrow("preparation cancelled");
expect(task.mock.calls.some(([op]) => op.kind === "submit")).toBe(false);
});
it.each(["unsupported", "denied", "wrong-configuration", "wrong-provider"])("withholds credentials when project validation is %s", async mode => {
const { task } = provider(); const perform = task.getMockImplementation()!;
task.mockImplementation(async op => op.kind === "check_project_auth"
? { kind: "project_auth", taskId: "task", allowed: mode !== "denied",
configurationId: mode === "wrong-configuration" ? "other" : op.configurationId,
provider: mode === "wrong-provider" ? "anthropic" : op.provider } : perform(op));
const owner = createPluginRunnerTaskOwner({ task, projectIds: [], harness: "codex",
projectAuthCheckVersion: mode === "unsupported" ? undefined : 1,
credentialIdentity: "revision", accountIdentity: "account", credential: { type: "codex-auth-json", authJson: '{"OPENAI_API_KEY":"never-transfer"}' } });
await expect(prepare(owner)).rejects.toThrow(mode === "unsupported" ? "validation unavailable" : "conflicts");
expect(() => owner.launcher(launchSpec())).toThrow("requires preparation");
expect(JSON.stringify(task.mock.calls)).not.toContain("never-transfer");
expect(task.mock.calls.some(([op]) => op.kind === "submit")).toBe(false);
});
it("rejects an installed account when the plugin lacks project validation", async () => {
const { task } = provider();
const owner = createPluginRunnerTaskOwner({ task, projectIds: [], harness: "codex" });
await expect(prepare(owner)).rejects.toThrow("validation unavailable");
expect(task.mock.calls.some(([op]) => op.kind === "submit")).toBe(false);
});
it("collects refreshed credentials after process termination and before releasing task grants", async () => {
const { task } = provider();
const perform = task.getMockImplementation()!;
const credential = { type: "codex-auth-json" as const, authJson: '{"OPENAI_API_KEY":"fixture-key"}' };
let copied = false;
task.mockImplementation(async operation => {
if (operation.kind === "credentials") {
expect(task.mock.calls.some(([op]) => op.kind === "stop")).toBe(true);
return { kind: "credentials", taskId: "task", credential };
}
if (operation.kind === "complete") expect(copied).toBe(true);
return perform(operation);
});
ingress.connect.mockReturnValue({ ready: Promise.resolve(), close: async () => {} });
const owner = createPluginRunnerTaskOwner({ projectAuthCheckVersion: 1, task, projectIds: [], harness: "codex", credential,
credentialIdentity: "revision", accountIdentity: "stable-account", pollMs: 1,
onCredentials: async value => { expect(value).toEqual(credential); copied = true; } });
await prepare(owner);
const registration = await owner.registration({} as never, "run");
const process = owner.launcher(launchSpec()); registration.activate(); await registration.ready();
expect(task.mock.calls.find(([op]) => op.kind === "submit")?.[0]).toMatchObject({ credential, credentialIdentity: "revision", accountIdentity: "stable-account" });
await registration.release(); await registration.checkpoint("settled"); await process.completion;
expect(copied).toBe(true);
});
it("retains grants when the credential handoff fails", async () => {
const { task } = provider();
const perform = task.getMockImplementation()!;
task.mockImplementation(async op => op.kind === "credentials" ? { kind: "credentials", taskId: "task", credential: { type: "codex-auth-json", authJson: "{}" } } : perform(op));
ingress.connect.mockReturnValue({ ready: Promise.resolve(), close: async () => {} });
const owner = createPluginRunnerTaskOwner({ projectAuthCheckVersion: 1, task, projectIds: [], harness: "codex", accountIdentity: "account", pollMs: 1,
onCredentials: async () => { throw new Error("credential handoff failed"); } });
await prepare(owner);
const registration = await owner.registration({} as never, "run");
const process = owner.launcher(launchSpec()); registration.activate(); await registration.ready(); await registration.release();
await expect(registration.checkpoint("settled")).rejects.toThrow("credential handoff failed");
await process.completion;
expect(task.mock.calls.some(([op]) => op.kind === "complete")).toBe(false);
});
it.each([false, true])("records retained sessions after status failures (retry exhausted: %s)", async exhausted => {
const { task } = provider();
const perform = task.getMockImplementation()!;
let failStatus = false;
let clock = Date.now();
const now = vi.spyOn(Date, "now").mockImplementation(() => clock);
task.mockImplementation(async operation => {
if (operation.kind === "stop") failStatus = false;
if (operation.kind === "status" && failStatus) {
failStatus = exhausted;
if (exhausted) clock += 16_000;
throw new Error("status connection lost");
}
return perform(operation);
});
ingress.connect.mockReturnValue({ ready: Promise.resolve(), close: async () => {} });
const checkpoint = vi.fn(async () => {});
const owner = createPluginRunnerTaskOwner({ projectAuthCheckVersion: 1, task, projectIds: [], harness: "codex", onSessionRetained: checkpoint, pollMs: 1,
});
await prepare(owner);
const registration = await owner.registration({} as never, "run");
const handle = owner.launcher(launchSpec());
const completed = vi.fn(); void handle.completion.then(completed);
try {
registration.activate(); await registration.ready();
expect(handle.restart).toBeNull();
failStatus = true;
if (exhausted) await expect(registration.failure).rejects.toThrow("reconcile the saved task");
else await vi.waitFor(() => expect(failStatus).toBe(false));
expect(completed).not.toHaveBeenCalled();
await registration.release(); await registration.checkpoint("settled");
await expect(handle.completion).resolves.toMatchObject({ code: 1 });
expect(checkpoint).toHaveBeenCalledOnce();
expect(task.mock.calls.filter(([op]) => op.kind === "submit")).toHaveLength(1);
expect(task.mock.calls.filter(([op]) => op.kind === "complete")).toHaveLength(1);
} finally { now.mockRestore(); handle.child.kill(); }
});
it("exposes connection failure before activation and forwards late recovery exhaustion", async () => {
const { task } = provider();
let fail!: (error: Error) => void;
const failure = new Promise<never>((_resolve, reject) => { fail = reject; });
ingress.connect.mockReturnValue({ ready: Promise.resolve(), failure, close: async () => {} });
const owner = createPluginRunnerTaskOwner({ projectAuthCheckVersion: 1, task, projectIds: [], harness: "codex", pollMs: 1 });
await prepare(owner);
const registration = await owner.registration({} as never, "run");
const observed = registration.failure.catch(error => error);
const handle = owner.launcher(launchSpec());
registration.activate(); await registration.ready();
const error = new Error("runner ingress recovery exhausted"); fail(error);
expect(await observed).toBe(error);
expect(task.mock.calls.find(([op]) => op.kind === "stop")).toBeUndefined();
await registration.release(); await registration.checkpoint("settled"); await handle.completion;
});
it("reconciles a lost submit reply without launching another attempt", async () => {
const { task } = provider();
const perform = task.getMockImplementation()!;
let lost = true, statusLost = true;
task.mockImplementation(async op => {
if (op.kind === "submit" && lost) { lost = false; throw new Error("lost submit reply"); }
if (op.kind === "status" && statusLost) { statusLost = false; throw new Error("status temporarily unavailable"); }
return perform(op);
});
ingress.connect.mockReturnValue({ ready: Promise.resolve(), close: async () => {} });
const owner = createPluginRunnerTaskOwner({ projectAuthCheckVersion: 1, task, projectIds: [], harness: "codex",
pollMs: 1 });
await prepare(owner);
const registration = await owner.registration({} as never, "run");
const process = owner.launcher(launchSpec());
try {
registration.activate(); await registration.ready();
expect(task.mock.calls.filter(([operation]) => operation.kind === "submit")).toHaveLength(1);
expect(task.mock.calls.find(([operation]) => operation.kind === "submit")?.[0].launch).not.toHaveProperty("diagnosticsDirectory");
expect(() => owner.launcher(launchSpec())).toThrow("new task lease");
await registration.release(); await registration.checkpoint("settled"); await process.completion;
expect(task.mock.calls.filter(([operation]) => operation.kind === "complete")).toHaveLength(1);
} finally { process.child.kill(); await process.completion.catch(() => {}); }
});
it("retains task grants when retention recording fails after route release", async () => {
const { task } = provider();
ingress.connect.mockReturnValue({ ready: Promise.resolve(), close: async () => {} });
const owner = createPluginRunnerTaskOwner({ projectAuthCheckVersion: 1, task, projectIds: [], harness: "codex", onSessionRetained: async () => { throw new Error("retention write failed"); }, pollMs: 1 });
await prepare(owner);
const registration = await owner.registration({} as never, "run");
const process = owner.launcher(launchSpec());
registration.activate(); await registration.ready(); await registration.release();
expect(task.mock.calls.some(([operation]) => operation.kind === "complete")).toBe(false);
await expect(registration.checkpoint("settled")).rejects.toThrow("retention write failed");
await process.completion;
expect(task.mock.calls.some(([operation]) => operation.kind === "complete")).toBe(false);
});
it("detaches a controller without stopping its durable task", async () => {
const { task } = provider();
const close = vi.fn(async () => {});
ingress.connect.mockReturnValue({ ready: Promise.resolve(), close });
const owner = createPluginRunnerTaskOwner({ projectAuthCheckVersion: 1, task, projectIds: [], harness: "codex",
pollMs: 1 });
await prepare(owner);
const registration = await owner.registration({} as never, "run");
owner.launcher(launchSpec());
registration.activate(); await registration.ready(); await registration.release("detach");
expect(close).toHaveBeenCalledOnce();
expect(task.mock.calls.some(([operation]) => operation.kind === "stop" || operation.kind === "complete")).toBe(false);
});
it("prevents a late submission after cancellation during dispatch recording", async () => {
let finish!: () => void;
const prepared = new Promise<void>(resolve => { finish = resolve; });
const { task } = provider();
const owner = createPluginRunnerTaskOwner({ projectAuthCheckVersion: 1, task, projectIds: [], harness: "codex",
onDispatched: () => prepared, pollMs: 1 });
await prepare(owner);
const registration = await owner.registration({} as never, "run");
const process = owner.launcher(launchSpec());
registration.activate();
process.child.kill(); await registration.release(); finish();
await expect(process.completion).resolves.toMatchObject({ code: 1 });
await expect(registration.ready()).rejects.toThrow("cancelled");
expect(task.mock.calls.some(([op]) => op.kind === "submit")).toBe(false);
});
it("does not connect a released or aborted startup to the PRP authority", async () => {
const { task } = provider();
ingress.connect.mockClear();
const controller = new AbortController(); controller.abort();
const owner = createPluginRunnerTaskOwner({ projectAuthCheckVersion: 1, task, projectIds: [], harness: "codex",
pollMs: 1 });
await prepare(owner);
const registration = await owner.registration({} as never, "run", controller.signal);
const process = owner.launcher(launchSpec());
try {
registration.activate();
await expect(registration.ready()).rejects.toThrow("cancelled");
expect(task.mock.calls.filter(([operation]) => operation.kind === "connection")).toHaveLength(0);
expect(ingress.connect).not.toHaveBeenCalled();
await registration.release(); await registration.checkpoint("settled"); await process.completion;
} finally { process.child.kill(); await process.completion.catch(() => {}); }
});
});
describe("plugin task workspace file transfer", () => {
it("reads only a submitted task, validates bytes, and rejects late responses after release", async () => {
const {task}=provider(); const perform=task.getMockImplementation()!;
const bytes=Buffer.from("VM proof\n");
let file={kind:"file",taskId:"task",base64:bytes.toString("base64"),byteSize:bytes.length,sha256:createHash("sha256").update(bytes).digest("hex")};
let afterRead=async()=>{};
task.mockImplementation(async op=>{if(op.kind==="read_file"){await afterRead();return file;}return perform(op);});
const owner=createPluginRunnerTaskOwner({projectAuthCheckVersion:1,task,projectIds:[],harness:"codex",pollMs:1});
await expect(owner.readWorkspaceFile("proof.txt",1024)).rejects.toThrow("unavailable");
await prepare(owner);
ingress.connect.mockReturnValue({ready:Promise.resolve(),close:async()=>{}});
const registration=await owner.registration({} as never,"run");
const handle=owner.launcher(launchSpec());registration.activate();await registration.ready();
try {
await expect(owner.readWorkspaceFile("proof.txt",1024)).resolves.toEqual(bytes);
await expect(owner.readWorkspaceFile("/home/agent/proof.txt",1024)).resolves.toEqual(bytes);
expect(task.mock.calls.find(([op])=>op.kind==="read_file")?.[0]).toMatchObject({identity:requirements.identity,path:"proof.txt",projectIds:[]});
for(const path of ["../escape","//etc/passwd","file:/secret","a/../b","a\\b"]) await expect(owner.readWorkspaceFile(path,1024)).rejects.toThrow();
await expect(owner.readWorkspaceFile("proof.txt",1)).rejects.toThrow("unavailable");
file={...file,sha256:"f".repeat(64)};
await expect(owner.readWorkspaceFile("proof.txt",1024)).rejects.toThrow("integrity");
afterRead=()=>registration.release();
await expect(owner.readWorkspaceFile("proof.txt",1024)).rejects.toThrow("unavailable");
} finally {await registration.release();await registration.checkpoint("unsettled");await handle.completion;}
});
});