Security: owner-only tools + command auth hardening (#9202)
* Security: gate whatsapp_login by sender auth * Security: treat undefined senderAuthorized as unauthorized (opt-in) * fix: gate whatsapp_login to owner senders (#8768) (thanks @victormier) * fix: add explicit owner allowlist for tools (#8768) (thanks @victormier) * fix: normalize escaped newlines in send actions (#8768) (thanks @victormier) --------- Co-authored-by: Victor Mier <victormier@gmail.com>
This commit is contained in:
committed by
GitHub
parent
0cd47d830f
commit
392bbddf29
@@ -88,6 +88,8 @@ export type CompactEmbeddedPiSessionParams = {
|
||||
groupSpace?: string | null;
|
||||
/** Parent session key for subagent policy inheritance. */
|
||||
spawnedBy?: string | null;
|
||||
/** Whether the sender is an owner (required for owner-only tools). */
|
||||
senderIsOwner?: boolean;
|
||||
sessionFile: string;
|
||||
workspaceDir: string;
|
||||
agentDir?: string;
|
||||
@@ -227,6 +229,7 @@ export async function compactEmbeddedPiSessionDirect(
|
||||
groupChannel: params.groupChannel,
|
||||
groupSpace: params.groupSpace,
|
||||
spawnedBy: params.spawnedBy,
|
||||
senderIsOwner: params.senderIsOwner,
|
||||
agentDir,
|
||||
workspaceDir: effectiveWorkspace,
|
||||
config: params.config,
|
||||
|
||||
@@ -324,6 +324,7 @@ export async function runEmbeddedPiAgent(
|
||||
groupChannel: params.groupChannel,
|
||||
groupSpace: params.groupSpace,
|
||||
spawnedBy: params.spawnedBy,
|
||||
senderIsOwner: params.senderIsOwner,
|
||||
currentChannelId: params.currentChannelId,
|
||||
currentThreadTs: params.currentThreadTs,
|
||||
replyToMode: params.replyToMode,
|
||||
@@ -391,6 +392,7 @@ export async function runEmbeddedPiAgent(
|
||||
agentDir,
|
||||
config: params.config,
|
||||
skillsSnapshot: params.skillsSnapshot,
|
||||
senderIsOwner: params.senderIsOwner,
|
||||
provider,
|
||||
model: modelId,
|
||||
thinkLevel,
|
||||
|
||||
@@ -225,6 +225,7 @@ export async function runEmbeddedAttempt(
|
||||
senderName: params.senderName,
|
||||
senderUsername: params.senderUsername,
|
||||
senderE164: params.senderE164,
|
||||
senderIsOwner: params.senderIsOwner,
|
||||
sessionKey: params.sessionKey ?? params.sessionId,
|
||||
agentDir,
|
||||
workspaceDir: effectiveWorkspace,
|
||||
|
||||
@@ -39,6 +39,8 @@ export type RunEmbeddedPiAgentParams = {
|
||||
senderName?: string | null;
|
||||
senderUsername?: string | null;
|
||||
senderE164?: string | null;
|
||||
/** Whether the sender is an owner (required for owner-only tools). */
|
||||
senderIsOwner?: boolean;
|
||||
/** Current channel ID for auto-threading (Slack). */
|
||||
currentChannelId?: string;
|
||||
/** Current thread timestamp for auto-threading (Slack). */
|
||||
|
||||
@@ -31,6 +31,8 @@ export type EmbeddedRunAttemptParams = {
|
||||
senderName?: string | null;
|
||||
senderUsername?: string | null;
|
||||
senderE164?: string | null;
|
||||
/** Whether the sender is an owner (required for owner-only tools). */
|
||||
senderIsOwner?: boolean;
|
||||
currentChannelId?: string;
|
||||
currentThreadTs?: string;
|
||||
replyToMode?: "off" | "first" | "all";
|
||||
|
||||
@@ -44,6 +44,7 @@ import {
|
||||
} from "./pi-tools.read.js";
|
||||
import { cleanToolSchemaForGemini, normalizeToolParameters } from "./pi-tools.schema.js";
|
||||
import {
|
||||
applyOwnerOnlyToolPolicy,
|
||||
buildPluginToolGroups,
|
||||
collectExplicitAllowlist,
|
||||
expandPolicyWithPluginGroups,
|
||||
@@ -161,6 +162,8 @@ export function createOpenClawCodingTools(options?: {
|
||||
requireExplicitMessageTarget?: boolean;
|
||||
/** If true, omit the message tool from the tool list. */
|
||||
disableMessageTool?: boolean;
|
||||
/** Whether the sender is an owner (required for owner-only tools). */
|
||||
senderIsOwner?: boolean;
|
||||
}): AnyAgentTool[] {
|
||||
const execToolName = "exec";
|
||||
const sandbox = options?.sandbox?.enabled ? options.sandbox : undefined;
|
||||
@@ -357,14 +360,17 @@ export function createOpenClawCodingTools(options?: {
|
||||
requesterAgentIdOverride: agentId,
|
||||
}),
|
||||
];
|
||||
// Security: treat unknown/undefined as unauthorized (opt-in, not opt-out)
|
||||
const senderIsOwner = options?.senderIsOwner === true;
|
||||
const toolsByAuthorization = applyOwnerOnlyToolPolicy(tools, senderIsOwner);
|
||||
const coreToolNames = new Set(
|
||||
tools
|
||||
toolsByAuthorization
|
||||
.filter((tool) => !getPluginToolMeta(tool))
|
||||
.map((tool) => normalizeToolName(tool.name))
|
||||
.filter(Boolean),
|
||||
);
|
||||
const pluginGroups = buildPluginToolGroups({
|
||||
tools,
|
||||
tools: toolsByAuthorization,
|
||||
toolMeta: (tool) => getPluginToolMeta(tool),
|
||||
});
|
||||
const resolvePolicy = (policy: typeof profilePolicy, label: string) => {
|
||||
@@ -401,8 +407,8 @@ export function createOpenClawCodingTools(options?: {
|
||||
const subagentPolicyExpanded = expandPolicyWithPluginGroups(subagentPolicy, pluginGroups);
|
||||
|
||||
const toolsFiltered = profilePolicyExpanded
|
||||
? filterToolsByPolicy(tools, profilePolicyExpanded)
|
||||
: tools;
|
||||
? filterToolsByPolicy(toolsByAuthorization, profilePolicyExpanded)
|
||||
: toolsByAuthorization;
|
||||
const providerProfileFiltered = providerProfileExpanded
|
||||
? filterToolsByPolicy(toolsFiltered, providerProfileExpanded)
|
||||
: toolsFiltered;
|
||||
|
||||
35
src/agents/pi-tools.whatsapp-login-gating.test.ts
Normal file
35
src/agents/pi-tools.whatsapp-login-gating.test.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import "./test-helpers/fast-coding-tools.js";
|
||||
import { createOpenClawCodingTools } from "./pi-tools.js";
|
||||
|
||||
vi.mock("./channel-tools.js", () => {
|
||||
const stubTool = (name: string) => ({
|
||||
name,
|
||||
description: `${name} stub`,
|
||||
parameters: { type: "object", properties: {} },
|
||||
execute: vi.fn(),
|
||||
});
|
||||
return {
|
||||
listChannelAgentTools: () => [stubTool("whatsapp_login")],
|
||||
};
|
||||
});
|
||||
|
||||
describe("whatsapp_login tool gating", () => {
|
||||
it("removes whatsapp_login for unauthorized senders", () => {
|
||||
const tools = createOpenClawCodingTools({ senderIsOwner: false });
|
||||
const toolNames = tools.map((tool) => tool.name);
|
||||
expect(toolNames).not.toContain("whatsapp_login");
|
||||
});
|
||||
|
||||
it("keeps whatsapp_login for authorized senders", () => {
|
||||
const tools = createOpenClawCodingTools({ senderIsOwner: true });
|
||||
const toolNames = tools.map((tool) => tool.name);
|
||||
expect(toolNames).toContain("whatsapp_login");
|
||||
});
|
||||
|
||||
it("defaults to removing whatsapp_login when owner status is unknown", () => {
|
||||
const tools = createOpenClawCodingTools();
|
||||
const toolNames = tools.map((tool) => tool.name);
|
||||
expect(toolNames).not.toContain("whatsapp_login");
|
||||
});
|
||||
});
|
||||
@@ -1,3 +1,5 @@
|
||||
import type { AnyAgentTool } from "./tools/common.js";
|
||||
|
||||
export type ToolProfileId = "minimal" | "coding" | "messaging" | "full";
|
||||
|
||||
type ToolProfilePolicy = {
|
||||
@@ -56,6 +58,8 @@ export const TOOL_GROUPS: Record<string, string[]> = {
|
||||
],
|
||||
};
|
||||
|
||||
const OWNER_ONLY_TOOL_NAMES = new Set<string>(["whatsapp_login"]);
|
||||
|
||||
const TOOL_PROFILES: Record<ToolProfileId, ToolProfilePolicy> = {
|
||||
minimal: {
|
||||
allow: ["session_status"],
|
||||
@@ -80,6 +84,31 @@ export function normalizeToolName(name: string) {
|
||||
return TOOL_NAME_ALIASES[normalized] ?? normalized;
|
||||
}
|
||||
|
||||
export function isOwnerOnlyToolName(name: string) {
|
||||
return OWNER_ONLY_TOOL_NAMES.has(normalizeToolName(name));
|
||||
}
|
||||
|
||||
export function applyOwnerOnlyToolPolicy(tools: AnyAgentTool[], senderIsOwner: boolean) {
|
||||
const withGuard = tools.map((tool) => {
|
||||
if (!isOwnerOnlyToolName(tool.name)) {
|
||||
return tool;
|
||||
}
|
||||
if (senderIsOwner || !tool.execute) {
|
||||
return tool;
|
||||
}
|
||||
return {
|
||||
...tool,
|
||||
execute: async () => {
|
||||
throw new Error("Tool restricted to owner senders.");
|
||||
},
|
||||
};
|
||||
});
|
||||
if (senderIsOwner) {
|
||||
return withGuard;
|
||||
}
|
||||
return withGuard.filter((tool) => !isOwnerOnlyToolName(tool.name));
|
||||
}
|
||||
|
||||
export function normalizeToolList(list?: string[]) {
|
||||
if (!list) {
|
||||
return [];
|
||||
|
||||
Reference in New Issue
Block a user