mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-12 21:06:02 +00:00
274 lines
8.6 KiB
TypeScript
274 lines
8.6 KiB
TypeScript
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { describe, expect, it } from "vitest";
|
|
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
|
import { INTERNAL_MESSAGE_CHANNEL } from "../utils/message-channel.js";
|
|
import { resolveConversationCapabilityProfile } from "./conversation-capability-profile.js";
|
|
|
|
describe("resolveConversationCapabilityProfile", () => {
|
|
it("prepares a direct conversation profile with sender tool restrictions", () => {
|
|
const cfg: OpenClawConfig = {
|
|
tools: {
|
|
toolsBySender: {
|
|
"id:guest": { deny: ["exec", "process"] },
|
|
},
|
|
},
|
|
};
|
|
|
|
const profile = resolveConversationCapabilityProfile({
|
|
config: cfg,
|
|
sessionKey: "agent:main:discord:dm:guest",
|
|
agentId: "main",
|
|
messageProvider: "discord",
|
|
chatType: "direct",
|
|
senderId: "guest",
|
|
modelProvider: "openai",
|
|
modelId: "gpt-5.5",
|
|
modelApi: "responses",
|
|
workspaceDir: "/tmp/openclaw-direct-profile",
|
|
cwd: "/tmp/openclaw-direct-profile/task",
|
|
agentDir: "/tmp/openclaw-agent-direct-profile",
|
|
skillsSnapshot: {
|
|
prompt: "",
|
|
skills: [{ name: "ops" }],
|
|
},
|
|
});
|
|
|
|
expect(profile.conversation.scope).toBe("direct");
|
|
expect(profile.policy.senderPolicy).toEqual({ deny: ["exec", "process"] });
|
|
expect(profile.policy.explicitToolDenylist).toEqual(["exec", "process"]);
|
|
expect(profile.model).toMatchObject({
|
|
provider: "openai",
|
|
id: "gpt-5.5",
|
|
api: "responses",
|
|
});
|
|
expect(profile.workspace).toMatchObject({
|
|
workspaceRoot: "/tmp/openclaw-direct-profile",
|
|
runtimeRoot: "/tmp/openclaw-direct-profile/task",
|
|
instructionRoot: "/tmp/openclaw-agent-direct-profile",
|
|
});
|
|
expect(profile.skills.snapshot?.skills).toEqual([{ name: "ops" }]);
|
|
});
|
|
|
|
it("exempts owner WebChat from wildcard sender tool restrictions", () => {
|
|
const cfg: OpenClawConfig = {
|
|
tools: {
|
|
toolsBySender: {
|
|
"*": { deny: ["exec", "process"] },
|
|
},
|
|
},
|
|
};
|
|
|
|
const profile = resolveConversationCapabilityProfile({
|
|
config: cfg,
|
|
messageProvider: INTERNAL_MESSAGE_CHANNEL,
|
|
chatType: "direct",
|
|
senderIsOwner: true,
|
|
});
|
|
|
|
expect(profile.policy.senderPolicy).toBeUndefined();
|
|
expect(profile.policy.explicitToolDenylist).toEqual([]);
|
|
});
|
|
|
|
it("exempts owner WebChat identified through the message channel", () => {
|
|
const cfg: OpenClawConfig = {
|
|
tools: {
|
|
toolsBySender: {
|
|
"*": { deny: ["exec", "process"] },
|
|
},
|
|
},
|
|
};
|
|
|
|
const profile = resolveConversationCapabilityProfile({
|
|
config: cfg,
|
|
messageChannel: INTERNAL_MESSAGE_CHANNEL,
|
|
chatType: "direct",
|
|
senderIsOwner: true,
|
|
});
|
|
|
|
expect(profile.policy.senderPolicy).toBeUndefined();
|
|
expect(profile.policy.explicitToolDenylist).toEqual([]);
|
|
});
|
|
|
|
it("keeps wildcard sender tool restrictions for non-owner WebChat", () => {
|
|
const cfg: OpenClawConfig = {
|
|
tools: {
|
|
toolsBySender: {
|
|
"*": { deny: ["exec", "process"] },
|
|
},
|
|
},
|
|
};
|
|
|
|
const profile = resolveConversationCapabilityProfile({
|
|
config: cfg,
|
|
messageProvider: INTERNAL_MESSAGE_CHANNEL,
|
|
chatType: "direct",
|
|
senderIsOwner: false,
|
|
});
|
|
|
|
expect(profile.policy.senderPolicy).toEqual({ deny: ["exec", "process"] });
|
|
expect(profile.policy.explicitToolDenylist).toEqual(["exec", "process"]);
|
|
});
|
|
|
|
it("keeps wildcard sender tool restrictions for owners on external channels", () => {
|
|
const cfg: OpenClawConfig = {
|
|
tools: {
|
|
toolsBySender: {
|
|
"*": { deny: ["exec", "process"] },
|
|
},
|
|
},
|
|
};
|
|
|
|
const profile = resolveConversationCapabilityProfile({
|
|
config: cfg,
|
|
messageProvider: "discord",
|
|
chatType: "direct",
|
|
senderIsOwner: true,
|
|
});
|
|
|
|
expect(profile.policy.senderPolicy).toEqual({ deny: ["exec", "process"] });
|
|
expect(profile.policy.explicitToolDenylist).toEqual(["exec", "process"]);
|
|
});
|
|
|
|
it("prepares a shared conversation profile with group per-sender restrictions", () => {
|
|
const cfg: OpenClawConfig = {
|
|
channels: {
|
|
whatsapp: {
|
|
groups: {
|
|
team: {
|
|
tools: { allow: ["read"] },
|
|
toolsBySender: {
|
|
"id:alice": { allow: ["read", "exec"] },
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
};
|
|
|
|
const profile = resolveConversationCapabilityProfile({
|
|
config: cfg,
|
|
sessionKey: "agent:main:whatsapp:group:team",
|
|
agentId: "main",
|
|
messageProvider: "whatsapp",
|
|
chatType: "group",
|
|
groupId: "team",
|
|
senderId: "alice",
|
|
modelProvider: "openai",
|
|
modelId: "gpt-5.5",
|
|
workspaceDir: "/tmp/openclaw-shared-profile",
|
|
});
|
|
|
|
expect(profile.conversation.scope).toBe("shared");
|
|
expect(profile.policy.trustedGroup).toEqual({ groupId: "team", dropped: false });
|
|
expect(profile.policy.groupPolicy).toEqual({ allow: ["read", "exec"] });
|
|
expect(profile.policy.explicitToolAllowlist).toEqual(["read", "exec"]);
|
|
});
|
|
|
|
it("keeps built-in profile grants out of explicit overrides", () => {
|
|
const profile = resolveConversationCapabilityProfile({
|
|
config: {
|
|
tools: {
|
|
profile: "coding",
|
|
allow: ["pdf"],
|
|
},
|
|
},
|
|
modelProvider: "ollama",
|
|
modelId: "qwen3.5:9b",
|
|
});
|
|
|
|
expect(profile.policy.explicitToolAllowlist).toContain("image_generate");
|
|
expect(profile.policy.explicitToolOverrideAllowlist).toEqual(["pdf"]);
|
|
});
|
|
|
|
it("keeps inherited subagent grants out of explicit overrides", () => {
|
|
const storePath = path.join(
|
|
os.tmpdir(),
|
|
`openclaw-capability-profile-inherited-${Date.now()}-${Math.random().toString(16).slice(2)}.json`,
|
|
);
|
|
fs.writeFileSync(
|
|
storePath,
|
|
JSON.stringify({
|
|
"agent:main:subagent:limited": {
|
|
sessionId: "limited-session",
|
|
updatedAt: Date.now(),
|
|
spawnDepth: 1,
|
|
subagentRole: "orchestrator",
|
|
subagentControlScope: "children",
|
|
inheritedToolAllow: ["image_generate"],
|
|
},
|
|
}),
|
|
);
|
|
|
|
try {
|
|
const profile = resolveConversationCapabilityProfile({
|
|
config: { session: { store: storePath } },
|
|
sessionKey: "agent:main:subagent:limited",
|
|
agentId: "main",
|
|
modelProvider: "ollama",
|
|
modelId: "qwen3.5:9b",
|
|
});
|
|
|
|
expect(profile.policy.explicitToolAllowlist).toContain("image_generate");
|
|
expect(profile.policy.explicitToolOverrideAllowlist).not.toContain("image_generate");
|
|
} finally {
|
|
fs.rmSync(storePath, { force: true });
|
|
}
|
|
});
|
|
|
|
it("does not classify the conversation as shared from a dropped caller group id", () => {
|
|
// Non-group session key cannot vouch for the caller-supplied group facts:
|
|
// the trust check drops them, so scope must stay unknown instead of
|
|
// reflecting untrusted input that the profile itself publishes as null.
|
|
const profile = resolveConversationCapabilityProfile({
|
|
sessionKey: "agent:main:discord:dm:guest",
|
|
agentId: "main",
|
|
messageProvider: "discord",
|
|
groupId: "team",
|
|
groupChannel: "#general",
|
|
groupSpace: "guild-1",
|
|
senderId: "guest",
|
|
});
|
|
|
|
expect(profile.policy.trustedGroup).toEqual({ groupId: null, dropped: true });
|
|
expect(profile.conversation.groupId).toBeNull();
|
|
expect(profile.conversation.groupChannel).toBeNull();
|
|
expect(profile.conversation.groupSpace).toBeNull();
|
|
expect(profile.conversation.scope).toBe("unknown");
|
|
});
|
|
|
|
it("classifies group-scoped session keys as shared without a live chat type", () => {
|
|
const profile = resolveConversationCapabilityProfile({
|
|
sessionKey: "agent:main:whatsapp:group:team",
|
|
agentId: "main",
|
|
messageProvider: "whatsapp",
|
|
});
|
|
|
|
expect(profile.conversation.scope).toBe("shared");
|
|
});
|
|
|
|
it("classifies shared scope from the live run session key behind a sandbox policy key", () => {
|
|
const profile = resolveConversationCapabilityProfile({
|
|
sessionKey: "agent:main:main",
|
|
runSessionKey: "agent:main:telegram:group:ops",
|
|
agentId: "main",
|
|
messageProvider: "telegram",
|
|
});
|
|
|
|
expect(profile.conversation.scope).toBe("shared");
|
|
});
|
|
|
|
it("keeps trusted caller group facts shared when the session key vouches for them", () => {
|
|
const profile = resolveConversationCapabilityProfile({
|
|
sessionKey: "agent:main:whatsapp:group:team",
|
|
agentId: "main",
|
|
messageProvider: "whatsapp",
|
|
groupId: "team",
|
|
});
|
|
|
|
expect(profile.policy.trustedGroup).toEqual({ groupId: "team", dropped: false });
|
|
expect(profile.conversation.scope).toBe("shared");
|
|
});
|
|
});
|