fix(security): bind install warning approvals

This commit is contained in:
Jesse Merhi
2026-08-10 16:08:51 +10:00
committed by jesse-merhi
parent 7412828da8
commit 7cf4e84584
18 changed files with 406 additions and 60 deletions
-1
View File
@@ -284,7 +284,6 @@ type ArchiveInstallCall = {
requestedSpecifier?: string;
source?: { kind?: string; authority?: string; mutable?: boolean; network?: boolean };
};
onInstallPolicyWarning?: () => Promise<boolean>;
trustedSourceLinkedOfficialInstall?: boolean;
};
+1 -1
View File
@@ -1466,7 +1466,7 @@ export async function installPluginFromClawHub(
},
});
if (!installResult.ok) {
return installResult;
return { ...installResult, version: versionState.version };
}
const pkg = detail.package!;
@@ -344,6 +344,12 @@ describe("legacy file install scan compatibility", () => {
targetName: "payload",
targetType: "plugin",
requestMode: "install",
warning: {
targetName: "payload",
targetType: "plugin",
requestMode: "install",
reason: "review this plugin",
},
});
expect(onInstallPolicyWarning).toHaveBeenCalledTimes(1);
expect(runInstallPolicyMock).toHaveBeenCalledTimes(2);
@@ -853,6 +853,7 @@ async function runOperatorInstallPolicy(params: {
targetName: params.targetName,
targetType: params.targetType,
requestMode: params.requestMode,
warning: installPolicyWarning,
});
if (acknowledgement.status === "approved") {
const reevaluated = await evaluatePolicy();
@@ -14,6 +14,7 @@ export type InstallPolicyWarningAcknowledgementRequest = {
targetName: string;
targetType: "skill" | "plugin";
requestMode: "install" | "update";
warning: InstallPolicyWarningDetails;
};
type InstallPolicyWarningAcknowledgementResult =
+5 -3
View File
@@ -1,6 +1,9 @@
import { expectDefined } from "@openclaw/normalization-core";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { expectOneShotInstallPolicyWarningAcknowledgement } from "./test-helpers/install-policy-warning.js";
import {
expectOneShotInstallPolicyWarningAcknowledgement,
officialDiffsWarningRequest,
} from "./test-helpers/install-policy-warning.js";
const mocks = vi.hoisted(() => ({
applyUninstall: vi.fn(),
@@ -785,7 +788,7 @@ describe("plugin management service", () => {
);
await installManagedPlugin({
request: { source: "official", pluginId: "diffs", acknowledgeInstallPolicyWarning: true },
request: officialDiffsWarningRequest,
env: {},
});
@@ -794,7 +797,6 @@ describe("plugin management service", () => {
spec: "clawhub:@openclaw/diffs@2026.6.11",
expectedPluginId: "diffs",
expectedIntegrity: `sha256-${Buffer.from("a".repeat(64), "hex").toString("base64")}`,
onInstallPolicyWarning: expect.any(Function),
}),
);
await expectOneShotInstallPolicyWarningAcknowledgement(mocks.clawhubInstall);
+54 -17
View File
@@ -1,5 +1,6 @@
// Structured plugin catalog and lifecycle operations shared by Gateway-facing surfaces.
import path from "node:path";
import { isDeepStrictEqual } from "node:util";
import { asSafeIntegerInRange } from "@openclaw/normalization-core/number-coercion";
import { normalizeOptionalString } from "@openclaw/normalization-core/string-coerce";
import { uniqueStrings } from "@openclaw/normalization-core/string-normalization";
@@ -117,18 +118,24 @@ type ManagedPluginCatalog = {
mutationAllowed: boolean;
};
type ManagedPluginInstallRequest =
export type ManagedPluginInstallRequest =
| {
source: "clawhub";
packageName: string;
version?: string;
acknowledgeClawHubRisk?: boolean;
acknowledgeInstallPolicyWarning?: boolean;
installPolicyWarningAcknowledgement?: {
warning: InstallPolicyWarningDetails;
resolvedRequest: ManagedPluginSourceInstallRequest;
};
}
| {
source: "official";
pluginId: string;
acknowledgeInstallPolicyWarning?: boolean;
installPolicyWarningAcknowledgement?: {
warning: InstallPolicyWarningDetails;
resolvedRequest: ManagedPluginSourceInstallRequest;
};
};
export type ManagedPluginSourceInstallRequest =
@@ -223,6 +230,7 @@ export class ManagedPluginLifecycleError extends Error {
readonly version?: string;
readonly warning?: string;
readonly installPolicyWarning?: InstallPolicyWarningDetails;
readonly installPolicyResolvedRequest?: ManagedPluginSourceInstallRequest;
constructor(
message: string,
@@ -232,6 +240,7 @@ export class ManagedPluginLifecycleError extends Error {
version?: string;
warning?: string;
installPolicyWarning?: InstallPolicyWarningDetails;
installPolicyResolvedRequest?: ManagedPluginSourceInstallRequest;
cause?: unknown;
},
) {
@@ -242,6 +251,7 @@ export class ManagedPluginLifecycleError extends Error {
this.version = details?.version;
this.warning = details?.warning;
this.installPolicyWarning = details?.installPolicyWarning;
this.installPolicyResolvedRequest = details?.installPolicyResolvedRequest;
}
}
@@ -990,13 +1000,27 @@ function buildClawHubSpec(packageName: string, version?: string): string {
return `clawhub:${packageName}${version ? `@${version}` : ""}`;
}
function throwInstallFailure(result: {
error: string;
code?: string;
version?: string;
warning?: string;
installPolicyWarning?: InstallPolicyWarningDetails;
}): never {
function pinInstallPolicyResolvedRequest(
request: ManagedPluginSourceInstallRequest,
version: string | undefined,
): ManagedPluginSourceInstallRequest {
if (request.source !== "clawhub" || !version) {
return request;
}
const parsed = parseClawHubPluginSpec(request.spec);
return parsed ? { ...request, spec: buildClawHubSpec(parsed.name, version) } : request;
}
function throwInstallFailure(
result: {
error: string;
code?: string;
version?: string;
warning?: string;
installPolicyWarning?: InstallPolicyWarningDetails;
},
resolvedRequest?: ManagedPluginSourceInstallRequest,
): never {
const unavailable =
!result.code ||
result.code === CLAWHUB_INSTALL_ERROR_CODE.ARTIFACT_UNAVAILABLE ||
@@ -1008,6 +1032,14 @@ function throwInstallFailure(result: {
version: result.version,
warning: result.warning,
installPolicyWarning: result.installPolicyWarning,
...(result.installPolicyWarning && resolvedRequest
? {
installPolicyResolvedRequest: pinInstallPolicyResolvedRequest(
resolvedRequest,
result.version,
),
}
: {}),
cause: result,
});
}
@@ -1085,6 +1117,7 @@ function throwPersistenceFailureWithCleanupWarnings(error: unknown, warnings: st
version: error.version,
warning: [error.warning, cleanupWarning].filter(Boolean).join("\n"),
installPolicyWarning: error.installPolicyWarning,
installPolicyResolvedRequest: error.installPolicyResolvedRequest,
cause: error,
});
}
@@ -1435,10 +1468,11 @@ export async function installManagedPlugin(params: {
const warnings: string[] = [];
const installLogger = createInstallLogger(warnings);
let installPolicyWarningAcknowledgementAvailable = Boolean(
params.request.acknowledgeInstallPolicyWarning,
params.request.installPolicyWarningAcknowledgement,
);
const request =
params.request.source === "clawhub"
params.request.installPolicyWarningAcknowledgement?.resolvedRequest ??
(params.request.source === "clawhub"
? resolveManagedClawHubInstallRequest({
request: params.request,
officialEntries: officialCatalog.entries,
@@ -1446,22 +1480,25 @@ export async function installManagedPlugin(params: {
: resolveManagedOfficialInstallRequest({
request: params.request,
officialEntries: officialCatalog.entries,
});
}));
const installed = await installManagedPluginSource({
request,
snapshot,
env,
logger: installLogger,
persistenceLogger: installLogger,
...(params.request.acknowledgeInstallPolicyWarning
...(params.request.installPolicyWarningAcknowledgement
? {
safetyOverrides: {
onInstallPolicyWarning: async () => {
onInstallPolicyWarning: async ({ warning }) => {
if (!installPolicyWarningAcknowledgementAvailable) {
return false;
}
installPolicyWarningAcknowledgementAvailable = false;
return true;
return isDeepStrictEqual(
warning,
params.request.installPolicyWarningAcknowledgement?.warning,
);
},
},
}
@@ -1471,7 +1508,7 @@ export async function installManagedPlugin(params: {
runtime: createSilentRuntime(),
});
if (!installed.ok) {
return throwInstallFailure(installed);
return throwInstallFailure(installed, request);
}
const catalog = await listManagedPlugins({
config: installed.config,
@@ -8,6 +8,25 @@ type InstallPolicyWarningCall = {
) => Promise<boolean>;
};
export const officialDiffsWarningRequest = {
source: "official",
pluginId: "diffs",
installPolicyWarningAcknowledgement: {
resolvedRequest: {
source: "clawhub",
spec: "clawhub:@openclaw/diffs@2026.6.11",
expectedPluginId: "diffs",
expectedIntegrity: `sha256-${Buffer.from("a".repeat(64), "hex").toString("base64")}`,
},
warning: {
targetName: "diffs",
targetType: "plugin",
requestMode: "install",
reason: "Review this warning",
},
},
} as const;
export async function expectOneShotInstallPolicyWarningAcknowledgement(mock: {
mock: { calls: unknown[][] };
}): Promise<void> {
@@ -21,6 +40,12 @@ export async function expectOneShotInstallPolicyWarningAcknowledgement(mock: {
targetName: "diffs",
targetType: "plugin",
requestMode: "install",
warning: {
targetName: "diffs",
targetType: "plugin",
requestMode: "install",
reason: "Review this warning",
},
};
expect(await acknowledge(request)).toBe(true);
expect(await acknowledge(request)).toBe(false);