mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-06 05:51:15 +08:00
fix(plugins): snapshot node invoke policies
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
// Node invoke policy registration tests cover plugin-owned policy snapshotting.
|
||||
import {
|
||||
createPluginRegistryFixture,
|
||||
registerTestPlugin,
|
||||
} from "openclaw/plugin-sdk/plugin-test-contracts";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
import {
|
||||
isForegroundRestrictedPluginNodeCommand,
|
||||
resolveNodeCommandAllowlist,
|
||||
} from "../../gateway/node-command-policy.js";
|
||||
import { resetPluginRuntimeStateForTest, setActivePluginRegistry } from "../runtime.js";
|
||||
import { createPluginRecord } from "../status.test-helpers.js";
|
||||
import type {
|
||||
OpenClawPluginNodeInvokePolicy,
|
||||
OpenClawPluginNodeInvokePolicyContext,
|
||||
} from "../types.js";
|
||||
|
||||
describe("plugin node invoke policy registration", () => {
|
||||
afterEach(() => {
|
||||
resetPluginRuntimeStateForTest();
|
||||
});
|
||||
|
||||
it("snapshots policy fields before node command policy projection", async () => {
|
||||
let commandsReads = 0;
|
||||
let defaultPlatformsReads = 0;
|
||||
let dangerousReads = 0;
|
||||
let foregroundRestrictedReads = 0;
|
||||
let handleReads = 0;
|
||||
const handler: OpenClawPluginNodeInvokePolicy["handle"] = (ctx) => ({
|
||||
ok: true,
|
||||
payload: ctx.command,
|
||||
});
|
||||
const { config, registry } = createPluginRegistryFixture();
|
||||
registerTestPlugin({
|
||||
registry,
|
||||
config,
|
||||
record: createPluginRecord({
|
||||
id: "volatile-node-policy",
|
||||
name: "Volatile Node Policy",
|
||||
}),
|
||||
register(api) {
|
||||
api.registerNodeInvokePolicy({
|
||||
get commands() {
|
||||
commandsReads += 1;
|
||||
if (commandsReads > 1) {
|
||||
throw new Error("policy commands getter re-read");
|
||||
}
|
||||
return [" volatile.snapshot ", "volatile.snapshot"];
|
||||
},
|
||||
get defaultPlatforms() {
|
||||
defaultPlatformsReads += 1;
|
||||
if (defaultPlatformsReads > 1) {
|
||||
throw new Error("policy defaultPlatforms getter re-read");
|
||||
}
|
||||
return ["ios", " ios ", "android"];
|
||||
},
|
||||
get dangerous() {
|
||||
dangerousReads += 1;
|
||||
if (dangerousReads > 1) {
|
||||
throw new Error("policy dangerous getter re-read");
|
||||
}
|
||||
return false;
|
||||
},
|
||||
get foregroundRestrictedOnIos() {
|
||||
foregroundRestrictedReads += 1;
|
||||
if (foregroundRestrictedReads > 1) {
|
||||
throw new Error("policy foregroundRestrictedOnIos getter re-read");
|
||||
}
|
||||
return true;
|
||||
},
|
||||
get handle() {
|
||||
handleReads += 1;
|
||||
if (handleReads > 1) {
|
||||
throw new Error("policy handle getter re-read");
|
||||
}
|
||||
return handler;
|
||||
},
|
||||
} as OpenClawPluginNodeInvokePolicy);
|
||||
},
|
||||
});
|
||||
setActivePluginRegistry(registry.registry);
|
||||
|
||||
const policy = registry.registry.nodeInvokePolicies?.[0]?.policy;
|
||||
expect(policy).toMatchObject({
|
||||
commands: ["volatile.snapshot"],
|
||||
defaultPlatforms: ["ios", "android"],
|
||||
foregroundRestrictedOnIos: true,
|
||||
});
|
||||
expect(policy?.dangerous).toBeUndefined();
|
||||
expect(
|
||||
resolveNodeCommandAllowlist({} as OpenClawConfig, { platform: "ios" }).has(
|
||||
"volatile.snapshot",
|
||||
),
|
||||
).toBe(true);
|
||||
expect(isForegroundRestrictedPluginNodeCommand(" volatile.snapshot ")).toBe(true);
|
||||
expect(
|
||||
await Promise.resolve(
|
||||
policy?.handle({
|
||||
nodeId: "node-1",
|
||||
command: "volatile.snapshot",
|
||||
params: {},
|
||||
config: {} as OpenClawConfig,
|
||||
invokeNode: async () => ({ ok: true, payload: "raw", payloadJSON: null }),
|
||||
} satisfies OpenClawPluginNodeInvokePolicyContext),
|
||||
),
|
||||
).toEqual({ ok: true, payload: "volatile.snapshot" });
|
||||
|
||||
expect(commandsReads).toBe(1);
|
||||
expect(defaultPlatformsReads).toBe(1);
|
||||
expect(dangerousReads).toBe(1);
|
||||
expect(foregroundRestrictedReads).toBe(1);
|
||||
expect(handleReads).toBe(1);
|
||||
});
|
||||
});
|
||||
@@ -1650,8 +1650,12 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
policy: OpenClawPluginNodeInvokePolicy,
|
||||
pluginConfig?: Record<string, unknown>,
|
||||
) => {
|
||||
const fields = readNodeInvokePolicyFields(record, policy);
|
||||
if (!fields) {
|
||||
return;
|
||||
}
|
||||
const commands = normalizeUniqueStringEntries(
|
||||
Array.isArray(policy.commands) ? policy.commands : [],
|
||||
Array.isArray(fields.commands) ? fields.commands : [],
|
||||
);
|
||||
if (commands.length === 0) {
|
||||
pushDiagnostic({
|
||||
@@ -1662,7 +1666,8 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (typeof policy.handle !== "function") {
|
||||
const handle = fields.handle;
|
||||
if (typeof handle !== "function") {
|
||||
pushDiagnostic({
|
||||
level: "error",
|
||||
pluginId: record.id,
|
||||
@@ -1689,13 +1694,63 @@ export function createPluginRegistry(registryParams: PluginRegistryParams) {
|
||||
registry.nodeInvokePolicies.push({
|
||||
pluginId: record.id,
|
||||
pluginName: record.name,
|
||||
policy: { ...policy, commands },
|
||||
policy: {
|
||||
commands,
|
||||
...(Array.isArray(fields.defaultPlatforms)
|
||||
? {
|
||||
defaultPlatforms: normalizeUniqueStringEntries(
|
||||
fields.defaultPlatforms,
|
||||
) as OpenClawPluginNodeInvokePolicy["defaultPlatforms"],
|
||||
}
|
||||
: {}),
|
||||
...(fields.dangerous === true ? { dangerous: true } : {}),
|
||||
...(fields.foregroundRestrictedOnIos === true ? { foregroundRestrictedOnIos: true } : {}),
|
||||
handle: handle as OpenClawPluginNodeInvokePolicy["handle"],
|
||||
},
|
||||
pluginConfig,
|
||||
source: record.source,
|
||||
rootDir: record.rootDir,
|
||||
});
|
||||
};
|
||||
|
||||
const readNodeInvokePolicyFields = (
|
||||
record: PluginRecord,
|
||||
policy: OpenClawPluginNodeInvokePolicy,
|
||||
):
|
||||
| {
|
||||
commands: unknown;
|
||||
defaultPlatforms: unknown;
|
||||
dangerous: unknown;
|
||||
foregroundRestrictedOnIos: unknown;
|
||||
handle: unknown;
|
||||
}
|
||||
| undefined => {
|
||||
let commands: unknown;
|
||||
try {
|
||||
commands = policy.commands;
|
||||
return {
|
||||
commands,
|
||||
defaultPlatforms: policy.defaultPlatforms,
|
||||
dangerous: policy.dangerous,
|
||||
foregroundRestrictedOnIos: policy.foregroundRestrictedOnIos,
|
||||
handle: policy.handle,
|
||||
};
|
||||
} catch (error) {
|
||||
const normalizedCommands = normalizeUniqueStringEntries(
|
||||
Array.isArray(commands) ? commands : [],
|
||||
);
|
||||
pushDiagnostic({
|
||||
level: "error",
|
||||
pluginId: record.id,
|
||||
source: record.source,
|
||||
message:
|
||||
`node invoke policy registration has unreadable fields` +
|
||||
`${normalizedCommands.length > 0 ? `: ${normalizedCommands.join(", ")}` : ""}: ${formatErrorMessage(error)}`,
|
||||
});
|
||||
return undefined;
|
||||
}
|
||||
};
|
||||
|
||||
const registerSecurityAuditCollector = (
|
||||
record: PluginRecord,
|
||||
collector: OpenClawPluginSecurityAuditCollector,
|
||||
|
||||
Reference in New Issue
Block a user