Skip to content

Commit eba344e

Browse files
fix: align WebChat owner policy checks
1 parent 1e475c5 commit eba344e

6 files changed

Lines changed: 129 additions & 20 deletions

File tree

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

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,32 @@ describe("Agent-specific tool filtering", () => {
479479
expect(names).not.toContain("process");
480480
});
481481

482+
it("keeps core tools for owner WebChat while restricting non-owners", () => {
483+
const cfg: OpenClawConfig = {
484+
tools: {
485+
toolsBySender: {
486+
"*": { deny: ["exec", "process"] },
487+
},
488+
},
489+
};
490+
const createWebChatTools = (senderIsOwner: boolean) =>
491+
createOpenClawCodingTools({
492+
config: cfg,
493+
messageProvider: "webchat",
494+
senderIsOwner,
495+
workspaceDir: "/tmp/test-webchat-owner-policy",
496+
agentDir: "/tmp/agent-webchat-owner-policy",
497+
}).map((tool) => tool.name);
498+
499+
const ownerTools = createWebChatTools(true);
500+
const nonOwnerTools = createWebChatTools(false);
501+
502+
expect(ownerTools).toContain("exec");
503+
expect(ownerTools).toContain("process");
504+
expect(nonOwnerTools).not.toContain("exec");
505+
expect(nonOwnerTools).not.toContain("process");
506+
});
507+
482508
it("should let agent per-sender policy override global sender wildcard", () => {
483509
const cfg: OpenClawConfig = {
484510
tools: {

src/agents/conversation-capability-profile.test.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,46 @@ describe("resolveConversationCapabilityProfile", () => {
7171
expect(profile.policy.explicitToolDenylist).toEqual([]);
7272
});
7373

74+
it("exempts owner WebChat identified through the message channel", () => {
75+
const cfg: OpenClawConfig = {
76+
tools: {
77+
toolsBySender: {
78+
"*": { deny: ["exec", "process"] },
79+
},
80+
},
81+
};
82+
83+
const profile = resolveConversationCapabilityProfile({
84+
config: cfg,
85+
messageChannel: INTERNAL_MESSAGE_CHANNEL,
86+
chatType: "direct",
87+
senderIsOwner: true,
88+
});
89+
90+
expect(profile.policy.senderPolicy).toBeUndefined();
91+
expect(profile.policy.explicitToolDenylist).toEqual([]);
92+
});
93+
94+
it("keeps wildcard sender tool restrictions for non-owner WebChat", () => {
95+
const cfg: OpenClawConfig = {
96+
tools: {
97+
toolsBySender: {
98+
"*": { deny: ["exec", "process"] },
99+
},
100+
},
101+
};
102+
103+
const profile = resolveConversationCapabilityProfile({
104+
config: cfg,
105+
messageProvider: INTERNAL_MESSAGE_CHANNEL,
106+
chatType: "direct",
107+
senderIsOwner: false,
108+
});
109+
110+
expect(profile.policy.senderPolicy).toEqual({ deny: ["exec", "process"] });
111+
expect(profile.policy.explicitToolDenylist).toEqual(["exec", "process"]);
112+
});
113+
74114
it("keeps wildcard sender tool restrictions for owners on external channels", () => {
75115
const cfg: OpenClawConfig = {
76116
tools: {

src/agents/conversation-capability-profile.ts

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -208,21 +208,22 @@ export function resolveConversationCapabilityProfile(
208208
senderUsername: params.senderUsername,
209209
senderE164: params.senderE164,
210210
});
211-
// Owner WebChat intentionally has no sender identity. Its trusted owner state
212-
// must not fall through to the wildcard policy for external senders.
213-
const senderPolicy =
211+
// Owner WebChat intentionally has no external sender identity. Its trusted
212+
// owner state must not fall through to the wildcard policy for guests.
213+
const isOwnerInternalSession =
214214
params.senderIsOwner === true &&
215-
normalizeMessageChannel(messageProvider) === INTERNAL_MESSAGE_CHANNEL
216-
? undefined
217-
: resolveSenderToolPolicy({
218-
config: params.config,
219-
agentId: effective.agentId,
220-
messageProvider,
221-
senderId: params.senderId,
222-
senderName: params.senderName,
223-
senderUsername: params.senderUsername,
224-
senderE164: params.senderE164,
225-
});
215+
normalizeMessageChannel(messageProvider ?? params.messageChannel) === INTERNAL_MESSAGE_CHANNEL;
216+
const senderPolicy = isOwnerInternalSession
217+
? undefined
218+
: resolveSenderToolPolicy({
219+
config: params.config,
220+
agentId: effective.agentId,
221+
messageProvider,
222+
senderId: params.senderId,
223+
senderName: params.senderName,
224+
senderUsername: params.senderUsername,
225+
senderE164: params.senderE164,
226+
});
226227
const profilePolicy = resolveToolProfilePolicy(effective.profile);
227228
const providerProfilePolicy = resolveToolProfilePolicy(effective.providerProfile);
228229
const subagentSessionKey = params.sandboxSessionKey ?? params.sessionKey;

src/agents/harness/selection.test.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -556,6 +556,24 @@ describe("runAgentHarnessAttempt", () => {
556556
).toBeUndefined();
557557
});
558558

559+
it("keeps non-owner WebChat restricted by wildcard sender policy for plugin harnesses", () => {
560+
const config = {
561+
tools: {
562+
toolsBySender: {
563+
"*": { deny: ["*"] },
564+
},
565+
},
566+
} as OpenClawConfig;
567+
568+
expect(
569+
resolvePluginHarnessPolicyToolsAllow({
570+
...createAttemptParams(config),
571+
messageProvider: "webchat",
572+
senderIsOwner: false,
573+
}),
574+
).toEqual([]);
575+
});
576+
559577
it("leaves OpenClaw harness params unchanged for channel group sender deny-all policy", async () => {
560578
await runAgentHarnessAttempt({
561579
...createAttemptParams(groupSenderDenyAllConfig()),

src/auto-reply/reply/commands-learn.test.ts

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import { describe, expect, it } from "vitest";
33
import type { OpenClawConfig } from "../../config/types.openclaw.js";
44
import { DEFAULT_LEARN_REQUEST } from "../../skills/workshop/learn-prompt.js";
5+
import { INTERNAL_MESSAGE_CHANNEL } from "../../utils/message-channel.js";
56
import { handleLearnCommand } from "./commands-learn.js";
67
import type { HandleCommandsParams } from "./commands-types.js";
78

@@ -32,8 +33,8 @@ function buildLearnParams(
3233
return {
3334
cfg: { ...cfg, models: cfg.models ?? DEFAULT_TEST_MODELS },
3435
ctx: {
35-
Provider: "web",
36-
Surface: "web",
36+
Provider: INTERNAL_MESSAGE_CHANNEL,
37+
Surface: INTERNAL_MESSAGE_CHANNEL,
3738
CommandSource: "text",
3839
Body: commandBodyNormalized,
3940
RawBody: commandBodyNormalized,
@@ -47,15 +48,15 @@ function buildLearnParams(
4748
isAuthorizedSender: true,
4849
senderIsOwner: true,
4950
senderId: "tester",
50-
channel: "web",
51-
channelId: "web",
52-
surface: "web",
51+
channel: INTERNAL_MESSAGE_CHANNEL,
52+
channelId: INTERNAL_MESSAGE_CHANNEL,
53+
surface: INTERNAL_MESSAGE_CHANNEL,
5354
ownerList: [],
5455
rawBodyNormalized: commandBodyNormalized,
5556
},
5657
directives: {},
5758
elevated: { enabled: true, allowed: true, failures: [] },
58-
sessionKey: "agent:main:web:test",
59+
sessionKey: "agent:main:webchat:test",
5960
workspaceDir: "/tmp",
6061
provider: "openai",
6162
model: "gpt-5.5",
@@ -124,6 +125,28 @@ describe("learn command", () => {
124125
expect(result?.reply?.text).toContain("Skill workshop is not available on this agent");
125126
});
126127

128+
it("keeps the workshop available for owner WebChat under a wildcard sender policy", async () => {
129+
const params = buildLearnParams("/learn", {
130+
tools: { toolsBySender: { "*": { deny: ["skill_workshop"] } } },
131+
});
132+
133+
const result = await handleLearnCommand(params, true);
134+
135+
expect(result?.shouldContinue).toBe(true);
136+
});
137+
138+
it("keeps the wildcard sender policy for non-owner WebChat", async () => {
139+
const params = buildLearnParams("/learn", {
140+
tools: { toolsBySender: { "*": { deny: ["skill_workshop"] } } },
141+
});
142+
params.command.senderIsOwner = false;
143+
144+
const result = await handleLearnCommand(params, true);
145+
146+
expect(result?.shouldContinue).toBe(false);
147+
expect(result?.reply?.text).toContain("Skill workshop is not available on this agent");
148+
});
149+
127150
it("replies without continuing when the runtime tool allowlist is empty", async () => {
128151
const params = buildLearnParams("/learn");
129152
params.opts = { toolsAllow: [] };

src/auto-reply/reply/commands-learn.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ function workshopIsAvailable(params: HandleCommandsParams): boolean {
148148
senderName: params.ctx.SenderName,
149149
senderUsername: params.ctx.SenderUsername,
150150
senderE164: params.ctx.SenderE164,
151+
senderIsOwner: params.command.senderIsOwner,
151152
agentAccountId: params.command.accountId ?? params.ctx.AccountId,
152153
modelProvider: params.provider,
153154
modelId: params.model,

0 commit comments

Comments
 (0)