mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-28 12:04:41 +01:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - Native Runner starts provider runtimes in remote execution environments. > - Sandbox images already contain qualified provider packs. > - Startup required the image pack to match the entire current app release manifest. > - An unrelated app commit could therefore cause pack verification, transfer, and installation again. > - This pull request selects protected packs by their provider interface and platform. > - The benefit is no extra pack probe, directory scan, version command, link, or upload on the compatible image path. ## Linked Issues or Issue Description **What happened?** Starting a remote task can reject a compatible image pack because its source revision or unrelated provider differs from the controller. Pack replacement added about 16 seconds in the investigated startup. Startup also repeated full content checks. **Expected behavior** Use an already qualified, protected pack when its selected provider interface and target platform are compatible. Keep the selected artifact identity for process launch and recovery. **Steps to reproduce** 1. Install a qualified provider pack in a sandbox image. 2. Start a controller built from another app commit. 3. Start a remote OpenCode task. The old complete-manifest comparison rejects the image pack even when the provider interface still works. Related repairs: #16493 reduced transfer cost. #17512 retained exact artifact authority during reattachment. This change preserves that recovery rule and changes fresh-launch pack selection. ## What Changed - Add explicit OpenCode and ACPX interface revisions to produced pack manifests. App Git SHAs and dependency pins remain provenance. - Read bounded protected-installation metadata in the existing final runner metadata query. Accept only root-owned paths that the execution user cannot write, including ancestors and manifest. Reject writable aliases. - Select and prepare the pack in server-owned environment setup before constructing the existing transport. Bind provider launch paths and digests from the selected installation. Save that identity for exact reattachment. - Keep full content verification for uploads, writable installations, and legacy packs. Reuse successful verification within one startup. Keep older attached runners compatible with the optional metadata request. - Seal image packs and document the one-time image refresh that enables reuse across later compatible releases. Keep selected ACPX agent qualification checks. ## Verification - Red/green regression: the old code rejects a protected pack from another app revision with `manifest mismatch`; the fixed path selects it without executing verification commands. - Native startup regression: select the installed pack's own Node digest with one existing runner metadata request and no pack verification or version commands. - Targeted pack, artifact, recovery, execution-driver, and Rust metadata tests pass. Incompatible interfaces/platforms, malformed identities, writable roots, path aliases, and exact reattachment are covered. - Final revision `91dc1676ff67bdbe5866c8586daac10240530cca`: workspace typecheck and build pass. All 53 current-head checks are successful, with no pending or failed checks. Greptile is 5/5 with no open findings. - The broad local `pnpm test:run` attempt reported a workspace-runtime conflict test failure; its isolated rerun passed. The broad run was stopped after that failure. Full CI now passes, including the server test shards. This is not a claim that the full local run passed. - Linux filesystem evidence: the production metadata functions read a real root-owned preinstalled pack as a non-root user in 16 microseconds median and 20 microseconds p95 across 100 warm reads. This measures metadata admission, not model or hosted response latency. - All 910 affected server tests and 57 Copilot attestation tests pass locally on the final implementation. Full CI covers the unchanged Runner transport. No provider qualification hash was refreshed. - Built the actual Linux daemon at `416e7ec7425d0d3565b4a5e1808002d650a60414` and ran its metadata command as UID 65534. The protected installation returned its real manifest with `immutable: true`; writable alias and missing-pack cases returned `immutable: false`. This is metadata/permission evidence, not provider execution. - Hosted rollout and a hosted end-to-end latency measurement have not run. ## Risks - Existing images need one rebuild to publish the compatibility declaration. Packs without it keep the legacy content-verification path. - Fast admission relies on image publication producing a qualified protected tree. Root execution and task-writable installations retain full verification. - Bump an interface revision when required runner/pack behavior changes incompatibly. Deploy a new image to roll out compatible runtime fixes or upgrades. - This removes provider-pack preparation delay. Other runner startup, context, model, and finalization costs remain separate. ## Model Used OpenAI Codex, configured model `gpt-6.1-sol`, reasoning effort `xhigh`, with tool use and code execution. The context-window size is not exposed in this session. ## 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 - [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>
193 lines
10 KiB
TypeScript
193 lines
10 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
import type { AdapterExecutionTarget } from "@paperclipai/adapter-utils/execution-target";
|
|
import type { RunnerIngressEndpoint } from "@paperclipai/adapter-utils/runner-connectivity";
|
|
import { prepareNativeRemoteRunner, createNativeRunnerExecutionDriver } from "./native-runner-execution-driver.js";
|
|
|
|
const transport = vi.hoisted(() => ({
|
|
register: vi.fn(),
|
|
connect: vi.fn(),
|
|
}));
|
|
vi.mock("../../realtime/runner-prp-ws.js", () => ({ registerRunnerPrpAuthority: transport.register }));
|
|
vi.mock("../../realtime/runner-prp-outbound.js", () => ({ connectRunnerPrpIngress: transport.connect }));
|
|
|
|
const binding = { companyId: "company", issueId: "issue", agentId: "agent", runId: "run-a" };
|
|
const authority = {} as Parameters<NonNullable<ReturnType<typeof createNativeRunnerExecutionDriver>["controlPlaneRegistration"]>>[0];
|
|
const ssh = { kind: "remote", transport: "ssh", remoteCwd: "/workspace", spec: {} } as AdapterExecutionTarget;
|
|
const sandbox = (ingress = true): AdapterExecutionTarget => ({
|
|
kind: "remote", transport: "sandbox", remoteCwd: "/workspace", leaseId: "lease",
|
|
effectiveCapabilities: { runnerWebSocketIngress: ingress },
|
|
} as AdapterExecutionTarget);
|
|
|
|
function endpoint(): RunnerIngressEndpoint {
|
|
return {
|
|
kind: "authenticated_websocket", websocketUrl: "wss://sandbox.example/runner",
|
|
secretHeaders: [{ name: "Authorization", value: "test-ingress-secret" }], generation: "1",
|
|
refresh: vi.fn(), close: vi.fn(async () => {}),
|
|
};
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
transport.register.mockResolvedValue({ connectUrl: "ws://127.0.0.1/core", release: vi.fn() });
|
|
transport.connect.mockReturnValue({ ready: Promise.resolve(), close: vi.fn(async () => {}) });
|
|
});
|
|
|
|
describe("shared Runner execution driver", () => {
|
|
it("keeps local execution on the registered loopback route", async () => {
|
|
const driver = createNativeRunnerExecutionDriver({ target: { kind: "local" }, binding });
|
|
const connection = await driver.controlPlaneRegistration!(authority);
|
|
expect(connection.connectUrl).toBe("ws://127.0.0.1/core");
|
|
expect(connection.startupFailureCode).toBe("runner_local_connect_failed");
|
|
expect(transport.register).toHaveBeenCalledWith({ ...binding, authority });
|
|
expect(transport.connect).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it.each([ssh, sandbox(false)])("prepares $transport before launching an outbound Runner", async target => {
|
|
const prepare = vi.fn(async () => {});
|
|
const launch = vi.fn();
|
|
const read = vi.fn();
|
|
const checkpoint = vi.fn();
|
|
const mapCaBundle = vi.fn(async () => "/remote/ca.pem");
|
|
const driver = createNativeRunnerExecutionDriver({
|
|
target, binding, runnerPublicUrl: "wss://core.example", prepareRemote: prepare,
|
|
process: { runnerProcessLauncher: launch, readRunnerState: read }, checkpoint, mapCaBundle,
|
|
});
|
|
const connection = await driver.controlPlaneRegistration!(authority);
|
|
expect(prepare).toHaveBeenCalledWith("dial_wss");
|
|
expect(connection.connection).toEqual({ mode: "connect", connectUrl: "wss://core.example/api/runner/v1/connect/run-a" });
|
|
expect(connection.checkpoint).toBe(checkpoint);
|
|
expect(driver.runnerProcessLauncher).toBe(launch);
|
|
expect(driver.readRunnerState).toBe(read);
|
|
expect(launch).not.toHaveBeenCalled();
|
|
expect(mapCaBundle).not.toHaveBeenCalled();
|
|
expect(transport.connect).not.toHaveBeenCalled();
|
|
expect(prepare.mock.invocationCallOrder[0]).toBeLessThan(transport.register.mock.invocationCallOrder[0]);
|
|
});
|
|
|
|
it("rejects invalid outbound connectivity before staging resources", async () => {
|
|
const prepare = vi.fn();
|
|
const driver = createNativeRunnerExecutionDriver({ target: ssh, binding, prepareRemote: prepare });
|
|
await expect(driver.controlPlaneRegistration!(authority)).rejects.toThrow("runner_transport_ineligible");
|
|
expect(prepare).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("rejects a missing CA file before preparing the remote environment", async () => {
|
|
const prepare = vi.fn();
|
|
const driver = createNativeRunnerExecutionDriver({
|
|
target: ssh, binding, prepareRemote: prepare, runnerPublicUrl: "wss://core.example",
|
|
runnerCaBundlePath: "/nonexistent/paperclip-runner-test-ca.pem",
|
|
});
|
|
await expect(driver.controlPlaneRegistration!(authority)).rejects.toThrow("runner_direct_wss_failed");
|
|
expect(prepare).not.toHaveBeenCalled();
|
|
expect(transport.register).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("binds an outbound CA to the process owner's filesystem", async () => {
|
|
const mapCaBundle = vi.fn(async () => "/remote/ca.pem");
|
|
const driver = createNativeRunnerExecutionDriver({
|
|
target: ssh, binding, prepareRemote: async () => {}, runnerPublicUrl: "wss://core.example",
|
|
runnerCaBundlePath: process.execPath, mapCaBundle,
|
|
});
|
|
const connection = await driver.controlPlaneRegistration!(authority);
|
|
expect(mapCaBundle).toHaveBeenCalledWith(process.execPath);
|
|
expect(connection.connection).toEqual(expect.objectContaining({ caBundlePath: "/remote/ca.pem" }));
|
|
expect(mapCaBundle.mock.invocationCallOrder[0]).toBeLessThan(transport.register.mock.invocationCallOrder[0]);
|
|
});
|
|
|
|
it("does not register an authority route when CA staging fails", async () => {
|
|
const driver = createNativeRunnerExecutionDriver({
|
|
target: ssh, binding, prepareRemote: async () => {}, runnerPublicUrl: "wss://core.example",
|
|
runnerCaBundlePath: process.execPath,
|
|
mapCaBundle: async () => { throw new Error("ca_copy_failed"); },
|
|
});
|
|
await expect(driver.controlPlaneRegistration!(authority)).rejects.toThrow("ca_copy_failed");
|
|
expect(transport.register).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("stages before acquiring ingress and waits for process dispatch before connecting", async () => {
|
|
const ingress = endpoint();
|
|
const acquire = vi.fn(async () => ingress);
|
|
const prepare = vi.fn(async () => {});
|
|
let started!: () => void;
|
|
const processStarted = new Promise<void>(resolve => { started = resolve; });
|
|
const driver = createNativeRunnerExecutionDriver({
|
|
target: { ...sandbox(), getRunnerIngressEndpoint: acquire } as AdapterExecutionTarget,
|
|
binding, runnerIngressAuthorized: true, prepareRemote: prepare, processStarted,
|
|
});
|
|
const connection = await driver.controlPlaneRegistration!(authority);
|
|
expect(prepare).toHaveBeenCalledWith("listen_ws");
|
|
expect(prepare.mock.invocationCallOrder[0]).toBeLessThan(acquire.mock.invocationCallOrder[0]);
|
|
expect(connection.connection?.mode).toBe("listen");
|
|
await connection.activate!();
|
|
expect(transport.connect).not.toHaveBeenCalled();
|
|
started();
|
|
await connection.ready!();
|
|
expect(transport.connect).toHaveBeenCalledWith(expect.objectContaining({ authority, endpoint: ingress }));
|
|
expect(transport.register).not.toHaveBeenCalled();
|
|
await connection.release();
|
|
expect(transport.connect.mock.results[0].value.close).toHaveBeenCalledOnce();
|
|
});
|
|
|
|
it("does not fall back to outbound WSS when sandbox ingress is unauthorized", async () => {
|
|
const prepare = vi.fn();
|
|
const acquire = vi.fn();
|
|
const driver = createNativeRunnerExecutionDriver({
|
|
target: { ...sandbox(), getRunnerIngressEndpoint: acquire } as AdapterExecutionTarget,
|
|
binding, runnerPublicUrl: "wss://core.example", prepareRemote: prepare,
|
|
});
|
|
await expect(driver.controlPlaneRegistration!(authority)).rejects.toThrow("runner_ingress_unavailable");
|
|
expect(prepare).not.toHaveBeenCalled();
|
|
expect(acquire).not.toHaveBeenCalled();
|
|
expect(transport.register).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("closes acquired ingress when setup ends before activation", async () => {
|
|
const ingress = endpoint();
|
|
const driver = createNativeRunnerExecutionDriver({
|
|
target: { ...sandbox(), getRunnerIngressEndpoint: async () => ingress } as AdapterExecutionTarget,
|
|
binding, runnerIngressAuthorized: true, prepareRemote: async () => {}, processStarted: Promise.resolve(),
|
|
});
|
|
const connection = await driver.controlPlaneRegistration!(authority);
|
|
await connection.release();
|
|
expect(ingress.close).toHaveBeenCalledOnce();
|
|
expect(transport.connect).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("uses the newly authorized run binding during warm registration", async () => {
|
|
const beforeConnect = vi.fn(async () => {});
|
|
const identity = { runId: "run-b" } as Parameters<NonNullable<ReturnType<typeof createNativeRunnerExecutionDriver>["controlPlaneRegistration"]>>[1];
|
|
const driver = createNativeRunnerExecutionDriver({ target: { kind: "local" }, binding, beforeConnect });
|
|
await driver.controlPlaneRegistration!(authority, identity);
|
|
expect(beforeConnect).toHaveBeenCalledWith(identity);
|
|
expect(transport.register).toHaveBeenCalledWith({ ...binding, runId: "run-b", authority });
|
|
expect(beforeConnect.mock.invocationCallOrder[0]).toBeLessThan(transport.register.mock.invocationCallOrder[0]);
|
|
});
|
|
|
|
it("blocks registration if warm recovery authority is no longer valid", async () => {
|
|
const driver = createNativeRunnerExecutionDriver({
|
|
target: { kind: "local" }, binding,
|
|
beforeConnect: async () => { throw new Error("recovery_denied"); },
|
|
});
|
|
await expect(driver.controlPlaneRegistration!(authority)).rejects.toThrow("recovery_denied");
|
|
expect(transport.register).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("requires exact process identity before admitting an external provider", async () => {
|
|
const driver = createNativeRunnerExecutionDriver({
|
|
target: ssh, binding, runnerPublicUrl: "wss://core.example", prepareRemote: async () => {},
|
|
processIdentityReady: Promise.reject(new Error("process_identity_invalid")),
|
|
});
|
|
const connection = await driver.controlPlaneRegistration!(authority);
|
|
await expect(connection.ready!()).rejects.toThrow("process_identity_invalid");
|
|
});
|
|
});
|
|
|
|
it("validates connectivity before resolving provider artifacts without acquiring an ingress endpoint", async () => {
|
|
const prepare = vi.fn(async () => {});
|
|
await expect(prepareNativeRemoteRunner({ target: ssh, runId: "run", prepareRemote: prepare })).rejects.toThrow("runner_transport_ineligible");
|
|
expect(prepare).not.toHaveBeenCalled();
|
|
await prepareNativeRemoteRunner({ target: sandbox(), runnerIngressAuthorized: true, runId: "run", prepareRemote: prepare });
|
|
expect(prepare).toHaveBeenCalledExactlyOnceWith("listen_ws");
|
|
expect(transport.connect).not.toHaveBeenCalled();
|
|
});
|