Skip to content

Commit 530b892

Browse files
JerranCsteipete
andauthored
feat(tools): add per-sender tool policies (#66933)
Co-authored-by: Peter Steinberger <[email protected]>
1 parent 2401ece commit 530b892

17 files changed

Lines changed: 276 additions & 9 deletions

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ Docs: https://docs.openclaw.ai
66

77
### Changes
88

9+
- Agents/tools: add per-sender tool policies with canonical channel-scoped sender keys, so operators can restrict dangerous tools by requester identity across global, agent, group, core, bundled, and plugin tool surfaces. (#66933) Thanks @JerranC.
910
- CI: add a non-blocking `plugin-inspector-advisory` artifact to Plugin Prerelease so release runs capture bundled plugin compatibility triage without changing the blocking gate.
1011
- Runtime/Fly: detect Fly Machines as container environments from their runtime env vars, so gateway bind and Bonjour defaults match remote container launches. (#80209) Thanks @liorb-mountapps.
1112
- Providers/fal: route GPT Image 2 and Nano Banana 2 reference-image edit requests to `/edit` with `image_urls` array, enforce NB2 edit geometry using `aspect_ratio` and `resolution` params, lift Fal edit mode input-image caps to 10 for GPT Image 2 and 14 for Nano Banana 2, and allow aspect-ratio hints in edit mode. (#77295) Thanks @leoge007.
Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
c963f607273fcce55080dc6d8068d8e124a4aa111a8ecf04807ebef98dfa5fd5 config-baseline.json
2-
647f7a12deed46b4a962848a17ed5666d24fc526b777feab62cf331d84ce957d config-baseline.core.json
1+
2800c9a6377fd02cb55bbf2577d63f2acc7193ed89be6fcffd32439893eaabc1 config-baseline.json
2+
ebbf966b41d99cd17282553f7882d1b826782599cbca6719b6f2c25b0e6b235d config-baseline.core.json
33
222d0338d6ed290870cac70cdf5e390bc1bb60c4462e46f847003bafe25c5a6e config-baseline.channel.json
44
18f71e9d4a62fe68fbd5bf18d5833a4e380fc705ad641769e1cf05794286344c config-baseline.plugin.json

docs/channels/groups.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ Replying to a bot message counts as an implicit mention when the channel support
362362
Some channel configs support restricting which tools are available **inside a specific group/room/channel**.
363363

364364
- `tools`: allow/deny tools for the whole group.
365-
- `toolsBySender`: per-sender overrides within the group. Use explicit key prefixes: `id:<senderId>`, `e164:<phone>`, `username:<handle>`, `name:<displayName>`, and `"*"` wildcard. Legacy unprefixed keys are still accepted and matched as `id:` only.
365+
- `toolsBySender`: per-sender overrides within the group. Use explicit key prefixes: `channel:<channelId>:<senderId>`, `id:<senderId>`, `e164:<phone>`, `username:<handle>`, `name:<displayName>`, and `"*"` wildcard. Channel ids use canonical OpenClaw channel ids; aliases such as `teams` normalize to `msteams`. Legacy unprefixed keys are still accepted and matched as `id:` only.
366366

367367
Resolution order (most specific wins):
368368

docs/channels/msteams.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -700,7 +700,7 @@ Key settings (see `/gateway/configuration` for shared channel patterns):
700700
- `channels.msteams.teams.<teamId>.channels.<conversationId>.tools`: per-channel tool policy overrides (`allow`/`deny`/`alsoAllow`).
701701
- `channels.msteams.teams.<teamId>.channels.<conversationId>.toolsBySender`: per-channel per-sender tool policy overrides (`"*"` wildcard supported).
702702
- `toolsBySender` keys should use explicit prefixes:
703-
`id:`, `e164:`, `username:`, `name:` (legacy unprefixed keys still map to `id:` only).
703+
`channel:`, `id:`, `e164:`, `username:`, `name:` (legacy unprefixed keys still map to `id:` only).
704704
- `channels.msteams.actions.memberInfo`: enable or disable the Graph-backed member info action (default: enabled when Graph credentials are available).
705705
- `channels.msteams.authType`: authentication type - `"secret"` (default) or `"federated"`.
706706
- `channels.msteams.certificatePath`: path to PEM certificate file (federated + certificate auth).

docs/channels/slack.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -915,7 +915,7 @@ Current Slack message actions include `send`, `upload-file`, `download-file`, `r
915915
- `skills`
916916
- `systemPrompt`
917917
- `tools`, `toolsBySender`
918-
- `toolsBySender` key format: `id:`, `e164:`, `username:`, `name:`, or `"*"` wildcard
918+
- `toolsBySender` key format: `channel:`, `id:`, `e164:`, `username:`, `name:`, or `"*"` wildcard
919919
(legacy unprefixed keys still map to `id:` only)
920920

921921
`allowBots` is conservative for channels and private channels: bot-authored room messages are accepted only when the sending bot is explicitly listed in that room's `users` allowlist, or when at least one explicit Slack owner ID from `channels.slack.allowFrom` is currently a room member. Wildcards and display-name owner entries do not satisfy owner presence. Owner presence uses Slack `conversations.members`; make sure the app has the matching read scope for the room type (`channels:read` for public channels, `groups:read` for private channels). If the member lookup fails, OpenClaw drops the bot-authored room message.

docs/gateway/config-tools.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,26 @@ Further restrict tools for specific providers or models. Order: base profile →
7878
}
7979
```
8080

81+
### `tools.toolsBySender`
82+
83+
Restricts tools for a specific requester identity. This is defense-in-depth on top of channel access control; sender values must come from the channel adapter, not message text.
84+
85+
```json5
86+
{
87+
tools: {
88+
toolsBySender: {
89+
"channel:discord:1234567890123": { alsoAllow: ["group:fs"] },
90+
"id:guest-user-id": { deny: ["group:runtime", "group:fs"] },
91+
"*": { deny: ["exec", "process", "write", "edit", "apply_patch"] },
92+
},
93+
},
94+
}
95+
```
96+
97+
Keys use explicit prefixes: `channel:<channelId>:<senderId>`, `id:<senderId>`, `e164:<phone>`, `username:<handle>`, `name:<displayName>`, or `"*"`. Channel ids are canonical OpenClaw ids; aliases such as `teams` normalize to `msteams`. Legacy unprefixed keys are accepted as `id:` only. Matching order is channel+id, id, e164, username, name, then wildcard.
98+
99+
Per-agent `agents.list[].tools.toolsBySender` overrides the global sender match when it matches, even with an empty `{}` policy.
100+
81101
### `tools.elevated`
82102

83103
Controls elevated exec access outside the sandbox:

src/agents/pi-embedded-runner/effective-tool-policy.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,24 @@ describe("applyFinalEffectiveToolPolicy", () => {
4646
expect(filtered.map((tool) => tool.name)).toEqual(["mcp__bundle__fs_read"]);
4747
});
4848

49+
it("applies channel-normalized per-sender policy to bundled tools", () => {
50+
const filtered = applyFinalEffectiveToolPolicy({
51+
bundledTools: [makeTool("mcp__bundle__exec"), makeTool("mcp__bundle__read")],
52+
config: {
53+
tools: {
54+
toolsBySender: {
55+
"channel:msteams:alice": { deny: ["mcp__bundle__exec"] },
56+
},
57+
},
58+
},
59+
messageProvider: "teams",
60+
senderId: "alice",
61+
warn: () => {},
62+
});
63+
64+
expect(filtered.map((tool) => tool.name)).toEqual(["mcp__bundle__read"]);
65+
});
66+
4967
it("applies owner-only filtering to bundled tools", () => {
5068
const filtered = applyFinalEffectiveToolPolicy({
5169
bundledTools: [makeTool("mcp__bundle__read"), makeTool("mcp__bundle__admin", true)],

src/agents/pi-embedded-runner/effective-tool-policy.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
resolveTrustedGroupId,
77
resolveSubagentToolPolicyForSession,
88
} from "../pi-tools.policy.js";
9+
import { resolveSenderToolPolicy } from "../sender-tool-policy.js";
910
import {
1011
isSubagentEnvelopeSession,
1112
resolveSubagentCapabilityStore,
@@ -106,6 +107,15 @@ export function applyFinalEffectiveToolPolicy(
106107
senderUsername: params.senderUsername,
107108
senderE164: params.senderE164,
108109
});
110+
const senderPolicy = resolveSenderToolPolicy({
111+
config: params.config,
112+
agentId,
113+
messageProvider: params.messageProvider,
114+
senderId: params.senderId,
115+
senderName: params.senderName,
116+
senderUsername: params.senderUsername,
117+
senderE164: params.senderE164,
118+
});
109119
const profilePolicy = resolveToolProfilePolicy(profile);
110120
const providerProfilePolicy = resolveToolProfilePolicy(providerProfile);
111121
const profilePolicyWithAlsoAllow = mergeAlsoAllowPolicy(profilePolicy, profileAlsoAllow);
@@ -154,6 +164,7 @@ export function applyFinalEffectiveToolPolicy(
154164
agentPolicy,
155165
agentProviderPolicy,
156166
groupPolicy,
167+
senderPolicy,
157168
agentId,
158169
}),
159170
{ policy: params.sandboxToolPolicy, label: "sandbox tools.allow" },

src/agents/pi-tools-agent-config.test.ts

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -420,6 +420,65 @@ describe("Agent-specific tool filtering", () => {
420420
).toEqual({ allow: ["read"] });
421421
});
422422

423+
it("should apply global per-sender tool policy to core tools", () => {
424+
const cfg: OpenClawConfig = {
425+
tools: {
426+
toolsBySender: {
427+
"id:guest": { deny: ["exec", "process"] },
428+
},
429+
},
430+
};
431+
432+
const tools = createOpenClawCodingTools({
433+
config: cfg,
434+
messageProvider: "discord",
435+
senderId: "guest",
436+
workspaceDir: "/tmp/test-global-sender-policy",
437+
agentDir: "/tmp/agent-global-sender-policy",
438+
});
439+
const names = tools.map((tool) => tool.name);
440+
441+
expect(names).toContain("read");
442+
expect(names).not.toContain("exec");
443+
expect(names).not.toContain("process");
444+
});
445+
446+
it("should let agent per-sender policy override global sender wildcard", () => {
447+
const cfg: OpenClawConfig = {
448+
tools: {
449+
toolsBySender: {
450+
"*": { deny: ["exec"] },
451+
},
452+
},
453+
agents: {
454+
list: [
455+
{
456+
id: "trusted",
457+
workspace: "~/openclaw-trusted",
458+
tools: {
459+
toolsBySender: {
460+
"id:alice": {},
461+
},
462+
},
463+
},
464+
],
465+
},
466+
};
467+
468+
const tools = createOpenClawCodingTools({
469+
config: cfg,
470+
sessionKey: "agent:trusted:discord:dm:alice",
471+
messageProvider: "discord",
472+
senderId: "alice",
473+
workspaceDir: "/tmp/test-agent-sender-policy",
474+
agentDir: "/tmp/agent-sender-policy",
475+
});
476+
const names = tools.map((tool) => tool.name);
477+
478+
expect(names).toContain("read");
479+
expect(names).toContain("exec");
480+
});
481+
423482
it("should not let default sender policy override group tools", () => {
424483
const cfg: OpenClawConfig = {
425484
channels: {

src/agents/pi-tools.policy.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -561,6 +561,7 @@ export function resolveGroupToolPolicy(params: {
561561
const configTools = resolveChannelGroupToolsPolicy({
562562
cfg: params.config,
563563
channel,
564+
messageProvider: channel,
564565
groupId: groupIds[0],
565566
groupIdCandidates: groupIds.slice(1),
566567
accountId: params.accountId,

0 commit comments

Comments
 (0)