mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-08 08:41:23 +00:00
fix: harden agent gateway authorization scopes
This commit is contained in:
@@ -39,6 +39,16 @@ describe("cron tool", () => {
|
||||
callGatewayMock.mockResolvedValue({ ok: true });
|
||||
});
|
||||
|
||||
it("rejects non-owner callers explicitly", async () => {
|
||||
const tool = createCronTool({ senderIsOwner: false });
|
||||
await expect(
|
||||
tool.execute("call-owner-check", {
|
||||
action: "status",
|
||||
}),
|
||||
).rejects.toThrow("Tool restricted to owner senders.");
|
||||
expect(callGatewayMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
[
|
||||
"update",
|
||||
|
||||
@@ -48,6 +48,7 @@ const CronToolSchema = Type.Object({
|
||||
|
||||
type CronToolOptions = {
|
||||
agentSessionKey?: string;
|
||||
senderIsOwner?: boolean;
|
||||
};
|
||||
|
||||
type ChatMessage = {
|
||||
@@ -259,6 +260,9 @@ WAKE MODES (for wake action):
|
||||
Use jobId as the canonical identifier; id is accepted for compatibility. Use contextMessages (0-10) to add previous messages as context to the job text.`,
|
||||
parameters: CronToolSchema,
|
||||
execute: async (_toolCallId, args) => {
|
||||
if (opts?.senderIsOwner === false) {
|
||||
throw new Error("Tool restricted to owner senders.");
|
||||
}
|
||||
const params = args as Record<string, unknown>;
|
||||
const action = readStringParam(params, "action", { required: true });
|
||||
const gatewayOpts: GatewayCallOptions = {
|
||||
|
||||
@@ -65,6 +65,7 @@ const GatewayToolSchema = Type.Object({
|
||||
export function createGatewayTool(opts?: {
|
||||
agentSessionKey?: string;
|
||||
config?: OpenClawConfig;
|
||||
senderIsOwner?: boolean;
|
||||
}): AnyAgentTool {
|
||||
return {
|
||||
label: "Gateway",
|
||||
@@ -73,6 +74,9 @@ export function createGatewayTool(opts?: {
|
||||
"Restart, apply config, or update the gateway in-place (SIGUSR1). Use config.patch for safe partial config updates (merges with existing). Use config.apply only when replacing entire config. Both trigger restart after writing. Always pass a human-readable completion message via the `note` parameter so the system can deliver it to the user after restart.",
|
||||
parameters: GatewayToolSchema,
|
||||
execute: async (_toolCallId, args) => {
|
||||
if (opts?.senderIsOwner === false) {
|
||||
throw new Error("Tool restricted to owner senders.");
|
||||
}
|
||||
const params = args as Record<string, unknown>;
|
||||
const action = readStringParam(params, "action", { required: true });
|
||||
if (action === "restart") {
|
||||
|
||||
@@ -32,6 +32,40 @@ describe("gateway tool defaults", () => {
|
||||
url: "ws://127.0.0.1:18789",
|
||||
token: "t",
|
||||
timeoutMs: 5000,
|
||||
scopes: ["operator.read"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses least-privilege write scope for write methods", async () => {
|
||||
callGatewayMock.mockResolvedValueOnce({ ok: true });
|
||||
await callGatewayTool("wake", {}, { mode: "now", text: "hi" });
|
||||
expect(callGatewayMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
method: "wake",
|
||||
scopes: ["operator.write"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("uses admin scope only for admin methods", async () => {
|
||||
callGatewayMock.mockResolvedValueOnce({ ok: true });
|
||||
await callGatewayTool("cron.add", {}, { id: "job-1" });
|
||||
expect(callGatewayMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
method: "cron.add",
|
||||
scopes: ["operator.admin"],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("default-denies unknown methods by sending no scopes", async () => {
|
||||
callGatewayMock.mockResolvedValueOnce({ ok: true });
|
||||
await callGatewayTool("nonexistent.method", {}, {});
|
||||
expect(callGatewayMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
method: "nonexistent.method",
|
||||
scopes: [],
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { loadConfig, resolveGatewayPort } from "../../config/config.js";
|
||||
import { callGateway } from "../../gateway/call.js";
|
||||
import { resolveLeastPrivilegeOperatorScopesForMethod } from "../../gateway/method-scopes.js";
|
||||
import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../../utils/message-channel.js";
|
||||
import { readStringParam } from "./common.js";
|
||||
|
||||
@@ -109,6 +110,7 @@ export async function callGatewayTool<T = Record<string, unknown>>(
|
||||
extra?: { expectFinal?: boolean },
|
||||
) {
|
||||
const gateway = resolveGatewayOptions(opts);
|
||||
const scopes = resolveLeastPrivilegeOperatorScopesForMethod(method);
|
||||
return await callGateway<T>({
|
||||
url: gateway.url,
|
||||
token: gateway.token,
|
||||
@@ -119,5 +121,6 @@ export async function callGatewayTool<T = Record<string, unknown>>(
|
||||
clientName: GATEWAY_CLIENT_NAMES.GATEWAY_CLIENT,
|
||||
clientDisplayName: "agent",
|
||||
mode: GATEWAY_CLIENT_MODES.BACKEND,
|
||||
scopes,
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user