fix(runner): restore rooted ACPX package resolution
This commit is contained in:
parent
6f0614ec74
commit
3d8720c16f
|
|
@ -154,13 +154,18 @@
|
|||
"dependencies": {
|
||||
"@agentclientprotocol/claude-agent-acp": "0.70.0",
|
||||
"@agentclientprotocol/codex-acp": "1.6.2",
|
||||
"@openai/codex": "0.148.0",
|
||||
"acpx": "0.13.1",
|
||||
"ajv": "^8.20.0",
|
||||
"json-schema-to-ts": "^3.1.1",
|
||||
"node": "24.11.0",
|
||||
"opencode-ai": "1.18.17",
|
||||
"react-markdown": "^10.1.0",
|
||||
"remark-gfm": "^4.0.1"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@openai/codex-linux-x64": "npm:@openai/codex@0.148.0-linux-x64"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=18",
|
||||
"react-dom": ">=18"
|
||||
|
|
|
|||
|
|
@ -120,6 +120,7 @@ impl AcpxSidecarTransport {
|
|||
"PAPERCLIP_NATIVE_MCP_NAME",
|
||||
"PAPERCLIP_NATIVE_MCP_URL",
|
||||
"PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT",
|
||||
"PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST",
|
||||
];
|
||||
keys.extend_from_slice(credential_keys);
|
||||
Self::start_with_environment_keys(config, &keys)
|
||||
|
|
|
|||
|
|
@ -208,6 +208,8 @@ it("preserves the controller-selected ACPX provider package root", () => {
|
|||
environment: {
|
||||
PATH: "/bin",
|
||||
PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT: "/verified/provider-pack",
|
||||
PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST:
|
||||
"/verified/provider-pack/package.json",
|
||||
NODE_PATH: "/untrusted/modules",
|
||||
},
|
||||
processLauncher: (spec) => {
|
||||
|
|
@ -233,6 +235,9 @@ it("preserves the controller-selected ACPX provider package root", () => {
|
|||
expect(launches[0]!.environment.PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT).toBe(
|
||||
"/verified/provider-pack",
|
||||
);
|
||||
expect(
|
||||
launches[0]!.environment.PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST,
|
||||
).toBe("/verified/provider-pack/package.json");
|
||||
expect(launches[0]!.environment.NODE_PATH).toBeUndefined();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -1929,6 +1929,7 @@ const runnerExplicitProviderEnvironmentKeys = [
|
|||
"PAPERCLIP_NATIVE_MCP_TOKEN",
|
||||
"PAPERCLIP_NATIVE_RUNTIME_CONTEXT_PATH",
|
||||
"PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT",
|
||||
"PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST",
|
||||
"PAPERCLIP_ACPX_PROVIDER_RECOVERY_POLICY",
|
||||
"PAPERCLIP_PROVIDER_TRACE_PATH",
|
||||
"PAPERCLIP_PROVIDER_TRACE_MAX_BYTES",
|
||||
|
|
|
|||
|
|
@ -76,6 +76,39 @@ describe("ACPX installation integrity", () => {
|
|||
"explicit normalized absolute path",
|
||||
);
|
||||
|
||||
const runnerPackage = join(root, "packages", "paperclip-runner");
|
||||
const runnerManifest = join(runnerPackage, "package.json");
|
||||
const pnpmProviderDirectory = join(
|
||||
root,
|
||||
"node_modules",
|
||||
".pnpm",
|
||||
"qualified-provider@1.0.0",
|
||||
"node_modules",
|
||||
"pnpm-provider",
|
||||
);
|
||||
await Promise.all([
|
||||
mkdir(join(runnerPackage, "node_modules"), { recursive: true }),
|
||||
mkdir(pnpmProviderDirectory, { recursive: true }),
|
||||
writeFile(runnerManifest, JSON.stringify({ private: true })),
|
||||
writeFile(
|
||||
join(pnpmProviderDirectory, "package.json"),
|
||||
JSON.stringify({ name: "pnpm-provider", version: "1.0.0" }),
|
||||
),
|
||||
]);
|
||||
await symlink(
|
||||
pnpmProviderDirectory,
|
||||
join(runnerPackage, "node_modules", "pnpm-provider"),
|
||||
);
|
||||
expect(
|
||||
createAcpxPackageJsonResolver(root, runnerManifest)("pnpm-provider"),
|
||||
).toBe(join(pnpmProviderDirectory, "package.json"));
|
||||
|
||||
const outsideManifest = join(parent, "outside-package.json");
|
||||
await writeFile(outsideManifest, JSON.stringify({ private: true }));
|
||||
expect(() =>
|
||||
createAcpxPackageJsonResolver(root, outsideManifest),
|
||||
).toThrow("manifest resolves outside the selected provider root");
|
||||
|
||||
const ancestorProviderDirectory = join(
|
||||
parent,
|
||||
"node_modules",
|
||||
|
|
|
|||
|
|
@ -218,6 +218,7 @@ export type AcpxPackageJsonResolver = (packageName: string) => string;
|
|||
|
||||
export function createAcpxPackageJsonResolver(
|
||||
providerPackageRoot: string | undefined,
|
||||
providerPackageManifest?: string,
|
||||
): AcpxPackageJsonResolver {
|
||||
const root = providerPackageRoot?.trim();
|
||||
if (
|
||||
|
|
@ -230,7 +231,26 @@ export function createAcpxPackageJsonResolver(
|
|||
"ACPX provider package root must be an explicit normalized absolute path",
|
||||
);
|
||||
}
|
||||
const manifest = (
|
||||
providerPackageManifest ?? resolve(root, "package.json")
|
||||
).trim();
|
||||
if (
|
||||
!manifest ||
|
||||
!isAbsolute(manifest) ||
|
||||
manifest.includes("\0") ||
|
||||
resolve(manifest) !== manifest
|
||||
) {
|
||||
throw new Error(
|
||||
"ACPX provider package manifest must be an explicit normalized absolute path",
|
||||
);
|
||||
}
|
||||
const canonicalRoot = realpathSync(root);
|
||||
const canonicalManifest = realpathSync(manifest);
|
||||
if (!pathIsInside(canonicalRoot, canonicalManifest)) {
|
||||
throw new Error(
|
||||
"ACPX provider package manifest resolves outside the selected provider root",
|
||||
);
|
||||
}
|
||||
const canonicalNodeModules = realpathSync(
|
||||
resolve(canonicalRoot, "node_modules"),
|
||||
);
|
||||
|
|
@ -239,7 +259,7 @@ export function createAcpxPackageJsonResolver(
|
|||
"ACPX provider node_modules resolves outside the selected provider root",
|
||||
);
|
||||
}
|
||||
const providerRequire = createRequire(resolve(root, "package.json"));
|
||||
const providerRequire = createRequire(canonicalManifest);
|
||||
return (packageName) => {
|
||||
const packageJsonPath = realpathSync(
|
||||
providerRequire.resolve(`${packageName}/package.json`),
|
||||
|
|
@ -576,7 +596,10 @@ export async function verifyQualifiedAcpxInstallation(
|
|||
function defaultPackageJsonResolver(packageName: string): string {
|
||||
const providerPackageRoot = process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT;
|
||||
if (providerPackageRoot !== undefined) {
|
||||
return createAcpxPackageJsonResolver(providerPackageRoot)(packageName);
|
||||
return createAcpxPackageJsonResolver(
|
||||
providerPackageRoot,
|
||||
process.env.PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST,
|
||||
)(packageName);
|
||||
}
|
||||
// Source-mode and direct runtimes still have a stable module URL. The
|
||||
// descriptor-backed runner sidecar always receives the explicit root above.
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import {
|
|||
import { createHash } from "node:crypto";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
import { expect, it } from "vitest";
|
||||
|
||||
|
|
@ -140,14 +141,26 @@ it.each(["acpx-runtime-sidecar.cjs", "opencode-app-server-proxy.cjs"] as const)(
|
|||
},
|
||||
);
|
||||
|
||||
it("derives the ACPX package root only from the verified dist/cli layout", () => {
|
||||
it("derives the ACPX package authority only from the verified dist/cli layout", () => {
|
||||
const runnerPackageRoot = fileURLToPath(new URL("../..", import.meta.url));
|
||||
expect(
|
||||
runnerdLaunchProfileInternals.acpxProviderPackageRoot(
|
||||
runnerdLaunchProfileInternals.acpxProviderPackageAuthority(
|
||||
resolve(runnerPackageRoot, "dist/cli/acpx-runtime-sidecar.cjs"),
|
||||
),
|
||||
).toEqual({
|
||||
root: resolve(runnerPackageRoot, "../.."),
|
||||
manifest: resolve(runnerPackageRoot, "package.json"),
|
||||
});
|
||||
expect(
|
||||
runnerdLaunchProfileInternals.acpxProviderPackageAuthority(
|
||||
"/provider-pack/dist/cli/acpx-runtime-sidecar.cjs",
|
||||
),
|
||||
).toBe("/provider-pack");
|
||||
).toEqual({
|
||||
root: "/provider-pack",
|
||||
manifest: "/provider-pack/package.json",
|
||||
});
|
||||
expect(() =>
|
||||
runnerdLaunchProfileInternals.acpxProviderPackageRoot(
|
||||
runnerdLaunchProfileInternals.acpxProviderPackageAuthority(
|
||||
"/unverified/acpx-runtime-sidecar.cjs",
|
||||
),
|
||||
).toThrow("ACPX sidecar must use the provider package dist/cli layout");
|
||||
|
|
@ -446,6 +459,8 @@ it.each([
|
|||
PATH: "/bin",
|
||||
...credentialEnvironment,
|
||||
PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT: "/attacker/package-root",
|
||||
PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST:
|
||||
"/attacker/package-root/package.json",
|
||||
PAPERCLIP_API_KEY: "must-not-reach-provider",
|
||||
DATABASE_URL: "must-not-reach-provider",
|
||||
},
|
||||
|
|
@ -472,6 +487,8 @@ it.each([
|
|||
PAPERCLIP_NORMALIZED_SESSION_ID: "session-1",
|
||||
PAPERCLIP_NATIVE_RUNTIME_CONTEXT_PATH: "/isolated/runtime-context.json",
|
||||
PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT: "/verified/provider-pack",
|
||||
PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST:
|
||||
"/verified/provider-pack/package.json",
|
||||
});
|
||||
for (const key of allowed)
|
||||
expect(environment[key]).toBe(credentialEnvironment[key]);
|
||||
|
|
|
|||
|
|
@ -1111,7 +1111,10 @@ function resolveBuildOwnedCliArtifact(
|
|||
);
|
||||
}
|
||||
|
||||
function acpxProviderPackageRoot(sidecarScript: string): string {
|
||||
function acpxProviderPackageAuthority(sidecarScript: string): {
|
||||
root: string;
|
||||
manifest: string;
|
||||
} {
|
||||
const cliDirectory = dirname(sidecarScript);
|
||||
if (
|
||||
basename(sidecarScript) !== "acpx-runtime-sidecar.cjs" ||
|
||||
|
|
@ -1126,8 +1129,14 @@ function acpxProviderPackageRoot(sidecarScript: string): string {
|
|||
// A local source build consumes pnpm's workspace-owned node_modules tree.
|
||||
// A deployed provider pack owns a closed node_modules tree at its own root.
|
||||
return sidecarPackageRoot === packageRoot
|
||||
? resolve(packageRoot, "../..")
|
||||
: sidecarPackageRoot;
|
||||
? {
|
||||
root: resolve(packageRoot, "../.."),
|
||||
manifest: resolve(packageRoot, "package.json"),
|
||||
}
|
||||
: {
|
||||
root: sidecarPackageRoot,
|
||||
manifest: resolve(sidecarPackageRoot, "package.json"),
|
||||
};
|
||||
}
|
||||
|
||||
function acpxRunnerLaunchProfile(
|
||||
|
|
@ -1342,6 +1351,8 @@ export function createCapabilityRunnerdProviderEnvironment(input: {
|
|||
input.acpxSidecarPath ??
|
||||
input.options.acpxSidecarPath ??
|
||||
resolve(packageRoot, "dist", "cli", "acpx-runtime-sidecar.cjs");
|
||||
const providerPackageAuthority =
|
||||
acpxProviderPackageAuthority(sidecarPath);
|
||||
return {
|
||||
...createSanitizedAcpxSpawnInput(
|
||||
input.options.environment,
|
||||
|
|
@ -1352,7 +1363,9 @@ export function createCapabilityRunnerdProviderEnvironment(input: {
|
|||
// executes it through /proc/self/fd. Anchor its closed provider package
|
||||
// lookups at the package that owns the already-authenticated bundle.
|
||||
PAPERCLIP_ACPX_PROVIDER_PACKAGE_ROOT:
|
||||
acpxProviderPackageRoot(sidecarPath),
|
||||
providerPackageAuthority.root,
|
||||
PAPERCLIP_ACPX_PROVIDER_PACKAGE_MANIFEST:
|
||||
providerPackageAuthority.manifest,
|
||||
...(input.options.providerRecoveryPolicy ===
|
||||
"allow_replacement_after_governed_wait"
|
||||
? {
|
||||
|
|
@ -3413,7 +3426,7 @@ export const createRunnerdCodexTransport =
|
|||
createCapabilityRunnerdCodexTransport;
|
||||
|
||||
export const runnerdLaunchProfileInternals = Object.freeze({
|
||||
acpxProviderPackageRoot,
|
||||
acpxProviderPackageAuthority,
|
||||
acpxRunnerLaunchProfile,
|
||||
resolveBuildOwnedCliArtifact,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -32,6 +32,12 @@ const claudePatch = await readFile(
|
|||
);
|
||||
|
||||
test("the runner pins every qualified ACPX production dependency", () => {
|
||||
assert.equal(runnerPackage.dependencies.node, "24.11.0");
|
||||
assert.equal(runnerPackage.dependencies["@openai/codex"], "0.148.0");
|
||||
assert.equal(
|
||||
runnerPackage.optionalDependencies["@openai/codex-linux-x64"],
|
||||
"npm:@openai/codex@0.148.0-linux-x64",
|
||||
);
|
||||
assert.equal(runnerPackage.dependencies.acpx, "0.13.1");
|
||||
assert.equal(
|
||||
runnerPackage.dependencies["@agentclientprotocol/codex-acp"],
|
||||
|
|
|
|||
|
|
@ -477,6 +477,9 @@ importers:
|
|||
'@agentclientprotocol/codex-acp':
|
||||
specifier: 1.6.2
|
||||
version: 1.6.2(patch_hash=grbydorkxatbzksnwwfuawwtim)
|
||||
'@openai/codex':
|
||||
specifier: 0.148.0
|
||||
version: 0.148.0
|
||||
acpx:
|
||||
specifier: 0.13.1
|
||||
version: 0.13.1(patch_hash=lzpwjtiaybzoijy455dfycwavu)
|
||||
|
|
@ -486,6 +489,9 @@ importers:
|
|||
json-schema-to-ts:
|
||||
specifier: ^3.1.1
|
||||
version: 3.1.1
|
||||
node:
|
||||
specifier: 24.11.0
|
||||
version: 24.11.0
|
||||
opencode-ai:
|
||||
specifier: 1.18.17
|
||||
version: 1.18.17
|
||||
|
|
@ -495,6 +501,10 @@ importers:
|
|||
remark-gfm:
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1
|
||||
optionalDependencies:
|
||||
'@openai/codex-linux-x64':
|
||||
specifier: npm:@openai/codex@0.148.0-linux-x64
|
||||
version: '@openai/codex@0.148.0-linux-x64'
|
||||
devDependencies:
|
||||
'@paperclipai/paperclip-eval-kernel':
|
||||
specifier: workspace:*
|
||||
|
|
@ -7088,6 +7098,9 @@ packages:
|
|||
resolution: {integrity: sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
node-bin-setup@1.1.4:
|
||||
resolution: {integrity: sha512-vWNHOne0ZUavArqPP5LJta50+S8R261Fr5SvGul37HbEDcowvLjwdvd0ZeSr0r2lTSrPxl6okq9QUw8BFGiAxA==}
|
||||
|
||||
node-domexception@1.0.0:
|
||||
resolution: {integrity: sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==}
|
||||
engines: {node: '>=10.5.0'}
|
||||
|
|
@ -7101,6 +7114,11 @@ packages:
|
|||
resolution: {integrity: sha512-D9UOmYG3UH1V+ENW56t5QXBwJw1YEY18ruVeus89Rw+SyIgjPkCO84bRzO3uNIYosJbNwiabWVn48o3uJLjxFQ==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
node@24.11.0:
|
||||
resolution: {integrity: sha512-pGLc7hd4xh0doDpnHC5+PL09WLUDKFoBFsC28341AYhHjzTDbNSwY+2615tYY6YFO2WQUKit4yqTwzIHaaityg==}
|
||||
engines: {npm: '>=5.0.0'}
|
||||
hasBin: true
|
||||
|
||||
npm-run-path@4.0.1:
|
||||
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
|
||||
engines: {node: '>=8'}
|
||||
|
|
@ -14577,6 +14595,8 @@ snapshots:
|
|||
|
||||
negotiator@1.0.0: {}
|
||||
|
||||
node-bin-setup@1.1.4: {}
|
||||
|
||||
node-domexception@1.0.0: {}
|
||||
|
||||
node-fetch@3.3.2:
|
||||
|
|
@ -14587,6 +14607,10 @@ snapshots:
|
|||
|
||||
node-releases@2.0.53: {}
|
||||
|
||||
node@24.11.0:
|
||||
dependencies:
|
||||
node-bin-setup: 1.1.4
|
||||
|
||||
npm-run-path@4.0.1:
|
||||
dependencies:
|
||||
path-key: 3.1.1
|
||||
|
|
|
|||
Loading…
Reference in New Issue