mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(plugins): bind warning approvals to stable inputs
This commit is contained in:
@@ -48,7 +48,7 @@ describe("resolveInstallPolicyWarningAcknowledgementCliOptions", () => {
|
||||
targetName: "demo\npkg",
|
||||
targetType: "plugin",
|
||||
requestMode: fixture.requestMode,
|
||||
warningFingerprint: "review-required",
|
||||
approvalFingerprint: "review-required",
|
||||
scan: {
|
||||
requestKind: "plugin-npm",
|
||||
originType: "plugin-npm",
|
||||
@@ -79,7 +79,7 @@ describe("resolveInstallPolicyWarningAcknowledgementCliOptions", () => {
|
||||
targetName: "demo",
|
||||
targetType: "skill",
|
||||
requestMode: "install",
|
||||
warningFingerprint: "review-required",
|
||||
approvalFingerprint: "review-required",
|
||||
scan: {
|
||||
requestKind: "skill-install",
|
||||
originType: "skill-managed-install",
|
||||
@@ -127,7 +127,7 @@ describe("resolveInstallPolicyWarningAcknowledgementCliOptions", () => {
|
||||
targetName: "demo",
|
||||
targetType: "plugin",
|
||||
requestMode: "install",
|
||||
warningFingerprint: "review-required",
|
||||
approvalFingerprint: "review-required",
|
||||
scan: {
|
||||
requestKind: "plugin-npm",
|
||||
originType: "plugin-npm",
|
||||
|
||||
@@ -50,7 +50,7 @@ function warningOccurrence(
|
||||
warning: InstallPolicyWarningDetails,
|
||||
scan: InstallPolicyWarningScanIdentity = packageScan,
|
||||
): InstallPolicyWarningOccurrence {
|
||||
return { scan, warning, warningFingerprint: `fingerprint:${warning.reason}` };
|
||||
return { scan, warning, approvalFingerprint: `fingerprint:${warning.reason}` };
|
||||
}
|
||||
|
||||
async function callHandler(
|
||||
@@ -435,7 +435,7 @@ describe("plugin management Gateway handlers", () => {
|
||||
warnings: [
|
||||
{
|
||||
scan: packageScan,
|
||||
warningFingerprint: "fingerprint:Scanner found behavior that needs review",
|
||||
approvalFingerprint: "fingerprint:Scanner found behavior that needs review",
|
||||
warning: {
|
||||
targetName: "demo-plugin",
|
||||
targetType: "plugin",
|
||||
@@ -485,7 +485,7 @@ describe("plugin management Gateway handlers", () => {
|
||||
warnings: [
|
||||
{
|
||||
scan: packageScan,
|
||||
warningFingerprint: "fingerprint:Scanner found a different issue",
|
||||
approvalFingerprint: "fingerprint:Scanner found a different issue",
|
||||
warning: {
|
||||
targetName: "demo-plugin",
|
||||
targetType: "plugin",
|
||||
@@ -591,7 +591,7 @@ describe("plugin management Gateway handlers", () => {
|
||||
"scan",
|
||||
);
|
||||
expect((first.error as { details?: Record<string, unknown> }).details).not.toHaveProperty(
|
||||
"warningFingerprint",
|
||||
"approvalFingerprint",
|
||||
);
|
||||
|
||||
managementMocks.install.mockRejectedValueOnce(
|
||||
@@ -616,7 +616,7 @@ describe("plugin management Gateway handlers", () => {
|
||||
"scan",
|
||||
);
|
||||
expect((second.error as { details?: Record<string, unknown> }).details).not.toHaveProperty(
|
||||
"warningFingerprint",
|
||||
"approvalFingerprint",
|
||||
);
|
||||
|
||||
managementMocks.install.mockResolvedValueOnce({
|
||||
|
||||
@@ -1784,6 +1784,15 @@ describe("installPluginFromClawHub", () => {
|
||||
expect(archiveInstallCall().onInstallPolicyWarning).toBe(onInstallPolicyWarning);
|
||||
});
|
||||
|
||||
it("preserves the reviewed policy spec when retrying a pinned ClawHub version", async () => {
|
||||
await installPluginFromClawHub({
|
||||
spec: "clawhub:demo@2026.3.22",
|
||||
installPolicyRequestedSpecifier: "clawhub:demo",
|
||||
});
|
||||
|
||||
expect(archiveInstallCall().installPolicyRequest?.requestedSpecifier).toBe("clawhub:demo");
|
||||
});
|
||||
|
||||
it("cleans up the downloaded archive even when archive install fails", async () => {
|
||||
installPluginFromArchiveMock.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
|
||||
@@ -1218,6 +1218,7 @@ export async function installPluginFromClawHub(
|
||||
dryRun?: boolean;
|
||||
expectedPluginId?: string;
|
||||
expectedIntegrity?: string;
|
||||
installPolicyRequestedSpecifier?: string;
|
||||
env?: RuntimeVersionEnv;
|
||||
acknowledgeClawHubRisk?: boolean;
|
||||
onClawHubRisk?: (request: ClawHubRiskAcknowledgementRequest) => boolean | Promise<boolean>;
|
||||
@@ -1457,7 +1458,7 @@ export async function installPluginFromClawHub(
|
||||
expectedPluginId: runtimeIdResolution.expectedPluginId,
|
||||
installPolicyRequest: {
|
||||
kind: "plugin-archive",
|
||||
requestedSpecifier: params.spec,
|
||||
requestedSpecifier: params.installPolicyRequestedSpecifier ?? params.spec,
|
||||
source: {
|
||||
kind: "clawhub",
|
||||
authority: officialClawHubPackage ? "official" : clawhubAuthority,
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import fs from "node:fs/promises";
|
||||
import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
type ResolveNpmSpecMetadata =
|
||||
typeof import("../infra/install-source-utils.js").resolveNpmSpecMetadata;
|
||||
type PreflightPluginNpmInstallPolicy =
|
||||
typeof import("./install-security-scan.js").preflightPluginNpmInstallPolicy;
|
||||
|
||||
const resolveNpmSpecMetadataMock = vi.fn<ResolveNpmSpecMetadata>();
|
||||
const preflightPluginNpmInstallPolicyMock = vi.fn<PreflightPluginNpmInstallPolicy>();
|
||||
|
||||
vi.mock("../infra/install-source-utils.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("../infra/install-source-utils.js")>();
|
||||
return { ...actual, resolveNpmSpecMetadata: resolveNpmSpecMetadataMock };
|
||||
});
|
||||
|
||||
vi.mock("./install-security-scan.js", async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import("./install-security-scan.js")>();
|
||||
return {
|
||||
...actual,
|
||||
preflightPluginNpmInstallPolicy: preflightPluginNpmInstallPolicyMock,
|
||||
};
|
||||
});
|
||||
|
||||
const { installPluginFromNpmSpec } = await import("./install-npm.js");
|
||||
|
||||
describe("npm install policy metadata", () => {
|
||||
beforeEach(() => {
|
||||
resolveNpmSpecMetadataMock.mockReset();
|
||||
preflightPluginNpmInstallPolicyMock.mockReset();
|
||||
resolveNpmSpecMetadataMock.mockResolvedValue({
|
||||
ok: true,
|
||||
metadata: {
|
||||
name: "@openclaw/demo",
|
||||
version: "1.2.3",
|
||||
resolvedSpec: "@openclaw/demo@1.2.3",
|
||||
integrity: "sha512-reviewed",
|
||||
shasum: "reviewed",
|
||||
packageOpenClaw: { extensions: ["./index.js"] },
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps policy metadata stable when a retry pins the resolved npm spec", async () => {
|
||||
const policyInputs: Array<{
|
||||
requestedSpecifier: string;
|
||||
metadata: Record<string, unknown>;
|
||||
}> = [];
|
||||
preflightPluginNpmInstallPolicyMock.mockImplementation(async (params) => {
|
||||
policyInputs.push({
|
||||
requestedSpecifier: params.requestedSpecifier ?? "",
|
||||
metadata: JSON.parse(await fs.readFile(params.sourcePath, "utf8")) as Record<
|
||||
string,
|
||||
unknown
|
||||
>,
|
||||
});
|
||||
return { blocked: { code: "security_scan_blocked", reason: "test stop" } };
|
||||
});
|
||||
|
||||
const requestedSpecifier = "@openclaw/demo@latest";
|
||||
const first = await installPluginFromNpmSpec({ spec: requestedSpecifier });
|
||||
const retry = await installPluginFromNpmSpec({
|
||||
spec: "@openclaw/demo@1.2.3",
|
||||
installPolicyRequestedSpecifier: requestedSpecifier,
|
||||
expectedIntegrity: "sha512-reviewed",
|
||||
});
|
||||
|
||||
expect(first.ok).toBe(false);
|
||||
expect(retry.ok).toBe(false);
|
||||
expect(policyInputs).toHaveLength(2);
|
||||
expect(policyInputs[0]).toMatchObject({
|
||||
requestedSpecifier,
|
||||
metadata: {
|
||||
packageName: "@openclaw/demo",
|
||||
requestedSpecifier,
|
||||
resolution: {
|
||||
resolvedSpec: "@openclaw/demo@1.2.3",
|
||||
integrity: "sha512-reviewed",
|
||||
},
|
||||
},
|
||||
});
|
||||
expect(policyInputs[1]).toEqual(policyInputs[0]);
|
||||
expect(policyInputs[0]).not.toHaveProperty("metadata.resolution.resolvedAt");
|
||||
});
|
||||
});
|
||||
@@ -56,6 +56,7 @@ export async function installPluginFromNpmSpec(
|
||||
expectedPluginId?: string;
|
||||
expectedReplacementPluginId?: string;
|
||||
expectedIntegrity?: string;
|
||||
installPolicyRequestedSpecifier?: string;
|
||||
onIntegrityDrift?: (params: PluginNpmIntegrityDriftParams) => boolean | Promise<boolean>;
|
||||
},
|
||||
): Promise<InstallPluginResult> {
|
||||
@@ -66,6 +67,7 @@ export async function installPluginFromNpmSpec(
|
||||
);
|
||||
const expectedPluginId = params.expectedPluginId;
|
||||
const spec = params.spec.trim();
|
||||
const installPolicyRequestedSpecifier = params.installPolicyRequestedSpecifier?.trim() || spec;
|
||||
const specError = runtime.validateRegistryNpmSpec(spec);
|
||||
if (specError) {
|
||||
return {
|
||||
@@ -211,14 +213,16 @@ export async function installPluginFromNpmSpec(
|
||||
|
||||
const policyTempDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-npm-policy-"));
|
||||
try {
|
||||
const policyNpmResolution = { ...npmResolution };
|
||||
delete policyNpmResolution.resolvedAt;
|
||||
const policyMetadataPath = path.join(policyTempDir, "npm-package-metadata.json");
|
||||
await fs.writeFile(
|
||||
policyMetadataPath,
|
||||
`${JSON.stringify(
|
||||
{
|
||||
packageName: parsedSpec.name,
|
||||
requestedSpecifier: spec,
|
||||
resolution: npmResolution,
|
||||
requestedSpecifier: installPolicyRequestedSpecifier,
|
||||
resolution: policyNpmResolution,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
@@ -239,7 +243,7 @@ export async function installPluginFromNpmSpec(
|
||||
mode: policyMode,
|
||||
packageName: parsedSpec.name,
|
||||
...(expectedPluginId ? { pluginId: expectedPluginId } : {}),
|
||||
requestedSpecifier: spec,
|
||||
requestedSpecifier: installPolicyRequestedSpecifier,
|
||||
source: npmInstallPolicySource,
|
||||
sourcePath: policyMetadataPath,
|
||||
sourcePathKind: "file",
|
||||
@@ -265,7 +269,7 @@ export async function installPluginFromNpmSpec(
|
||||
displaySpec: spec,
|
||||
installPolicyRequest: {
|
||||
kind: "plugin-npm",
|
||||
requestedSpecifier: spec,
|
||||
requestedSpecifier: installPolicyRequestedSpecifier,
|
||||
source: npmInstallPolicySource,
|
||||
},
|
||||
extensionsDir: params.extensionsDir,
|
||||
|
||||
@@ -3,7 +3,9 @@ import path from "node:path";
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
||||
import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js";
|
||||
|
||||
const runInstallPolicyMock = vi.fn();
|
||||
type RunInstallPolicy = typeof import("../security/install-policy.js").runInstallPolicy;
|
||||
|
||||
const runInstallPolicyMock = vi.fn<RunInstallPolicy>();
|
||||
const getGlobalHookRunnerMock = vi.fn();
|
||||
|
||||
vi.mock("../security/install-policy.js", async (importOriginal) => {
|
||||
@@ -326,10 +328,18 @@ describe("legacy file install scan compatibility", () => {
|
||||
const onInstallPolicyWarning = vi.fn().mockResolvedValue({ status: "approved" });
|
||||
runInstallPolicyMock
|
||||
.mockResolvedValueOnce({
|
||||
warning: { reason: "review this plugin", fingerprint: "warning-a" },
|
||||
warning: {
|
||||
reason: "review this plugin",
|
||||
fingerprint: "warning-a",
|
||||
approvalFingerprint: "approval-a",
|
||||
},
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
warning: { reason: "review this plugin", fingerprint: "warning-a" },
|
||||
warning: {
|
||||
reason: "review this plugin",
|
||||
fingerprint: "warning-a",
|
||||
approvalFingerprint: "approval-a",
|
||||
},
|
||||
});
|
||||
|
||||
const result = await scanFileInstallSourceRuntime({
|
||||
@@ -344,7 +354,7 @@ describe("legacy file install scan compatibility", () => {
|
||||
targetName: "payload",
|
||||
targetType: "plugin",
|
||||
requestMode: "install",
|
||||
warningFingerprint: "warning-a",
|
||||
approvalFingerprint: "approval-a",
|
||||
scan: {
|
||||
requestKind: "plugin-file",
|
||||
originType: "plugin-file",
|
||||
@@ -365,10 +375,18 @@ describe("legacy file install scan compatibility", () => {
|
||||
const onInstallPolicyWarning = vi.fn().mockResolvedValue({ status: "approved" });
|
||||
runInstallPolicyMock
|
||||
.mockResolvedValueOnce({
|
||||
warning: { reason: "review this plugin", fingerprint: "warning-a" },
|
||||
warning: {
|
||||
reason: "review this plugin",
|
||||
fingerprint: "warning-a",
|
||||
approvalFingerprint: "approval-a",
|
||||
},
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
warning: { reason: "review the new finding", fingerprint: "warning-b" },
|
||||
warning: {
|
||||
reason: "review the new finding",
|
||||
fingerprint: "warning-b",
|
||||
approvalFingerprint: "approval-a",
|
||||
},
|
||||
findings: [
|
||||
{
|
||||
ruleId: "changed-warning",
|
||||
@@ -406,7 +424,7 @@ describe("legacy file install scan compatibility", () => {
|
||||
},
|
||||
],
|
||||
},
|
||||
warningFingerprint: "warning-b",
|
||||
approvalFingerprint: "approval-a",
|
||||
},
|
||||
});
|
||||
expect(result?.blocked?.reason).toContain("Reason: review the new finding");
|
||||
@@ -419,7 +437,11 @@ describe("legacy file install scan compatibility", () => {
|
||||
it("renders metadata changes that require approval again", async () => {
|
||||
const onInstallPolicyWarning = vi.fn().mockResolvedValue({ status: "approved" });
|
||||
const initialWarning = {
|
||||
warning: { reason: "same bounded reason", fingerprint: "full-warning-a" },
|
||||
warning: {
|
||||
reason: "same bounded reason",
|
||||
fingerprint: "full-warning-a",
|
||||
approvalFingerprint: "approval-a",
|
||||
},
|
||||
findings: [
|
||||
{
|
||||
ruleId: "initial-rule",
|
||||
@@ -430,7 +452,11 @@ describe("legacy file install scan compatibility", () => {
|
||||
],
|
||||
};
|
||||
runInstallPolicyMock.mockResolvedValueOnce(initialWarning).mockResolvedValueOnce({
|
||||
warning: { ...initialWarning.warning, fingerprint: "full-warning-b" },
|
||||
warning: {
|
||||
...initialWarning.warning,
|
||||
fingerprint: "full-warning-b",
|
||||
approvalFingerprint: "approval-b",
|
||||
},
|
||||
findings: [
|
||||
{
|
||||
ruleId: "refreshed-rule",
|
||||
@@ -455,7 +481,7 @@ describe("legacy file install scan compatibility", () => {
|
||||
);
|
||||
expect(result?.blocked?.reason).not.toContain("initial-rule");
|
||||
expect(result?.blocked?.reason).not.toContain("initial evidence");
|
||||
expect(result?.blocked?.installPolicyWarning?.warningFingerprint).toBe("full-warning-b");
|
||||
expect(result?.blocked?.installPolicyWarning?.approvalFingerprint).toBe("approval-b");
|
||||
expect(onInstallPolicyWarning).toHaveBeenCalledTimes(1);
|
||||
expect(runInstallPolicyMock).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
@@ -463,7 +489,11 @@ describe("legacy file install scan compatibility", () => {
|
||||
it("keeps a block from policy re-evaluation terminal", async () => {
|
||||
runInstallPolicyMock
|
||||
.mockResolvedValueOnce({
|
||||
warning: { reason: "review this plugin", fingerprint: "warning-a" },
|
||||
warning: {
|
||||
reason: "review this plugin",
|
||||
fingerprint: "warning-a",
|
||||
approvalFingerprint: "approval-a",
|
||||
},
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
blocked: { code: "security_scan_blocked", reason: "now blocked" },
|
||||
@@ -481,7 +511,11 @@ describe("legacy file install scan compatibility", () => {
|
||||
|
||||
it("keeps the deprecated unsafe flag inert when policy warns", async () => {
|
||||
runInstallPolicyMock.mockResolvedValue({
|
||||
warning: { reason: "review this plugin", fingerprint: "warning-a" },
|
||||
warning: {
|
||||
reason: "review this plugin",
|
||||
fingerprint: "warning-a",
|
||||
approvalFingerprint: "approval-a",
|
||||
},
|
||||
});
|
||||
|
||||
const result = await scanFileInstallSourceRuntime({
|
||||
@@ -505,7 +539,7 @@ describe("legacy file install scan compatibility", () => {
|
||||
requestMode: "install",
|
||||
reason: "review this plugin",
|
||||
},
|
||||
warningFingerprint: "warning-a",
|
||||
approvalFingerprint: "approval-a",
|
||||
},
|
||||
reason: expectedInstallPolicyNotice({
|
||||
decision: "warn",
|
||||
@@ -528,7 +562,11 @@ describe("legacy file install scan compatibility", () => {
|
||||
it("keeps a block from acknowledged policy re-evaluation terminal", async () => {
|
||||
runInstallPolicyMock
|
||||
.mockResolvedValueOnce({
|
||||
warning: { reason: "review this plugin", fingerprint: "warning-a" },
|
||||
warning: {
|
||||
reason: "review this plugin",
|
||||
fingerprint: "warning-a",
|
||||
approvalFingerprint: "approval-a",
|
||||
},
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
blocked: { code: "security_scan_blocked", reason: "now blocked" },
|
||||
@@ -547,7 +585,11 @@ describe("legacy file install scan compatibility", () => {
|
||||
|
||||
it("distinguishes an exhausted noninteractive approval from cancellation", async () => {
|
||||
runInstallPolicyMock.mockResolvedValue({
|
||||
warning: { reason: "review the dependency warning", fingerprint: "warning-a" },
|
||||
warning: {
|
||||
reason: "review the dependency warning",
|
||||
fingerprint: "warning-a",
|
||||
approvalFingerprint: "approval-a",
|
||||
},
|
||||
findings: [
|
||||
{
|
||||
ruleId: "dependency-warning",
|
||||
@@ -584,7 +626,7 @@ describe("legacy file install scan compatibility", () => {
|
||||
requestMode: "install",
|
||||
reason: "review the dependency warning",
|
||||
},
|
||||
warningFingerprint: "warning-a",
|
||||
approvalFingerprint: "approval-a",
|
||||
});
|
||||
expect(runInstallPolicyMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
@@ -592,7 +634,11 @@ describe("legacy file install scan compatibility", () => {
|
||||
it("renders warning details as one readable review notice", async () => {
|
||||
const warnings: string[] = [];
|
||||
runInstallPolicyMock.mockResolvedValue({
|
||||
warning: { reason: "review this plugin", fingerprint: "warning-a" },
|
||||
warning: {
|
||||
reason: "review this plugin",
|
||||
fingerprint: "warning-a",
|
||||
approvalFingerprint: "approval-a",
|
||||
},
|
||||
findings: [{ ruleId: "context", severity: "info", message: "Informational context." }],
|
||||
});
|
||||
|
||||
@@ -616,7 +662,7 @@ describe("legacy file install scan compatibility", () => {
|
||||
reason: "review this plugin",
|
||||
findings: [{ ruleId: "context", severity: "info", message: "Informational context." }],
|
||||
},
|
||||
warningFingerprint: "warning-a",
|
||||
approvalFingerprint: "approval-a",
|
||||
});
|
||||
expect(warnings).toEqual([
|
||||
`${expectedInstallPolicyNotice({
|
||||
|
||||
@@ -841,7 +841,7 @@ async function runOperatorInstallPolicy(params: {
|
||||
...(params.skill ? { skillInstallId: params.skill.installId } : {}),
|
||||
},
|
||||
warning: installPolicyWarning,
|
||||
warningFingerprint: result.warning.fingerprint,
|
||||
approvalFingerprint: result.warning.approvalFingerprint,
|
||||
};
|
||||
if (!params.onInstallPolicyWarning) {
|
||||
return {
|
||||
@@ -899,7 +899,7 @@ async function runOperatorInstallPolicy(params: {
|
||||
reason: reevaluated.warning.reason,
|
||||
...(reevaluated.findings?.length ? { findings: reevaluated.findings } : {}),
|
||||
},
|
||||
warningFingerprint: reevaluated.warning.fingerprint,
|
||||
approvalFingerprint: reevaluated.warning.approvalFingerprint,
|
||||
},
|
||||
reason: formatInstallPolicyNotice({
|
||||
decision: "warn",
|
||||
|
||||
@@ -21,7 +21,7 @@ type InstallPolicyWarningScanIdentity = {
|
||||
export type InstallPolicyWarningOccurrence = {
|
||||
scan: InstallPolicyWarningScanIdentity;
|
||||
warning: InstallPolicyWarningDetails;
|
||||
warningFingerprint: string;
|
||||
approvalFingerprint: string;
|
||||
};
|
||||
|
||||
export type InstallPolicyWarningAcknowledgementRequest = InstallPolicyWarningOccurrence & {
|
||||
@@ -30,7 +30,7 @@ export type InstallPolicyWarningAcknowledgementRequest = InstallPolicyWarningOcc
|
||||
requestMode: "install" | "update";
|
||||
};
|
||||
|
||||
type InstallPolicyWarningAcknowledgementResult =
|
||||
export type InstallPolicyWarningAcknowledgementResult =
|
||||
| { status: "approved" }
|
||||
| { status: "declined" }
|
||||
| {
|
||||
|
||||
@@ -17,7 +17,7 @@ describe("installPluginDirectoryIntoExtensions", () => {
|
||||
fs.mkdirSync(sourceDir, { recursive: true });
|
||||
fs.writeFileSync(path.join(sourceDir, "index.js"), "export default {};\n");
|
||||
const installPolicyWarning = {
|
||||
warningFingerprint: "review-installed-dependency-tree",
|
||||
approvalFingerprint: "review-installed-dependency-tree",
|
||||
scan: {
|
||||
requestKind: "plugin-archive" as const,
|
||||
originType: "plugin-dependency-tree",
|
||||
|
||||
@@ -207,7 +207,7 @@ describe("plugin management install-policy acknowledgements", () => {
|
||||
await expectOneShotInstallPolicyWarningAcknowledgement(mocks.clawhubInstall);
|
||||
});
|
||||
|
||||
it("acknowledges a reviewed warning only at the same scan stage and only once", async () => {
|
||||
it("acknowledges the same warning only for the same target and scan stage once", async () => {
|
||||
mocks.readConfig.mockResolvedValue(configSnapshot());
|
||||
mockHostedOfficialCatalog([hostedFeedDiffsEntry]);
|
||||
mockClawHubInstall("diffs", "@openclaw/diffs");
|
||||
@@ -220,7 +220,7 @@ describe("plugin management install-policy acknowledgements", () => {
|
||||
"first approved warning",
|
||||
);
|
||||
const dependencyWarning: InstallPolicyWarningOccurrence = {
|
||||
warningFingerprint: packageWarning.warningFingerprint,
|
||||
approvalFingerprint: packageWarning.approvalFingerprint,
|
||||
scan: {
|
||||
...packageWarning.scan,
|
||||
originType: "plugin-dependency-tree",
|
||||
@@ -230,7 +230,18 @@ describe("plugin management install-policy acknowledgements", () => {
|
||||
};
|
||||
const changedFingerprintWarning: InstallPolicyWarningOccurrence = {
|
||||
...packageWarning,
|
||||
warningFingerprint: "changed-full-warning",
|
||||
approvalFingerprint: "changed-warning-approval",
|
||||
};
|
||||
const wrongTargetWarning: InstallPolicyWarningOccurrence = {
|
||||
...packageWarning,
|
||||
warning: { ...packageWarning.warning, targetName: "other-plugin" },
|
||||
};
|
||||
const restagedPackageWarning: InstallPolicyWarningOccurrence = {
|
||||
...packageWarning,
|
||||
warning: {
|
||||
...packageWarning.warning,
|
||||
reason: "Review this warning in /tmp/openclaw-stage-b",
|
||||
},
|
||||
};
|
||||
|
||||
await installManagedPlugin({
|
||||
@@ -269,11 +280,19 @@ describe("plugin management install-policy acknowledgements", () => {
|
||||
...dependencyWarning,
|
||||
}),
|
||||
).toEqual({ status: "unavailable", reason: "warning-not-approved" });
|
||||
expect(
|
||||
await acknowledge({
|
||||
targetName: wrongTargetWarning.warning.targetName,
|
||||
targetType: wrongTargetWarning.warning.targetType,
|
||||
requestMode: wrongTargetWarning.warning.requestMode,
|
||||
...wrongTargetWarning,
|
||||
}),
|
||||
).toEqual({ status: "unavailable", reason: "warning-not-approved" });
|
||||
const packageRequest: InstallPolicyWarningAcknowledgementRequest = {
|
||||
targetName: packageWarning.warning.targetName,
|
||||
targetType: packageWarning.warning.targetType,
|
||||
requestMode: packageWarning.warning.requestMode,
|
||||
...packageWarning,
|
||||
targetName: restagedPackageWarning.warning.targetName,
|
||||
targetType: restagedPackageWarning.warning.targetType,
|
||||
requestMode: restagedPackageWarning.warning.requestMode,
|
||||
...restagedPackageWarning,
|
||||
};
|
||||
expect(await acknowledge(packageRequest)).toEqual({ status: "approved" });
|
||||
expect(await acknowledge(packageRequest)).toEqual({
|
||||
@@ -284,7 +303,7 @@ describe("plugin management install-policy acknowledgements", () => {
|
||||
|
||||
it("pins reviewed npm warnings to the first resolved version and integrity", async () => {
|
||||
const warning: InstallPolicyWarningOccurrence = {
|
||||
warningFingerprint: "review-npm-package",
|
||||
approvalFingerprint: "review-npm-package",
|
||||
scan: {
|
||||
requestKind: "plugin-npm",
|
||||
originType: "plugin-npm",
|
||||
@@ -343,6 +362,7 @@ describe("plugin management install-policy acknowledgements", () => {
|
||||
expect(resolvedRequest).toMatchObject({
|
||||
source: "official",
|
||||
spec: npmResolution.resolvedSpec,
|
||||
installPolicyRequestedSpecifier: "@openclaw/npm-demo",
|
||||
expectedIntegrity: npmResolution.integrity,
|
||||
});
|
||||
|
||||
@@ -362,6 +382,7 @@ describe("plugin management install-policy acknowledgements", () => {
|
||||
expect(mocks.npmInstall).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({
|
||||
spec: npmResolution.resolvedSpec,
|
||||
installPolicyRequestedSpecifier: "@openclaw/npm-demo",
|
||||
expectedIntegrity: npmResolution.integrity,
|
||||
}),
|
||||
);
|
||||
@@ -369,7 +390,7 @@ describe("plugin management install-policy acknowledgements", () => {
|
||||
|
||||
it("keeps npm warnings terminal when immutable resolution metadata is incomplete", async () => {
|
||||
const warning: InstallPolicyWarningOccurrence = {
|
||||
warningFingerprint: "review-npm-package",
|
||||
approvalFingerprint: "review-npm-package",
|
||||
scan: {
|
||||
requestKind: "plugin-npm",
|
||||
originType: "plugin-npm",
|
||||
@@ -424,7 +445,7 @@ describe("plugin management install-policy acknowledgements", () => {
|
||||
|
||||
it("pins reviewed ClawHub warnings to the downloaded archive integrity", async () => {
|
||||
const warning: InstallPolicyWarningOccurrence = {
|
||||
warningFingerprint: "review-clawhub-package",
|
||||
approvalFingerprint: "review-clawhub-package",
|
||||
scan: {
|
||||
requestKind: "plugin-archive",
|
||||
originType: "plugin-package",
|
||||
@@ -462,6 +483,7 @@ describe("plugin management install-policy acknowledgements", () => {
|
||||
expect(failure.installPolicyResolvedRequest).toMatchObject({
|
||||
source: "clawhub",
|
||||
spec: "clawhub:community/demo@1.2.3",
|
||||
installPolicyRequestedSpecifier: "clawhub:community/demo",
|
||||
expectedIntegrity: "sha256-reviewed",
|
||||
});
|
||||
});
|
||||
|
||||
@@ -156,6 +156,7 @@ export type ManagedPluginSourceInstallRequest =
|
||||
| {
|
||||
source: "clawhub";
|
||||
spec: string;
|
||||
installPolicyRequestedSpecifier?: string;
|
||||
mode?: "install" | "update";
|
||||
expectedPluginId?: string;
|
||||
expectedIntegrity?: string;
|
||||
@@ -171,6 +172,7 @@ export type ManagedPluginSourceInstallRequest =
|
||||
| {
|
||||
source: "official";
|
||||
spec: string;
|
||||
installPolicyRequestedSpecifier?: string;
|
||||
pluginId: string;
|
||||
expectedIntegrity?: string;
|
||||
mode: "install" | "update";
|
||||
@@ -179,6 +181,7 @@ export type ManagedPluginSourceInstallRequest =
|
||||
| {
|
||||
source: "npm";
|
||||
spec: string;
|
||||
installPolicyRequestedSpecifier?: string;
|
||||
mode: "install" | "update";
|
||||
pin?: boolean;
|
||||
expectedPluginId?: string;
|
||||
@@ -1020,6 +1023,7 @@ function pinInstallPolicyResolvedRequest(
|
||||
return {
|
||||
...request,
|
||||
spec: npmResolution.resolvedSpec,
|
||||
installPolicyRequestedSpecifier: request.installPolicyRequestedSpecifier ?? request.spec,
|
||||
expectedIntegrity: npmResolution.integrity,
|
||||
};
|
||||
}
|
||||
@@ -1034,6 +1038,7 @@ function pinInstallPolicyResolvedRequest(
|
||||
? {
|
||||
...request,
|
||||
spec: buildClawHubSpec(parsed.name, version),
|
||||
installPolicyRequestedSpecifier: request.installPolicyRequestedSpecifier ?? request.spec,
|
||||
expectedIntegrity: integrity,
|
||||
}
|
||||
: undefined;
|
||||
@@ -1378,6 +1383,9 @@ export async function installManagedPluginSource(params: {
|
||||
installPluginFromClawHub({
|
||||
...common,
|
||||
spec: request.spec,
|
||||
...(request.installPolicyRequestedSpecifier
|
||||
? { installPolicyRequestedSpecifier: request.installPolicyRequestedSpecifier }
|
||||
: {}),
|
||||
mode: request.mode,
|
||||
...(request.expectedPluginId ? { expectedPluginId: request.expectedPluginId } : {}),
|
||||
...(request.expectedIntegrity ? { expectedIntegrity: request.expectedIntegrity } : {}),
|
||||
@@ -1401,6 +1409,9 @@ export async function installManagedPluginSource(params: {
|
||||
installPluginFromNpmSpec({
|
||||
...common,
|
||||
spec: request.spec,
|
||||
...(request.installPolicyRequestedSpecifier
|
||||
? { installPolicyRequestedSpecifier: request.installPolicyRequestedSpecifier }
|
||||
: {}),
|
||||
mode: request.mode,
|
||||
...(request.source === "official" || request.trustedSourceLinkedOfficialInstall
|
||||
? { trustedSourceLinkedOfficialInstall: true }
|
||||
@@ -1533,10 +1544,20 @@ export async function installManagedPlugin(params: {
|
||||
...(params.request.installPolicyWarningAcknowledgement
|
||||
? {
|
||||
safetyOverrides: {
|
||||
onInstallPolicyWarning: async ({ scan, warning, warningFingerprint }) => {
|
||||
const currentWarning = { scan, warning, warningFingerprint };
|
||||
const warningIndex = remainingInstallPolicyWarnings.findIndex((approved) =>
|
||||
isDeepStrictEqual(currentWarning, approved),
|
||||
onInstallPolicyWarning: async ({
|
||||
scan,
|
||||
approvalFingerprint,
|
||||
targetName,
|
||||
targetType,
|
||||
requestMode,
|
||||
}) => {
|
||||
const warningIndex = remainingInstallPolicyWarnings.findIndex(
|
||||
(approved) =>
|
||||
approved.approvalFingerprint === approvalFingerprint &&
|
||||
isDeepStrictEqual(approved.scan, scan) &&
|
||||
approved.warning.targetName === targetName &&
|
||||
approved.warning.targetType === targetType &&
|
||||
approved.warning.requestMode === requestMode,
|
||||
);
|
||||
if (warningIndex < 0) {
|
||||
return { status: "unavailable", reason: "warning-not-approved" };
|
||||
|
||||
@@ -7,7 +7,7 @@ import type {
|
||||
} from "../install-security-scan.types.js";
|
||||
|
||||
const officialDiffsWarningOccurrence: InstallPolicyWarningOccurrence = {
|
||||
warningFingerprint: "review-diffs-warning",
|
||||
approvalFingerprint: "review-diffs-warning",
|
||||
scan: {
|
||||
requestKind: "plugin-archive",
|
||||
originType: "plugin-package",
|
||||
|
||||
Reference in New Issue
Block a user