mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-23 10:55:31 -06:00
fix(security): bind install warning approvals
This commit is contained in:
@@ -284,7 +284,6 @@ type ArchiveInstallCall = {
|
||||
requestedSpecifier?: string;
|
||||
source?: { kind?: string; authority?: string; mutable?: boolean; network?: boolean };
|
||||
};
|
||||
onInstallPolicyWarning?: () => Promise<boolean>;
|
||||
trustedSourceLinkedOfficialInstall?: boolean;
|
||||
};
|
||||
|
||||
|
||||
@@ -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 =
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user