mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-02 14:47:15 +00:00
Gateway: default-deny dangerous node commands
This commit is contained in:
39
src/gateway/node-command-policy.test.ts
Normal file
39
src/gateway/node-command-policy.test.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import { resolveNodeCommandAllowlist } from "./node-command-policy.js";
|
||||||
|
|
||||||
|
describe("resolveNodeCommandAllowlist", () => {
|
||||||
|
it("includes iOS service commands by default", () => {
|
||||||
|
const allow = resolveNodeCommandAllowlist(
|
||||||
|
{},
|
||||||
|
{
|
||||||
|
platform: "ios 26.0",
|
||||||
|
deviceFamily: "iPhone",
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(allow.has("device.info")).toBe(true);
|
||||||
|
expect(allow.has("device.status")).toBe(true);
|
||||||
|
expect(allow.has("system.notify")).toBe(true);
|
||||||
|
expect(allow.has("contacts.search")).toBe(true);
|
||||||
|
expect(allow.has("calendar.events")).toBe(true);
|
||||||
|
expect(allow.has("reminders.list")).toBe(true);
|
||||||
|
expect(allow.has("photos.latest")).toBe(true);
|
||||||
|
expect(allow.has("motion.activity")).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("applies denyCommands as exact removals", () => {
|
||||||
|
const allow = resolveNodeCommandAllowlist(
|
||||||
|
{
|
||||||
|
gateway: {
|
||||||
|
nodes: {
|
||||||
|
denyCommands: ["camera.snap", "screen.record"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{ platform: "ios", deviceFamily: "iPhone" },
|
||||||
|
);
|
||||||
|
expect(allow.has("camera.snap")).toBe(false);
|
||||||
|
expect(allow.has("screen.record")).toBe(false);
|
||||||
|
expect(allow.has("camera.clip")).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -18,8 +18,23 @@ const SCREEN_COMMANDS = ["screen.record"];
|
|||||||
|
|
||||||
const LOCATION_COMMANDS = ["location.get"];
|
const LOCATION_COMMANDS = ["location.get"];
|
||||||
|
|
||||||
|
const DEVICE_COMMANDS = ["device.info", "device.status"];
|
||||||
|
|
||||||
|
const CONTACTS_COMMANDS = ["contacts.search", "contacts.add"];
|
||||||
|
|
||||||
|
const CALENDAR_COMMANDS = ["calendar.events", "calendar.add"];
|
||||||
|
|
||||||
|
const REMINDERS_COMMANDS = ["reminders.list", "reminders.add"];
|
||||||
|
|
||||||
|
const PHOTOS_COMMANDS = ["photos.latest"];
|
||||||
|
|
||||||
|
const MOTION_COMMANDS = ["motion.activity", "motion.pedometer"];
|
||||||
|
|
||||||
const SMS_COMMANDS = ["sms.send"];
|
const SMS_COMMANDS = ["sms.send"];
|
||||||
|
|
||||||
|
// iOS nodes don't implement system.run/which, but they do support notifications.
|
||||||
|
const IOS_SYSTEM_COMMANDS = ["system.notify"];
|
||||||
|
|
||||||
const SYSTEM_COMMANDS = [
|
const SYSTEM_COMMANDS = [
|
||||||
"system.run",
|
"system.run",
|
||||||
"system.which",
|
"system.which",
|
||||||
@@ -30,12 +45,30 @@ const SYSTEM_COMMANDS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const PLATFORM_DEFAULTS: Record<string, string[]> = {
|
const PLATFORM_DEFAULTS: Record<string, string[]> = {
|
||||||
ios: [...CANVAS_COMMANDS, ...CAMERA_COMMANDS, ...SCREEN_COMMANDS, ...LOCATION_COMMANDS],
|
ios: [
|
||||||
|
...CANVAS_COMMANDS,
|
||||||
|
...CAMERA_COMMANDS,
|
||||||
|
...SCREEN_COMMANDS,
|
||||||
|
...LOCATION_COMMANDS,
|
||||||
|
...DEVICE_COMMANDS,
|
||||||
|
...CONTACTS_COMMANDS,
|
||||||
|
...CALENDAR_COMMANDS,
|
||||||
|
...REMINDERS_COMMANDS,
|
||||||
|
...PHOTOS_COMMANDS,
|
||||||
|
...MOTION_COMMANDS,
|
||||||
|
...IOS_SYSTEM_COMMANDS,
|
||||||
|
],
|
||||||
android: [
|
android: [
|
||||||
...CANVAS_COMMANDS,
|
...CANVAS_COMMANDS,
|
||||||
...CAMERA_COMMANDS,
|
...CAMERA_COMMANDS,
|
||||||
...SCREEN_COMMANDS,
|
...SCREEN_COMMANDS,
|
||||||
...LOCATION_COMMANDS,
|
...LOCATION_COMMANDS,
|
||||||
|
...DEVICE_COMMANDS,
|
||||||
|
...CONTACTS_COMMANDS,
|
||||||
|
...CALENDAR_COMMANDS,
|
||||||
|
...REMINDERS_COMMANDS,
|
||||||
|
...PHOTOS_COMMANDS,
|
||||||
|
...MOTION_COMMANDS,
|
||||||
...SMS_COMMANDS,
|
...SMS_COMMANDS,
|
||||||
],
|
],
|
||||||
macos: [
|
macos: [
|
||||||
@@ -43,6 +76,12 @@ const PLATFORM_DEFAULTS: Record<string, string[]> = {
|
|||||||
...CAMERA_COMMANDS,
|
...CAMERA_COMMANDS,
|
||||||
...SCREEN_COMMANDS,
|
...SCREEN_COMMANDS,
|
||||||
...LOCATION_COMMANDS,
|
...LOCATION_COMMANDS,
|
||||||
|
...DEVICE_COMMANDS,
|
||||||
|
...CONTACTS_COMMANDS,
|
||||||
|
...CALENDAR_COMMANDS,
|
||||||
|
...REMINDERS_COMMANDS,
|
||||||
|
...PHOTOS_COMMANDS,
|
||||||
|
...MOTION_COMMANDS,
|
||||||
...SYSTEM_COMMANDS,
|
...SYSTEM_COMMANDS,
|
||||||
],
|
],
|
||||||
linux: [...SYSTEM_COMMANDS],
|
linux: [...SYSTEM_COMMANDS],
|
||||||
|
|||||||
@@ -64,5 +64,13 @@ describe("configureGatewayForOnboarding", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
expect(result.settings.gatewayToken).toBe("generated-token");
|
expect(result.settings.gatewayToken).toBe("generated-token");
|
||||||
|
expect(result.nextConfig.gateway?.nodes?.denyCommands).toEqual([
|
||||||
|
"camera.snap",
|
||||||
|
"camera.clip",
|
||||||
|
"screen.record",
|
||||||
|
"calendar.add",
|
||||||
|
"contacts.add",
|
||||||
|
"reminders.add",
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -10,6 +10,20 @@ import type { WizardPrompter } from "./prompts.js";
|
|||||||
import { normalizeGatewayTokenInput, randomToken } from "../commands/onboard-helpers.js";
|
import { normalizeGatewayTokenInput, randomToken } from "../commands/onboard-helpers.js";
|
||||||
import { findTailscaleBinary } from "../infra/tailscale.js";
|
import { findTailscaleBinary } from "../infra/tailscale.js";
|
||||||
|
|
||||||
|
// These commands are "high risk" (privacy writes/recording) and should be
|
||||||
|
// explicitly armed by the user when they want to use them.
|
||||||
|
//
|
||||||
|
// This only affects what the gateway will accept via node.invoke; the iOS app
|
||||||
|
// still prompts for OS permissions (camera/photos/contacts/etc) on first use.
|
||||||
|
const DEFAULT_DANGEROUS_NODE_DENY_COMMANDS = [
|
||||||
|
"camera.snap",
|
||||||
|
"camera.clip",
|
||||||
|
"screen.record",
|
||||||
|
"calendar.add",
|
||||||
|
"contacts.add",
|
||||||
|
"reminders.add",
|
||||||
|
];
|
||||||
|
|
||||||
type ConfigureGatewayOptions = {
|
type ConfigureGatewayOptions = {
|
||||||
flow: WizardFlow;
|
flow: WizardFlow;
|
||||||
baseConfig: OpenClawConfig;
|
baseConfig: OpenClawConfig;
|
||||||
@@ -236,6 +250,27 @@ export async function configureGatewayForOnboarding(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// If this is a new gateway setup (no existing gateway settings), start with a
|
||||||
|
// denylist for high-risk node commands. Users can arm these temporarily via
|
||||||
|
// /phone arm ... (phone-control plugin).
|
||||||
|
if (
|
||||||
|
!quickstartGateway.hasExisting &&
|
||||||
|
nextConfig.gateway?.nodes?.denyCommands === undefined &&
|
||||||
|
nextConfig.gateway?.nodes?.allowCommands === undefined &&
|
||||||
|
nextConfig.gateway?.nodes?.browser === undefined
|
||||||
|
) {
|
||||||
|
nextConfig = {
|
||||||
|
...nextConfig,
|
||||||
|
gateway: {
|
||||||
|
...nextConfig.gateway,
|
||||||
|
nodes: {
|
||||||
|
...nextConfig.gateway?.nodes,
|
||||||
|
denyCommands: [...DEFAULT_DANGEROUS_NODE_DENY_COMMANDS],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
nextConfig,
|
nextConfig,
|
||||||
settings: {
|
settings: {
|
||||||
|
|||||||
Reference in New Issue
Block a user