From 5ca8f5ccd7d8b8dd0ccd2aaddac49b063f8a7562 Mon Sep 17 00:00:00 2001 From: jesse-merhi <79823012+jesse-merhi@users.noreply.github.com> Date: Thu, 13 Aug 2026 03:59:23 +1000 Subject: [PATCH] fix(plugins): preserve archive publication authority --- src/plugins/clawhub.test.ts | 15 ++++++ src/plugins/install-managed-npm.ts | 1 - src/plugins/install-package.ts | 8 +-- src/plugins/install.test.ts | 86 ++++++++++++++++++++++++++++++ 4 files changed, 102 insertions(+), 8 deletions(-) diff --git a/src/plugins/clawhub.test.ts b/src/plugins/clawhub.test.ts index 65ec66851563..fba8a84362ac 100644 --- a/src/plugins/clawhub.test.ts +++ b/src/plugins/clawhub.test.ts @@ -279,6 +279,7 @@ type ArchiveInstallCall = { dangerouslyForceUnsafeInstall?: boolean; expectedPluginId?: string; onInstallPolicyWarning?: unknown; + publicationAuthority?: unknown; installPolicyRequest?: { kind?: string; requestedSpecifier?: string; @@ -1784,6 +1785,20 @@ describe("installPluginFromClawHub", () => { expect(archiveInstallCall().onInstallPolicyWarning).toBe(onInstallPolicyWarning); }); + it("passes install publication authority through to archive installs", async () => { + const publicationAuthority = { + assertCurrent: vi.fn(), + commit: vi.fn(), + }; + + await installPluginFromClawHub({ + spec: "clawhub:demo", + publicationAuthority, + }); + + expect(archiveInstallCall().publicationAuthority).toBe(publicationAuthority); + }); + it("preserves the reviewed policy spec when retrying a pinned ClawHub version", async () => { await installPluginFromClawHub({ spec: "clawhub:demo@2026.3.22", diff --git a/src/plugins/install-managed-npm.ts b/src/plugins/install-managed-npm.ts index ab6cef6b70f5..c84d27906265 100644 --- a/src/plugins/install-managed-npm.ts +++ b/src/plugins/install-managed-npm.ts @@ -586,7 +586,6 @@ export async function installPluginFromManagedNpmRoot( } } const result = await installPluginFromInstalledPackageDir({ - publicationAuthority: params.publicationAuthority, dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall, onInstallPolicyWarning: params.onInstallPolicyWarning, config: params.config, diff --git a/src/plugins/install-package.ts b/src/plugins/install-package.ts index 5fd6b01773d2..3896f1f9fbd8 100644 --- a/src/plugins/install-package.ts +++ b/src/plugins/install-package.ts @@ -386,16 +386,10 @@ export async function installPluginFromArchive( await installPluginFromSourceDir({ sourceDir, ...pickPackageInstallCommonParams({ - dangerouslyForceUnsafeInstall: params.dangerouslyForceUnsafeInstall, - onInstallPolicyWarning: params.onInstallPolicyWarning, - extensionsDir: params.extensionsDir, + ...params, timeoutMs, logger, mode, - dryRun: params.dryRun, - config: params.config, - expectedPluginId: params.expectedPluginId, - trustedSourceLinkedOfficialInstall: params.trustedSourceLinkedOfficialInstall, requirePluginManifest: true, installPolicyRequest, onEffectiveMode: (resolvedMode) => { diff --git a/src/plugins/install.test.ts b/src/plugins/install.test.ts index fe0318425b8c..094753efdd3d 100644 --- a/src/plugins/install.test.ts +++ b/src/plugins/install.test.ts @@ -1040,6 +1040,92 @@ describe("installPluginFromArchive", () => { expect(updatedVersion).toBe("0.0.2"); }); + it("does not publish a new archive install after its approval expires", async () => { + const stateDir = suiteTempRootTracker.makeTempDir(); + const extensionsDir = path.join(stateDir, "extensions"); + const archivePath = await ensureDynamicArchiveTemplate({ + outName: "archive-publication-authority.tgz", + packageJson: { + name: "archive-publication-authority", + version: "1.0.0", + openclaw: { extensions: ["./dist/index.js"] }, + }, + withDistIndex: true, + }); + const commit = vi.fn(() => { + throw new Error("approval expired"); + }); + + const result = await installPluginFromArchive({ + archivePath, + extensionsDir, + publicationAuthority: { assertCurrent: vi.fn(), commit }, + }); + + expect(result).toEqual({ + ok: false, + error: "failed to copy plugin: Error: approval expired", + }); + expect(commit).toHaveBeenCalledOnce(); + expect( + fs.existsSync(resolvePluginInstallDir("archive-publication-authority", extensionsDir)), + ).toBe(false); + }); + + it("restores an archive update when its approval expires during publication", async () => { + const stateDir = suiteTempRootTracker.makeTempDir(); + const extensionsDir = path.join(stateDir, "extensions"); + const archiveV1 = await ensureDynamicArchiveTemplate({ + outName: "archive-publication-update-1.tgz", + packageJson: { + name: "archive-publication-update", + version: "1.0.0", + openclaw: { extensions: ["./dist/index.js"] }, + }, + withDistIndex: true, + }); + const archiveV2 = await ensureDynamicArchiveTemplate({ + outName: "archive-publication-update-2.tgz", + packageJson: { + name: "archive-publication-update", + version: "2.0.0", + openclaw: { extensions: ["./dist/index.js"] }, + }, + withDistIndex: true, + }); + const first = await installPluginFromArchive({ archivePath: archiveV1, extensionsDir }); + expect(first.ok).toBe(true); + if (!first.ok) { + return; + } + let approvalCurrent = true; + const assertCurrent = vi.fn(() => { + approvalCurrent = false; + }); + const commit = vi.fn(() => { + if (!approvalCurrent) { + throw new Error("approval expired"); + } + }); + + const update = await installPluginFromArchive({ + archivePath: archiveV2, + extensionsDir, + mode: "update", + publicationAuthority: { assertCurrent, commit }, + }); + + expect(update).toEqual({ + ok: false, + error: "failed to copy plugin: Error: approval expired", + }); + expect(assertCurrent).toHaveBeenCalledOnce(); + expect(commit).toHaveBeenCalledOnce(); + expect( + JSON.parse(fs.readFileSync(path.join(first.targetDir, "package.json"), "utf8")), + ).toMatchObject({ version: "1.0.0" }); + }); + it("emits effective install mode when requested archive update creates a new target", async () => { const stateDir = suiteTempRootTracker.makeTempDir(); const extensionsDir = path.join(stateDir, "extensions");