diff --git a/CHANGELOG.md b/CHANGELOG.md index 60ec3103..3b3c06bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### Fixes +- API: keep publish-time Plugin Inspector target preparation inside its disposable workspace when hosted runtimes expose an unusable home directory. - API: keep older code-plugin and Claw backports from replacing the highest-semver `latest` release while preserving custom distribution tags. - Integrations: truncate publisher-controlled Discord webhook titles to the platform's 256-character embed limit. - Security: recover scheduled temporal publisher-abuse scans from strict Convex payload validation failures without leaving zombie running runs (thanks @jesse-merhi). diff --git a/convex/openclaw-plugin-inspector.d.ts b/convex/openclaw-plugin-inspector.d.ts index 59c63806..7abe5a7f 100644 --- a/convex/openclaw-plugin-inspector.d.ts +++ b/convex/openclaw-plugin-inspector.d.ts @@ -26,9 +26,15 @@ declare module "@openclaw/plugin-inspector" { allowExecution?: boolean; configPath?: string; generatedAt?: string; + targetOpenClaw?: unknown; }): Promise<{ report: PluginInspectorReport; paths: PluginInspectorPaths }>; }; + export const openClawTargets: { + resolveVersion(requestedVersion: string): Promise; + prepare(resolvedTarget: unknown, options?: { cacheDir?: string }): Promise; + }; + export const reports: { renderTextSummary(report: PluginInspectorReport, options?: Record): string; sanitizeArtifact(report: PluginInspectorReport): unknown; diff --git a/convex/packageInspectorNode.test.ts b/convex/packageInspectorNode.test.ts index cf726724..e3f70761 100644 --- a/convex/packageInspectorNode.test.ts +++ b/convex/packageInspectorNode.test.ts @@ -1,11 +1,12 @@ /* @vitest-environment node */ import path from "node:path"; -import { afterEach, describe, expect, it } from "vitest"; +import { afterEach, describe, expect, it, vi } from "vitest"; import { buildPublishInspectorRunCheckOptions, createPackageInspectorWorkspace, normalizeInspectorReportForPublish, + preparePublishInspectorOpenClawTarget, } from "./packageInspectorNode"; const originalPlatform = process.platform; @@ -60,12 +61,37 @@ describe("package inspector publish normalization", () => { ); }); - it("targets latest stable OpenClaw for publish-time inspection", () => { - expect(buildPublishInspectorRunCheckOptions("/tmp/plugin", "2026-07-30T00:00:00.000Z")).toEqual( + it("prepares latest stable OpenClaw with a cache outside the inspected package", async () => { + const resolvedTarget = { version: "2026.7.0" }; + const preparedTarget = { status: "ok", version: "2026.7.0" }; + const resolveVersion = vi.fn(async () => resolvedTarget); + const prepare = vi.fn(async () => preparedTarget); + + await expect( + preparePublishInspectorOpenClawTarget("/tmp/plugin", { resolveVersion, prepare }), + ).resolves.toBe(preparedTarget); + expect(resolveVersion).toHaveBeenCalledWith("latest"); + expect(prepare).toHaveBeenCalledWith(resolvedTarget, { + cacheDir: path.join("/tmp/plugin", ".plugin-inspector-cache"), + }); + expect(path.join("/tmp/plugin", ".plugin-inspector-cache")).not.toContain( + path.join("/tmp/plugin", "package") + path.sep, + ); + }); + + it("uses the prepared OpenClaw target for publish-time inspection", () => { + const targetOpenClaw = { status: "ok", version: "2026.7.0" }; + expect( + buildPublishInspectorRunCheckOptions( + "/tmp/plugin", + "2026-07-30T00:00:00.000Z", + targetOpenClaw, + ), + ).toEqual( expect.objectContaining({ pluginRoot: "/tmp/plugin", openclawPath: false, - openclawVersion: "latest", + targetOpenClaw, authorFacing: true, }), ); diff --git a/convex/packageInspectorNode.ts b/convex/packageInspectorNode.ts index ce0b16a2..a7981b4b 100644 --- a/convex/packageInspectorNode.ts +++ b/convex/packageInspectorNode.ts @@ -130,11 +130,32 @@ const inspectorMetadataValidator = v.object({ targetOpenClawVersion: v.optional(v.string()), }); -export function buildPublishInspectorRunCheckOptions(root: string, generatedAt: string) { +export async function preparePublishInspectorOpenClawTarget( + root: string, + targets: { + resolveVersion: (requestedVersion: string) => Promise; + prepare: ( + resolvedTarget: ResolvedTarget, + options: { cacheDir: string }, + ) => Promise; + }, +) { + const resolvedTarget = await targets.resolveVersion("latest"); + return await targets.prepare(resolvedTarget, { + // The dependency defaults to os.homedir(), which can be unusable in serverless runtimes. + cacheDir: path.join(root, ".plugin-inspector-cache"), + }); +} + +export function buildPublishInspectorRunCheckOptions( + root: string, + generatedAt: string, + targetOpenClaw: unknown, +) { return { pluginRoot: root, openclawPath: false, - openclawVersion: "latest", + targetOpenClaw, outDir: "reports", capture: false, mockSdk: true, @@ -187,9 +208,11 @@ export const runPackageInspectorForPublishInternal = internalAction({ metadata: inspectorMetadataValidator, }), handler: async (ctx, args) => { - let root: string | undefined; + let workspaceRoot: string | undefined; try { - root = await createPackageInspectorWorkspace(); + workspaceRoot = await createPackageInspectorWorkspace(); + const root = path.join(workspaceRoot, "package"); + await mkdir(root, { recursive: true }); for (const file of args.files) { const blob = await ctx.storage.get(file.storageId); if (!blob) { @@ -201,8 +224,16 @@ export const runPackageInspectorForPublishInternal = internalAction({ } await writeSyntheticInspectorConfigIfNeeded(root, args.files, args.packageName); - const { pluginRoot } = await import("@openclaw/plugin-inspector"); - const runCheckOptions = buildPublishInspectorRunCheckOptions(root, new Date().toISOString()); + const { openClawTargets, pluginRoot } = await import("@openclaw/plugin-inspector"); + const targetOpenClaw = await preparePublishInspectorOpenClawTarget( + workspaceRoot, + openClawTargets, + ); + const runCheckOptions = buildPublishInspectorRunCheckOptions( + root, + new Date().toISOString(), + targetOpenClaw, + ); const { report } = await pluginRoot.runCheck(runCheckOptions); return normalizeInspectorReportForPublish(report); @@ -230,8 +261,8 @@ export const runPackageInspectorForPublishInternal = internalAction({ }, }; } finally { - if (root) { - await rm(root, { recursive: true, force: true }); + if (workspaceRoot) { + await rm(workspaceRoot, { recursive: true, force: true }); } } },