diff --git a/packages/plugins/sdk/src/worker-rpc-host.ts b/packages/plugins/sdk/src/worker-rpc-host.ts index 81104fea8fe..643f9b0df43 100644 --- a/packages/plugins/sdk/src/worker-rpc-host.ts +++ b/packages/plugins/sdk/src/worker-rpc-host.ts @@ -34,6 +34,7 @@ * @see PLUGIN_SPEC.md §14 — SDK Surface */ +import fs from "node:fs"; import path from "node:path"; import { createInterface, type Interface as ReadlineInterface } from "node:readline"; import { fileURLToPath } from "node:url"; @@ -175,6 +176,21 @@ interface EventRegistration { /** Default timeout for worker→host RPC calls. */ const DEFAULT_RPC_TIMEOUT_MS = 30_000; +function realpathOrResolvedPath(filePath: string): string { + const resolvedPath = path.resolve(filePath); + try { + return fs.realpathSync.native(resolvedPath); + } catch { + return resolvedPath; + } +} + +export function isWorkerEntrypoint(entry: string, moduleUrl: string): boolean { + const thisFile = realpathOrResolvedPath(fileURLToPath(moduleUrl)); + const entryPath = realpathOrResolvedPath(entry); + return thisFile === entryPath; +} + // --------------------------------------------------------------------------- // startWorkerRpcHost // --------------------------------------------------------------------------- @@ -223,9 +239,7 @@ export function runWorker( } const entry = process.argv[1]; if (typeof entry !== "string") return; - const thisFile = path.resolve(fileURLToPath(moduleUrl)); - const entryPath = path.resolve(entry); - if (thisFile === entryPath) { + if (isWorkerEntrypoint(entry, moduleUrl)) { startWorkerRpcHost({ plugin }); } } diff --git a/packages/plugins/sdk/tests/worker-rpc-host.test.ts b/packages/plugins/sdk/tests/worker-rpc-host.test.ts new file mode 100644 index 00000000000..f8e0a38e5dc --- /dev/null +++ b/packages/plugins/sdk/tests/worker-rpc-host.test.ts @@ -0,0 +1,57 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; + +import { afterEach, describe, expect, it } from "vitest"; + +import { isWorkerEntrypoint } from "../src/worker-rpc-host.js"; + +describe("isWorkerEntrypoint", () => { + const tempRoots: string[] = []; + + afterEach(() => { + for (const tempRoot of tempRoots.splice(0)) { + fs.rmSync(tempRoot, { recursive: true, force: true }); + } + }); + + function createTempRoot(): string { + const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "paperclip-sdk-worker-")); + tempRoots.push(tempRoot); + return tempRoot; + } + + it("matches an entrypoint reached through a symlinked directory", () => { + const tempRoot = createTempRoot(); + const realDir = path.join(tempRoot, "real"); + const linkDir = path.join(tempRoot, "link"); + fs.mkdirSync(realDir); + fs.symlinkSync(realDir, linkDir, "dir"); + + const workerPath = path.join(realDir, "worker.js"); + fs.writeFileSync(workerPath, ""); + + expect( + isWorkerEntrypoint( + path.join(linkDir, "worker.js"), + pathToFileURL(workerPath).toString(), + ), + ).toBe(true); + }); + + it("does not match a different entrypoint", () => { + const tempRoot = createTempRoot(); + const workerPath = path.join(tempRoot, "worker.js"); + const otherPath = path.join(tempRoot, "other.js"); + fs.writeFileSync(workerPath, ""); + fs.writeFileSync(otherPath, ""); + + expect( + isWorkerEntrypoint( + otherPath, + pathToFileURL(workerPath).toString(), + ), + ).toBe(false); + }); +}); diff --git a/packages/plugins/sdk/vitest.config.ts b/packages/plugins/sdk/vitest.config.ts new file mode 100644 index 00000000000..faa6d98e10c --- /dev/null +++ b/packages/plugins/sdk/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + environment: "node", + include: ["tests/**/*.test.ts"], + }, +}); diff --git a/scripts/run-vitest-stable.mjs b/scripts/run-vitest-stable.mjs index 4ded9794581..c85c48aa9ef 100644 --- a/scripts/run-vitest-stable.mjs +++ b/scripts/run-vitest-stable.mjs @@ -14,6 +14,7 @@ const nonServerProjects = [ "@paperclipai/adapter-acpx-local", "@paperclipai/adapter-codex-local", "@paperclipai/adapter-opencode-local", + "@paperclipai/plugin-sdk", "@paperclipai/ui", "paperclipai", ]; diff --git a/vitest.config.ts b/vitest.config.ts index 42b8e879212..f27929e74f8 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -14,6 +14,7 @@ export default defineConfig({ "packages/adapters/gemini-local", "packages/adapters/opencode-local", "packages/adapters/pi-local", + "packages/plugins/sdk", "server", "ui", "cli",