mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-24 08:21:39 +00:00
device-pair: align internal command checks
This commit is contained in:
@@ -43,6 +43,7 @@ vi.mock("./notify.js", () => ({
|
||||
registerPairingNotifierService: vi.fn(),
|
||||
}));
|
||||
|
||||
import { approveDevicePairing, listDevicePairing } from "./api.js";
|
||||
import registerDevicePair from "./index.js";
|
||||
|
||||
function createApi(params?: {
|
||||
@@ -371,3 +372,89 @@ describe("device-pair /pair qr", () => {
|
||||
expect(result).toEqual({ text: "Invalidated 2 unused setup codes." });
|
||||
});
|
||||
});
|
||||
|
||||
describe("device-pair /pair approve", () => {
|
||||
it("rejects internal gateway callers without operator.pairing", async () => {
|
||||
vi.mocked(listDevicePairing).mockResolvedValueOnce({
|
||||
pending: [
|
||||
{
|
||||
requestId: "req-1",
|
||||
deviceId: "victim-phone",
|
||||
publicKey: "victim-public-key",
|
||||
displayName: "Victim Phone",
|
||||
platform: "ios",
|
||||
ts: Date.now(),
|
||||
},
|
||||
],
|
||||
paired: [],
|
||||
});
|
||||
|
||||
const command = registerPairCommand();
|
||||
const result = await command.handler(
|
||||
createCommandContext({
|
||||
channel: "webchat",
|
||||
args: "approve latest",
|
||||
commandBody: "/pair approve latest",
|
||||
gatewayClientScopes: ["operator.write"],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(vi.mocked(approveDevicePairing)).not.toHaveBeenCalled();
|
||||
expect(result).toEqual({
|
||||
text: "⚠️ This command requires operator.pairing for internal gateway callers.",
|
||||
});
|
||||
});
|
||||
|
||||
it("allows internal gateway callers with operator.pairing", async () => {
|
||||
vi.mocked(listDevicePairing).mockResolvedValueOnce({
|
||||
pending: [
|
||||
{
|
||||
requestId: "req-1",
|
||||
deviceId: "victim-phone",
|
||||
publicKey: "victim-public-key",
|
||||
displayName: "Victim Phone",
|
||||
platform: "ios",
|
||||
ts: Date.now(),
|
||||
},
|
||||
],
|
||||
paired: [],
|
||||
});
|
||||
vi.mocked(approveDevicePairing).mockResolvedValueOnce({
|
||||
status: "approved",
|
||||
requestId: "req-1",
|
||||
device: {
|
||||
deviceId: "victim-phone",
|
||||
publicKey: "victim-public-key",
|
||||
displayName: "Victim Phone",
|
||||
platform: "ios",
|
||||
role: "operator",
|
||||
roles: ["operator"],
|
||||
scopes: ["operator.pairing"],
|
||||
approvedScopes: ["operator.pairing"],
|
||||
tokens: {
|
||||
operator: {
|
||||
token: "token-1",
|
||||
role: "operator",
|
||||
scopes: ["operator.pairing"],
|
||||
createdAtMs: Date.now(),
|
||||
},
|
||||
},
|
||||
createdAtMs: Date.now(),
|
||||
approvedAtMs: Date.now(),
|
||||
},
|
||||
});
|
||||
|
||||
const command = registerPairCommand();
|
||||
const result = await command.handler(
|
||||
createCommandContext({
|
||||
channel: "webchat",
|
||||
args: "approve latest",
|
||||
commandBody: "/pair approve latest",
|
||||
gatewayClientScopes: ["operator.write", "operator.pairing"],
|
||||
}),
|
||||
);
|
||||
|
||||
expect(vi.mocked(approveDevicePairing)).toHaveBeenCalledWith("req-1");
|
||||
expect(result).toEqual({ text: "✅ Paired Victim Phone (ios)." });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -577,6 +577,9 @@ export default definePluginEntry({
|
||||
const args = ctx.args?.trim() ?? "";
|
||||
const tokens = args.split(/\s+/).filter(Boolean);
|
||||
const action = tokens[0]?.toLowerCase() ?? "";
|
||||
const gatewayClientScopes = Array.isArray(ctx.gatewayClientScopes)
|
||||
? ctx.gatewayClientScopes
|
||||
: null;
|
||||
api.logger.info?.(
|
||||
`device-pair: /pair invoked channel=${ctx.channel} sender=${ctx.senderId ?? "unknown"} action=${
|
||||
action || "new"
|
||||
@@ -598,6 +601,15 @@ export default definePluginEntry({
|
||||
}
|
||||
|
||||
if (action === "approve") {
|
||||
if (
|
||||
gatewayClientScopes &&
|
||||
!gatewayClientScopes.includes("operator.pairing") &&
|
||||
!gatewayClientScopes.includes("operator.admin")
|
||||
) {
|
||||
return {
|
||||
text: "⚠️ This command requires operator.pairing for internal gateway callers.",
|
||||
};
|
||||
}
|
||||
const requested = tokens[1]?.trim();
|
||||
const list = await listDevicePairing();
|
||||
if (list.pending.length === 0) {
|
||||
|
||||
@@ -37,6 +37,7 @@ export const handlePluginCommand: CommandHandler = async (
|
||||
channel: command.channel,
|
||||
channelId: command.channelId,
|
||||
isAuthorizedSender: command.isAuthorizedSender,
|
||||
gatewayClientScopes: params.ctx.GatewayClientScopes,
|
||||
commandBody: command.commandBodyNormalized,
|
||||
config: cfg,
|
||||
from: command.from,
|
||||
|
||||
@@ -184,6 +184,7 @@ export async function executePluginCommand(params: {
|
||||
channel: string;
|
||||
channelId?: PluginCommandContext["channelId"];
|
||||
isAuthorizedSender: boolean;
|
||||
gatewayClientScopes?: PluginCommandContext["gatewayClientScopes"];
|
||||
commandBody: string;
|
||||
config: OpenClawConfig;
|
||||
from?: PluginCommandContext["from"];
|
||||
@@ -217,6 +218,7 @@ export async function executePluginCommand(params: {
|
||||
channel,
|
||||
channelId: params.channelId,
|
||||
isAuthorizedSender,
|
||||
gatewayClientScopes: params.gatewayClientScopes,
|
||||
args: sanitizedArgs,
|
||||
commandBody,
|
||||
config,
|
||||
|
||||
@@ -971,6 +971,8 @@ export type PluginCommandContext = {
|
||||
channelId?: ChannelId;
|
||||
/** Whether the sender is on the allowlist */
|
||||
isAuthorizedSender: boolean;
|
||||
/** Gateway client scopes for internal control-plane callers */
|
||||
gatewayClientScopes?: string[];
|
||||
/** Raw command arguments after the command name */
|
||||
args?: string;
|
||||
/** The full normalized command body */
|
||||
|
||||
Reference in New Issue
Block a user