diff --git a/packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts b/packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts index e96722c9759..9017824a5a1 100644 --- a/packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts +++ b/packages/paperclip-runner/src/drivers/acpx/installation-integrity.ts @@ -30,6 +30,7 @@ import { import type { Readable, Writable } from "node:stream"; import { resolveQualifiedAcpxProfile, type QualifiedAcpxProfile } from "./qualified-profiles.js"; +import { resolveInstalledProviderAuthority } from "./installed-provider-authority.js"; import { VERIFIED_RUNTIME_EXECUTABLE_ENV, verifiedRuntimeExecutableHandoff, @@ -645,11 +646,13 @@ export async function verifyQualifiedAcpxInstallation( ): Promise { // Keep the selected package authority available when its bundled provider // wrapper delegates platform installation to the published server manifest. - if ((profile.agent === "codex" || profile.agent === "claude") && resolvePackageJson === defaultPackageJsonResolver && process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT !== undefined) { - resolvePackageJson = createAcpxPackageJsonResolver( - process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT, - process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST, - ); + if ((profile.agent === "codex" || profile.agent === "claude") && resolvePackageJson === defaultPackageJsonResolver) { + const boundRoot = process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT; + const authority = boundRoot === undefined ? resolveInstalledProviderAuthority(import.meta.url) : { + root: boundRoot, + manifest: process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST, + }; + if (authority) resolvePackageJson = createAcpxPackageJsonResolver(authority.root, authority.manifest); } const builtin = profile.agent === "grok"; if (builtin && (profile.agentServerPackage !== "builtin:grok-acp" || profile.agentServerVersion !== "1" || profile.agentRuntimePackage !== "native:grok" || profile.agentRuntimeVersion !== "1.0.13")) { diff --git a/packages/paperclip-runner/src/drivers/acpx/installed-provider-authority.test.ts b/packages/paperclip-runner/src/drivers/acpx/installed-provider-authority.test.ts new file mode 100644 index 00000000000..c1274972577 --- /dev/null +++ b/packages/paperclip-runner/src/drivers/acpx/installed-provider-authority.test.ts @@ -0,0 +1,60 @@ +import { mkdtemp, mkdir, writeFile, symlink, rm, realpath } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { pathToFileURL } from "node:url"; +import { afterEach, describe, expect, it } from "vitest"; +import { resolveInstalledProviderAuthority } from "./installed-provider-authority.js"; + +const roots: string[] = []; +afterEach(async () => { await Promise.all(roots.splice(0).map(root => rm(root, { recursive: true, force: true }))); }); +async function fixture(name = "@paperclipai/server") { + const root = await realpath(await mkdtemp(join(tmpdir(), "installed-acpx-authority-"))); + roots.push(root); + const directory = join(root, "dist/vendor/paperclip-runner/drivers/acpx"); + await mkdir(directory, { recursive: true }); + const module = join(directory, "installation-integrity.js"); + await writeFile(module, "export {};\n"); + const manifest = join(root, "package.json"); + await writeFile(manifest, JSON.stringify({ name })); + return { root, module, manifest }; +} + +describe("installed provider authority", () => { + it("derives the owning server only from the normal installed verifier layout", async () => { + const f = await fixture(); + expect(resolveInstalledProviderAuthority(pathToFileURL(f.module).href)).toEqual({ root: f.root, manifest: f.manifest }); + expect(resolveInstalledProviderAuthority(pathToFileURL(join(f.root, "src/drivers/acpx/installation-integrity.ts")).href)).toBeUndefined(); + expect(resolveInstalledProviderAuthority(pathToFileURL(join(f.root, "dist/drivers/acpx/installation-integrity.js")).href)).toBeUndefined(); + }); + + it("rejects a substituted package owner", async () => { + const f = await fixture("unrelated-package"); + expect(() => resolveInstalledProviderAuthority(pathToFileURL(f.module).href)).toThrow("owning server package"); + }); + + it("rejects a symlinked manifest before adopting its declarations", async () => { + const f = await fixture(); + const other = join(f.root, "other.json"); + await writeFile(other, JSON.stringify({ name: "@paperclipai/server" })); + await rm(f.manifest); + await symlink(other, f.manifest); + expect(() => resolveInstalledProviderAuthority(pathToFileURL(f.module).href)).toThrow(); + }); + + it("rejects a redirected verifier directory", async () => { + const f = await fixture(); + const old = join(f.root, "dist/vendor/paperclip-runner/drivers/acpx"); + const other = join(f.root, "other"); + await mkdir(other); + await writeFile(join(other, "installation-integrity.js"), "export {};\n"); + await rm(old, { recursive: true }); + await symlink(other, old); + expect(() => resolveInstalledProviderAuthority(pathToFileURL(f.module).href)).toThrow("package layout"); + }); + + it.each(["", " ".repeat(256 * 1024 + 1)])("rejects unbounded or empty owner metadata", async contents => { + const f = await fixture(); + await writeFile(f.manifest, contents); + expect(() => resolveInstalledProviderAuthority(pathToFileURL(f.module).href)).toThrow("bounded regular file"); + }); +}); diff --git a/packages/paperclip-runner/src/drivers/acpx/installed-provider-authority.ts b/packages/paperclip-runner/src/drivers/acpx/installed-provider-authority.ts new file mode 100644 index 00000000000..fd0979295c4 --- /dev/null +++ b/packages/paperclip-runner/src/drivers/acpx/installed-provider-authority.ts @@ -0,0 +1,51 @@ +import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, realpathSync } from "node:fs"; +import { isAbsolute, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; + +export function resolveInstalledProviderAuthority(moduleUrl: string): { root: string; manifest: string } | undefined { + const url = new URL(moduleUrl); + if (url.protocol !== "file:") return undefined; + const modulePath = fileURLToPath(url); + const suffix = "/dist/vendor/paperclip-runner/drivers/acpx/installation-integrity.js"; + if (!modulePath.endsWith(suffix)) return undefined; + const root = modulePath.slice(0, -suffix.length); + if (!isAbsolute(root) || resolve(root) !== root || realpathSync(root) !== root) { + throw new Error("Installed ACPX server authority must be a canonical package directory"); + } + for (const relative of ["", "dist", "dist/vendor", "dist/vendor/paperclip-runner", "dist/vendor/paperclip-runner/drivers", "dist/vendor/paperclip-runner/drivers/acpx"]) { + const path = join(root, relative); + const metadata = lstatSync(path); + if (!metadata.isDirectory() || metadata.isSymbolicLink() || realpathSync(path) !== path) { + throw new Error("Installed ACPX server authority escapes its package layout"); + } + } + const moduleMetadata = lstatSync(modulePath); + if (!moduleMetadata.isFile() || moduleMetadata.isSymbolicLink() || realpathSync(modulePath) !== modulePath) { + throw new Error("Installed ACPX verifier must remain in its package layout"); + } + const manifest = join(root, "package.json"); + const fd = openSync(manifest, constants.O_RDONLY | constants.O_NOFOLLOW); + try { + const before = fstatSync(fd, { bigint: true }); + if (!before.isFile() || before.size < 1n || before.size > 256n * 1024n) { + throw new Error("Installed ACPX server manifest must be a bounded regular file"); + } + const bytes = readFileSync(fd); + const after = fstatSync(fd, { bigint: true }); + const named = lstatSync(manifest, { bigint: true }); + if (before.dev !== after.dev || before.ino !== after.ino || before.size !== after.size + || before.mtimeNs !== after.mtimeNs || before.ctimeNs !== after.ctimeNs + || named.dev !== before.dev || named.ino !== before.ino || named.isSymbolicLink() + || bytes.length !== Number(before.size) || realpathSync(manifest) !== manifest) { + throw new Error("Installed ACPX server manifest changed during admission"); + } + const value = JSON.parse(bytes.toString("utf8")); + if (!value || typeof value !== "object" || Array.isArray(value) + || !Object.hasOwn(value, "name") || value.name !== "@paperclipai/server") { + throw new Error("Installed ACPX verifier requires the owning server package"); + } + } finally { + closeSync(fd); + } + return { root, manifest }; +}