diff --git a/package.json b/package.json index 0cc69ce..ff89367 100644 --- a/package.json +++ b/package.json @@ -29,14 +29,12 @@ "release:metadata": "node ./scripts/release-metadata.mjs", "release:apply-version": "node ./scripts/apply-release-version.mjs" }, - "peerDependencies": { - "openclaw": ">=2026.3.22" - }, "dependencies": { "ws": "^8.18.3" }, "devDependencies": { "@types/node": "^24.6.0", + "openclaw": "2026.4.1", "typescript": "^5.9.2", "vitest": "^3.2.4", "yaml": "^2.8.2" diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 837acfe..0a4139a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,9 +8,6 @@ importers: .: dependencies: - openclaw: - specifier: '>=2026.3.22' - version: 2026.4.1(@napi-rs/canvas@0.1.96) ws: specifier: ^8.18.3 version: 8.19.0 @@ -18,6 +15,9 @@ importers: '@types/node': specifier: ^24.6.0 version: 24.12.0 + openclaw: + specifier: 2026.4.1 + version: 2026.4.1(@napi-rs/canvas@0.1.96) typescript: specifier: ^5.9.2 version: 5.9.3 @@ -1280,6 +1280,7 @@ packages: basic-ftp@5.2.0: resolution: {integrity: sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==} engines: {node: '>=10.0.0'} + deprecated: Security vulnerability fixed in 5.2.1, please upgrade bignumber.js@9.3.1: resolution: {integrity: sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==} @@ -2473,10 +2474,6 @@ packages: strnum@2.2.0: resolution: {integrity: sha512-Y7Bj8XyJxnPAORMZj/xltsfo55uOiyHcU2tnAVzHUnSJR/KsEX+9RoDeXEnsXtl/CX4fAcrt64gZ13aGaWPeBg==} - strtok3@10.3.4: - resolution: {integrity: sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg==} - engines: {node: '>=18'} - strtok3@10.3.5: resolution: {integrity: sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==} engines: {node: '>=18'} @@ -4617,7 +4614,7 @@ snapshots: file-type@21.3.4: dependencies: '@tokenizer/inflate': 0.4.1 - strtok3: 10.3.4 + strtok3: 10.3.5 token-types: 6.1.2 uint8array-extras: 1.5.0 transitivePeerDependencies: @@ -5605,10 +5602,6 @@ snapshots: strnum@2.2.0: {} - strtok3@10.3.4: - dependencies: - '@tokenizer/token': 0.3.0 - strtok3@10.3.5: dependencies: '@tokenizer/token': 0.3.0 diff --git a/src/openclaw-sdk-compat.test.ts b/src/openclaw-sdk-compat.test.ts index bcc3ad3..ea19008 100644 --- a/src/openclaw-sdk-compat.test.ts +++ b/src/openclaw-sdk-compat.test.ts @@ -40,6 +40,19 @@ describe("openclaw sdk compat", () => { }); it("falls back to the dist facade when the public subpath is gone", async () => { + const files = new Map([ + [ + "/tmp/node_modules/openclaw/package.json", + JSON.stringify({ + name: "openclaw", + exports: { + "./plugin-sdk": { default: "./dist/plugin-sdk/index.js" }, + "./cli-entry": { default: "./dist/cli-entry.js" }, + }, + }), + ], + ["/tmp/node_modules/openclaw/dist/plugin-sdk/discord.js", ""], + ]); const importer = vi.fn(async (specifier: string) => { if (specifier === "openclaw/plugin-sdk/discord") { throw Object.assign( @@ -58,7 +71,15 @@ describe("openclaw sdk compat", () => { label: "discord", importer, resolver: () => "/tmp/node_modules/openclaw/dist/index.js", - pathExists: () => true, + pathExists: (targetPath) => + targetPath === "/tmp/node_modules/openclaw/dist/index.js" || files.has(targetPath), + readFile: (targetPath) => { + const content = files.get(targetPath); + if (!content) { + throw new Error(`missing ${targetPath}`); + } + return content; + }, cache: new Map(), }); @@ -103,6 +124,76 @@ describe("openclaw sdk compat", () => { expect(result).toBe("/host/openclaw/dist/index.js"); }); + it("falls back to the host OpenClaw entrypoint from require.main when argv/cwd do not identify it", () => { + const files = new Map([ + [ + "/opt/homebrew/lib/node_modules/openclaw/package.json", + JSON.stringify({ + name: "openclaw", + exports: { + "./plugin-sdk": { default: "./dist/plugin-sdk/index.js" }, + "./cli-entry": { default: "./dist/cli-entry.js" }, + }, + }), + ], + ]); + + const result = resolveOpenClawEntrypointPath({ + argv1: "/Users/huntharo/.openclaw/extensions/openclaw-codex-app-server/index.ts", + cwd: "/Users/huntharo/.openclaw/extensions/openclaw-codex-app-server", + mainFilename: "/opt/homebrew/lib/node_modules/openclaw/dist/index.js", + pathExists: (targetPath) => + targetPath === "/opt/homebrew/lib/node_modules/openclaw/dist/index.js" || + files.has(targetPath), + readFile: (targetPath) => { + const content = files.get(targetPath); + if (!content) { + throw new Error(`missing ${targetPath}`); + } + return content; + }, + resolver: () => + "/Users/huntharo/.openclaw/extensions/openclaw-codex-app-server/node_modules/openclaw/dist/index.js", + }); + + expect(result).toBe("/opt/homebrew/lib/node_modules/openclaw/dist/index.js"); + }); + + it("rejects extension-local vendored openclaw fallbacks", () => { + const files = new Map([ + [ + "/Users/huntharo/.openclaw/extensions/openclaw-codex-app-server/node_modules/openclaw/package.json", + JSON.stringify({ + name: "openclaw", + exports: { + "./plugin-sdk": { default: "./dist/plugin-sdk/index.js" }, + "./cli-entry": { default: "./dist/cli-entry.js" }, + }, + }), + ], + ]); + + expect(() => + resolveOpenClawEntrypointPath({ + argv1: "/Users/huntharo/.openclaw/extensions/openclaw-codex-app-server/index.ts", + cwd: "/Users/huntharo/.openclaw/extensions/openclaw-codex-app-server", + pathExists: (targetPath) => + targetPath === + "/Users/huntharo/.openclaw/extensions/openclaw-codex-app-server/node_modules/openclaw/dist/index.js" || + files.has(targetPath), + readFile: (targetPath) => { + const content = files.get(targetPath); + if (!content) { + throw new Error(`missing ${targetPath}`); + } + return content; + }, + resolver: () => + "/Users/huntharo/.openclaw/extensions/openclaw-codex-app-server/node_modules/openclaw/dist/index.js", + }), + ).toThrow("Unable to resolve a trusted host OpenClaw installation"); + }); + it("rethrows non-resolution failures from the public import", async () => { const importer = vi.fn(async (_specifier: string) => { throw new Error("boom"); diff --git a/src/openclaw-sdk-compat.ts b/src/openclaw-sdk-compat.ts index f14c1c1..20f2340 100644 --- a/src/openclaw-sdk-compat.ts +++ b/src/openclaw-sdk-compat.ts @@ -1,9 +1,10 @@ import { existsSync, readFileSync } from "node:fs"; import { createRequire } from "node:module"; import path from "node:path"; -import { pathToFileURL } from "node:url"; +import { fileURLToPath, pathToFileURL } from "node:url"; const require = createRequire(import.meta.url); +const THIS_PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); export type PluginSdkCompatLogger = { debug?: (message: string) => void; @@ -113,19 +114,36 @@ function resolveTrustedOpenClawRootFromStart( return null; } +function isDisallowedResolvedOpenClawRoot(packageRoot: string): boolean { + const normalizedRoot = path.resolve(packageRoot); + if ( + normalizedRoot === THIS_PACKAGE_ROOT || + normalizedRoot.startsWith(`${THIS_PACKAGE_ROOT}${path.sep}`) + ) { + return true; + } + return normalizedRoot.includes(`${path.sep}.openclaw${path.sep}extensions${path.sep}`); +} + export function resolveOpenClawEntrypointPath(params?: { resolver?: CompatResolver; pathExists?: CompatPathExists; readFile?: CompatReadFile; argv1?: string; cwd?: string; + mainFilename?: string; }): string { const resolver = params?.resolver ?? ((specifier: string) => require.resolve(specifier)); const pathExists = params?.pathExists ?? existsSync; const readFile = params?.readFile ?? ((targetPath: string) => readFileSync(targetPath, "utf-8")); const hostRoot = resolveTrustedOpenClawRootFromStart(params?.argv1 ?? process.argv[1], pathExists, readFile) ?? - resolveTrustedOpenClawRootFromStart(params?.cwd ?? process.cwd(), pathExists, readFile); + resolveTrustedOpenClawRootFromStart(params?.cwd ?? process.cwd(), pathExists, readFile) ?? + resolveTrustedOpenClawRootFromStart( + params?.mainFilename ?? require.main?.filename, + pathExists, + readFile, + ); if (hostRoot) { const distEntrypoint = path.join(hostRoot, "dist", "index.js"); if (pathExists(distEntrypoint)) { @@ -133,7 +151,18 @@ export function resolveOpenClawEntrypointPath(params?: { } return path.join(hostRoot, "src", "index.ts"); } - return resolver("openclaw"); + const resolvedEntrypoint = resolver("openclaw"); + const resolvedRoot = resolveTrustedOpenClawRootFromStart(resolvedEntrypoint, pathExists, readFile); + if (!resolvedRoot || isDisallowedResolvedOpenClawRoot(resolvedRoot)) { + throw new Error( + `Unable to resolve a trusted host OpenClaw installation from ${resolvedEntrypoint}`, + ); + } + const distEntrypoint = path.join(resolvedRoot, "dist", "index.js"); + if (pathExists(distEntrypoint)) { + return distEntrypoint; + } + return path.join(resolvedRoot, "src", "index.ts"); } export async function loadOpenClawCompatModule(params: { @@ -144,6 +173,10 @@ export async function loadOpenClawCompatModule(params: { importer?: CompatImporter; resolver?: CompatResolver; pathExists?: CompatPathExists; + readFile?: CompatReadFile; + argv1?: string; + cwd?: string; + mainFilename?: string; cache?: Map>; }): Promise { const cache = params.cache ?? compatModuleCache; @@ -167,6 +200,10 @@ export async function loadOpenClawCompatModule(params: { const openClawEntrypointPath = resolveOpenClawEntrypointPath({ resolver: params.resolver, pathExists, + readFile: params.readFile, + argv1: params.argv1, + cwd: params.cwd, + mainFilename: params.mainFilename, }); const fallbackPath = resolveCompatFallbackPath( openClawEntrypointPath,