Merge branch 'pr-1757' into temp/landpr-1757
# Conflicts: # CHANGELOG.md
This commit is contained in:
commit
d5bf20f1cb
@ -7,6 +7,7 @@ Status: unreleased.
|
||||
|
||||
### Changes
|
||||
- macOS: limit project-local `node_modules/.bin` PATH preference to debug builds (reduce PATH hijacking risk).
|
||||
- Tools: add per-sender group tool policies and fix precedence. (#1757) Thanks @adam91holt.
|
||||
- Agents: summarize dropped messages during compaction safeguard pruning. (#2509) Thanks @jogi47.
|
||||
- Skills: add multi-image input support to Nano Banana Pro skill. (#1958) Thanks @tyler6204.
|
||||
- Agents: honor tools.exec.safeBins in exec allowlist checks. (#2281)
|
||||
|
||||
@ -298,8 +298,12 @@ ack reaction after the bot replies.
|
||||
- `guilds."*"`: default per-guild settings applied when no explicit entry exists.
|
||||
- `guilds.<id>.slug`: optional friendly slug used for display names.
|
||||
- `guilds.<id>.users`: optional per-guild user allowlist (ids or names).
|
||||
- `guilds.<id>.tools`: optional per-guild tool policy overrides (`allow`/`deny`/`alsoAllow`) used when the channel override is missing.
|
||||
- `guilds.<id>.toolsBySender`: optional per-sender tool policy overrides at the guild level (applies when the channel override is missing; `"*"` wildcard supported).
|
||||
- `guilds.<id>.channels.<channel>.allow`: allow/deny the channel when `groupPolicy="allowlist"`.
|
||||
- `guilds.<id>.channels.<channel>.requireMention`: mention gating for the channel.
|
||||
- `guilds.<id>.channels.<channel>.tools`: optional per-channel tool policy overrides (`allow`/`deny`/`alsoAllow`).
|
||||
- `guilds.<id>.channels.<channel>.toolsBySender`: optional per-sender tool policy overrides within the channel (`"*"` wildcard supported).
|
||||
- `guilds.<id>.channels.<channel>.users`: optional per-channel user allowlist.
|
||||
- `guilds.<id>.channels.<channel>.skills`: skill filter (omit = all skills, empty = none).
|
||||
- `guilds.<id>.channels.<channel>.systemPrompt`: extra system prompt for the channel (combined with channel topic).
|
||||
|
||||
@ -421,8 +421,12 @@ Key settings (see `/gateway/configuration` for shared channel patterns):
|
||||
- `channels.msteams.replyStyle`: `thread | top-level` (see [Reply Style](#reply-style-threads-vs-posts)).
|
||||
- `channels.msteams.teams.<teamId>.replyStyle`: per-team override.
|
||||
- `channels.msteams.teams.<teamId>.requireMention`: per-team override.
|
||||
- `channels.msteams.teams.<teamId>.tools`: default per-team tool policy overrides (`allow`/`deny`/`alsoAllow`) used when a channel override is missing.
|
||||
- `channels.msteams.teams.<teamId>.toolsBySender`: default per-team per-sender tool policy overrides (`"*"` wildcard supported).
|
||||
- `channels.msteams.teams.<teamId>.channels.<conversationId>.replyStyle`: per-channel override.
|
||||
- `channels.msteams.teams.<teamId>.channels.<conversationId>.requireMention`: per-channel override.
|
||||
- `channels.msteams.teams.<teamId>.channels.<conversationId>.tools`: per-channel tool policy overrides (`allow`/`deny`/`alsoAllow`).
|
||||
- `channels.msteams.teams.<teamId>.channels.<conversationId>.toolsBySender`: per-channel per-sender tool policy overrides (`"*"` wildcard supported).
|
||||
- `channels.msteams.sharePointSiteId`: SharePoint site ID for file uploads in group chats/channels (see [Sending files in group chats](#sending-files-in-group-chats)).
|
||||
|
||||
## Routing & Sessions
|
||||
|
||||
@ -464,6 +464,8 @@ For fine-grained control, use these tags in agent responses:
|
||||
Channel options (`channels.slack.channels.<id>` or `channels.slack.channels.<name>`):
|
||||
- `allow`: allow/deny the channel when `groupPolicy="allowlist"`.
|
||||
- `requireMention`: mention gating for the channel.
|
||||
- `tools`: optional per-channel tool policy overrides (`allow`/`deny`/`alsoAllow`).
|
||||
- `toolsBySender`: optional per-sender tool policy overrides within the channel (keys are sender ids/@handles/emails; `"*"` wildcard supported).
|
||||
- `allowBots`: allow bot-authored messages in this channel (default: false).
|
||||
- `users`: optional per-channel user allowlist.
|
||||
- `skills`: skill filter (omit = all skills, empty = none).
|
||||
|
||||
@ -232,6 +232,42 @@ Notes:
|
||||
- Discord defaults live in `channels.discord.guilds."*"` (overridable per guild/channel).
|
||||
- Group history context is wrapped uniformly across channels and is **pending-only** (messages skipped due to mention gating); use `messages.groupChat.historyLimit` for the global default and `channels.<channel>.historyLimit` (or `channels.<channel>.accounts.*.historyLimit`) for overrides. Set `0` to disable.
|
||||
|
||||
## Group/channel tool restrictions (optional)
|
||||
Some channel configs support restricting which tools are available **inside a specific group/room/channel**.
|
||||
|
||||
- `tools`: allow/deny tools for the whole group.
|
||||
- `toolsBySender`: per-sender overrides within the group (keys are sender IDs/usernames/emails/phone numbers depending on the channel). Use `"*"` as a wildcard.
|
||||
|
||||
Resolution order (most specific wins):
|
||||
1) group/channel `toolsBySender` match
|
||||
2) group/channel `tools`
|
||||
3) default (`"*"`) `toolsBySender` match
|
||||
4) default (`"*"`) `tools`
|
||||
|
||||
Example (Telegram):
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
telegram: {
|
||||
groups: {
|
||||
"*": { tools: { deny: ["exec"] } },
|
||||
"-1001234567890": {
|
||||
tools: { deny: ["exec", "read", "write"] },
|
||||
toolsBySender: {
|
||||
"123456789": { alsoAllow: ["exec"] }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Notes:
|
||||
- Group/channel tool restrictions are applied in addition to global/agent tool policy (deny still wins).
|
||||
- Some channels use different nesting for rooms/channels (e.g., Discord `guilds.*.channels.*`, Slack `channels.*`, MS Teams `teams.*.channels.*`).
|
||||
|
||||
## Group allowlists
|
||||
When `channels.whatsapp.groups`, `channels.telegram.groups`, or `channels.imessage.groups` is configured, the keys act as a group allowlist. Use `"*"` to allow all groups while still setting default mention behavior.
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ import type {
|
||||
import {
|
||||
buildChannelKeyCandidates,
|
||||
normalizeChannelSlug,
|
||||
resolveToolsBySender,
|
||||
resolveChannelEntryMatchWithFallback,
|
||||
resolveNestedAllowlistDecision,
|
||||
} from "clawdbot/plugin-sdk";
|
||||
@ -106,9 +107,36 @@ export function resolveMSTeamsGroupToolPolicy(
|
||||
});
|
||||
|
||||
if (resolved.channelConfig) {
|
||||
return resolved.channelConfig.tools ?? resolved.teamConfig?.tools;
|
||||
const senderPolicy = resolveToolsBySender({
|
||||
toolsBySender: resolved.channelConfig.toolsBySender,
|
||||
senderId: params.senderId,
|
||||
senderName: params.senderName,
|
||||
senderUsername: params.senderUsername,
|
||||
senderE164: params.senderE164,
|
||||
});
|
||||
if (senderPolicy) return senderPolicy;
|
||||
if (resolved.channelConfig.tools) return resolved.channelConfig.tools;
|
||||
const teamSenderPolicy = resolveToolsBySender({
|
||||
toolsBySender: resolved.teamConfig?.toolsBySender,
|
||||
senderId: params.senderId,
|
||||
senderName: params.senderName,
|
||||
senderUsername: params.senderUsername,
|
||||
senderE164: params.senderE164,
|
||||
});
|
||||
if (teamSenderPolicy) return teamSenderPolicy;
|
||||
return resolved.teamConfig?.tools;
|
||||
}
|
||||
if (resolved.teamConfig) {
|
||||
const teamSenderPolicy = resolveToolsBySender({
|
||||
toolsBySender: resolved.teamConfig.toolsBySender,
|
||||
senderId: params.senderId,
|
||||
senderName: params.senderName,
|
||||
senderUsername: params.senderUsername,
|
||||
senderE164: params.senderE164,
|
||||
});
|
||||
if (teamSenderPolicy) return teamSenderPolicy;
|
||||
if (resolved.teamConfig.tools) return resolved.teamConfig.tools;
|
||||
}
|
||||
if (resolved.teamConfig?.tools) return resolved.teamConfig.tools;
|
||||
|
||||
if (!groupId) return undefined;
|
||||
|
||||
@ -125,7 +153,24 @@ export function resolveMSTeamsGroupToolPolicy(
|
||||
normalizeKey: normalizeChannelSlug,
|
||||
});
|
||||
if (match.entry) {
|
||||
return match.entry.tools ?? teamConfig?.tools;
|
||||
const senderPolicy = resolveToolsBySender({
|
||||
toolsBySender: match.entry.toolsBySender,
|
||||
senderId: params.senderId,
|
||||
senderName: params.senderName,
|
||||
senderUsername: params.senderUsername,
|
||||
senderE164: params.senderE164,
|
||||
});
|
||||
if (senderPolicy) return senderPolicy;
|
||||
if (match.entry.tools) return match.entry.tools;
|
||||
const teamSenderPolicy = resolveToolsBySender({
|
||||
toolsBySender: teamConfig?.toolsBySender,
|
||||
senderId: params.senderId,
|
||||
senderName: params.senderName,
|
||||
senderUsername: params.senderUsername,
|
||||
senderE164: params.senderE164,
|
||||
});
|
||||
if (teamSenderPolicy) return teamSenderPolicy;
|
||||
return teamConfig?.tools;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -20,8 +20,10 @@ describe("gateway tool", () => {
|
||||
vi.useFakeTimers();
|
||||
const kill = vi.spyOn(process, "kill").mockImplementation(() => true);
|
||||
const previousStateDir = process.env.CLAWDBOT_STATE_DIR;
|
||||
const previousProfile = process.env.CLAWDBOT_PROFILE;
|
||||
const stateDir = await fs.mkdtemp(path.join(os.tmpdir(), "clawdbot-test-"));
|
||||
process.env.CLAWDBOT_STATE_DIR = stateDir;
|
||||
process.env.CLAWDBOT_PROFILE = "isolated";
|
||||
|
||||
try {
|
||||
const tool = createClawdbotTools({
|
||||
@ -62,6 +64,11 @@ describe("gateway tool", () => {
|
||||
} else {
|
||||
process.env.CLAWDBOT_STATE_DIR = previousStateDir;
|
||||
}
|
||||
if (previousProfile === undefined) {
|
||||
delete process.env.CLAWDBOT_PROFILE;
|
||||
} else {
|
||||
process.env.CLAWDBOT_PROFILE = previousProfile;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
import "./test-helpers/fast-coding-tools.js";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { ensureClawdbotModelsJson } from "./models-config.js";
|
||||
|
||||
|
||||
@ -215,6 +215,10 @@ export async function runEmbeddedAttempt(
|
||||
groupChannel: params.groupChannel,
|
||||
groupSpace: params.groupSpace,
|
||||
spawnedBy: params.spawnedBy,
|
||||
senderId: params.senderId,
|
||||
senderName: params.senderName,
|
||||
senderUsername: params.senderUsername,
|
||||
senderE164: params.senderE164,
|
||||
sessionKey: params.sessionKey ?? params.sessionId,
|
||||
agentDir,
|
||||
workspaceDir: effectiveWorkspace,
|
||||
|
||||
@ -35,6 +35,10 @@ export type RunEmbeddedPiAgentParams = {
|
||||
groupSpace?: string | null;
|
||||
/** Parent session key for subagent policy inheritance. */
|
||||
spawnedBy?: string | null;
|
||||
senderId?: string | null;
|
||||
senderName?: string | null;
|
||||
senderUsername?: string | null;
|
||||
senderE164?: string | null;
|
||||
/** Current channel ID for auto-threading (Slack). */
|
||||
currentChannelId?: string;
|
||||
/** Current thread timestamp for auto-threading (Slack). */
|
||||
|
||||
@ -31,6 +31,10 @@ export type EmbeddedRunAttemptParams = {
|
||||
groupSpace?: string | null;
|
||||
/** Parent session key for subagent policy inheritance. */
|
||||
spawnedBy?: string | null;
|
||||
senderId?: string | null;
|
||||
senderName?: string | null;
|
||||
senderUsername?: string | null;
|
||||
senderE164?: string | null;
|
||||
currentChannelId?: string;
|
||||
currentThreadTs?: string;
|
||||
replyToMode?: "off" | "first" | "all";
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import "./test-helpers/fast-coding-tools.js";
|
||||
import type { ClawdbotConfig } from "../config/config.js";
|
||||
import { createClawdbotCodingTools } from "./pi-tools.js";
|
||||
import type { SandboxDockerConfig } from "./sandbox.js";
|
||||
@ -270,6 +271,75 @@ describe("Agent-specific tool filtering", () => {
|
||||
expect(defaultNames).not.toContain("exec");
|
||||
});
|
||||
|
||||
it("should apply per-sender tool policies for group tools", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
channels: {
|
||||
whatsapp: {
|
||||
groups: {
|
||||
"*": {
|
||||
tools: { allow: ["read"] },
|
||||
toolsBySender: {
|
||||
alice: { allow: ["read", "exec"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const aliceTools = createClawdbotCodingTools({
|
||||
config: cfg,
|
||||
sessionKey: "agent:main:whatsapp:group:family",
|
||||
senderId: "alice",
|
||||
workspaceDir: "/tmp/test-group-sender",
|
||||
agentDir: "/tmp/agent-group-sender",
|
||||
});
|
||||
const aliceNames = aliceTools.map((t) => t.name);
|
||||
expect(aliceNames).toContain("read");
|
||||
expect(aliceNames).toContain("exec");
|
||||
|
||||
const bobTools = createClawdbotCodingTools({
|
||||
config: cfg,
|
||||
sessionKey: "agent:main:whatsapp:group:family",
|
||||
senderId: "bob",
|
||||
workspaceDir: "/tmp/test-group-sender-bob",
|
||||
agentDir: "/tmp/agent-group-sender",
|
||||
});
|
||||
const bobNames = bobTools.map((t) => t.name);
|
||||
expect(bobNames).toContain("read");
|
||||
expect(bobNames).not.toContain("exec");
|
||||
});
|
||||
|
||||
it("should not let default sender policy override group tools", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
channels: {
|
||||
whatsapp: {
|
||||
groups: {
|
||||
"*": {
|
||||
toolsBySender: {
|
||||
admin: { allow: ["read", "exec"] },
|
||||
},
|
||||
},
|
||||
locked: {
|
||||
tools: { allow: ["read"] },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
const adminTools = createClawdbotCodingTools({
|
||||
config: cfg,
|
||||
sessionKey: "agent:main:whatsapp:group:locked",
|
||||
senderId: "admin",
|
||||
workspaceDir: "/tmp/test-group-default-override",
|
||||
agentDir: "/tmp/agent-group-default-override",
|
||||
});
|
||||
const adminNames = adminTools.map((t) => t.name);
|
||||
expect(adminNames).toContain("read");
|
||||
expect(adminNames).not.toContain("exec");
|
||||
});
|
||||
|
||||
it("should resolve telegram group tool policy for topic session keys", () => {
|
||||
const cfg: ClawdbotConfig = {
|
||||
channels: {
|
||||
|
||||
@ -233,6 +233,10 @@ export function resolveGroupToolPolicy(params: {
|
||||
groupChannel?: string | null;
|
||||
groupSpace?: string | null;
|
||||
accountId?: string | null;
|
||||
senderId?: string | null;
|
||||
senderName?: string | null;
|
||||
senderUsername?: string | null;
|
||||
senderE164?: string | null;
|
||||
}): SandboxToolPolicy | undefined {
|
||||
if (!params.config) return undefined;
|
||||
const sessionContext = resolveGroupContextFromSessionKey(params.sessionKey);
|
||||
@ -255,12 +259,20 @@ export function resolveGroupToolPolicy(params: {
|
||||
groupChannel: params.groupChannel,
|
||||
groupSpace: params.groupSpace,
|
||||
accountId: params.accountId,
|
||||
senderId: params.senderId,
|
||||
senderName: params.senderName,
|
||||
senderUsername: params.senderUsername,
|
||||
senderE164: params.senderE164,
|
||||
}) ??
|
||||
resolveChannelGroupToolsPolicy({
|
||||
cfg: params.config,
|
||||
channel,
|
||||
groupId,
|
||||
accountId: params.accountId,
|
||||
senderId: params.senderId,
|
||||
senderName: params.senderName,
|
||||
senderUsername: params.senderUsername,
|
||||
senderE164: params.senderE164,
|
||||
});
|
||||
return pickToolPolicy(toolsConfig);
|
||||
}
|
||||
|
||||
@ -140,6 +140,10 @@ export function createClawdbotCodingTools(options?: {
|
||||
groupSpace?: string | null;
|
||||
/** Parent session key for subagent group policy inheritance. */
|
||||
spawnedBy?: string | null;
|
||||
senderId?: string | null;
|
||||
senderName?: string | null;
|
||||
senderUsername?: string | null;
|
||||
senderE164?: string | null;
|
||||
/** Reply-to mode for Slack auto-threading. */
|
||||
replyToMode?: "off" | "first" | "all";
|
||||
/** Mutable ref to track if a reply was sent (for "first" mode). */
|
||||
@ -174,6 +178,10 @@ export function createClawdbotCodingTools(options?: {
|
||||
groupChannel: options?.groupChannel,
|
||||
groupSpace: options?.groupSpace,
|
||||
accountId: options?.agentAccountId,
|
||||
senderId: options?.senderId,
|
||||
senderName: options?.senderName,
|
||||
senderUsername: options?.senderUsername,
|
||||
senderE164: options?.senderE164,
|
||||
});
|
||||
const profilePolicy = resolveToolProfilePolicy(profile);
|
||||
const providerProfilePolicy = resolveToolProfilePolicy(providerProfile);
|
||||
|
||||
@ -232,6 +232,10 @@ export async function runAgentTurnWithFallback(params: {
|
||||
groupChannel:
|
||||
params.sessionCtx.GroupChannel?.trim() ?? params.sessionCtx.GroupSubject?.trim(),
|
||||
groupSpace: params.sessionCtx.GroupSpace?.trim() ?? undefined,
|
||||
senderId: params.sessionCtx.SenderId?.trim() || undefined,
|
||||
senderName: params.sessionCtx.SenderName?.trim() || undefined,
|
||||
senderUsername: params.sessionCtx.SenderUsername?.trim() || undefined,
|
||||
senderE164: params.sessionCtx.SenderE164?.trim() || undefined,
|
||||
// Provider threading context for tool auto-injection
|
||||
...buildThreadingToolContext({
|
||||
sessionCtx: params.sessionCtx,
|
||||
|
||||
@ -115,6 +115,10 @@ export async function runMemoryFlushIfNeeded(params: {
|
||||
config: params.followupRun.run.config,
|
||||
hasRepliedRef: params.opts?.hasRepliedRef,
|
||||
}),
|
||||
senderId: params.sessionCtx.SenderId?.trim() || undefined,
|
||||
senderName: params.sessionCtx.SenderName?.trim() || undefined,
|
||||
senderUsername: params.sessionCtx.SenderUsername?.trim() || undefined,
|
||||
senderE164: params.sessionCtx.SenderE164?.trim() || undefined,
|
||||
sessionFile: params.followupRun.run.sessionFile,
|
||||
workspaceDir: params.followupRun.run.workspaceDir,
|
||||
agentDir: params.followupRun.run.agentDir,
|
||||
|
||||
@ -147,6 +147,10 @@ export function createFollowupRunner(params: {
|
||||
groupId: queued.run.groupId,
|
||||
groupChannel: queued.run.groupChannel,
|
||||
groupSpace: queued.run.groupSpace,
|
||||
senderId: queued.run.senderId,
|
||||
senderName: queued.run.senderName,
|
||||
senderUsername: queued.run.senderUsername,
|
||||
senderE164: queued.run.senderE164,
|
||||
sessionFile: queued.run.sessionFile,
|
||||
workspaceDir: queued.run.workspaceDir,
|
||||
config: queued.run.config,
|
||||
|
||||
@ -370,6 +370,10 @@ export async function runPreparedReply(
|
||||
groupId: resolveGroupSessionKey(sessionCtx)?.id ?? undefined,
|
||||
groupChannel: sessionCtx.GroupChannel?.trim() ?? sessionCtx.GroupSubject?.trim(),
|
||||
groupSpace: sessionCtx.GroupSpace?.trim() ?? undefined,
|
||||
senderId: sessionCtx.SenderId?.trim() || undefined,
|
||||
senderName: sessionCtx.SenderName?.trim() || undefined,
|
||||
senderUsername: sessionCtx.SenderUsername?.trim() || undefined,
|
||||
senderE164: sessionCtx.SenderE164?.trim() || undefined,
|
||||
sessionFile,
|
||||
workspaceDir,
|
||||
config: cfg,
|
||||
|
||||
@ -51,6 +51,10 @@ export type FollowupRun = {
|
||||
groupId?: string;
|
||||
groupChannel?: string;
|
||||
groupSpace?: string;
|
||||
senderId?: string;
|
||||
senderName?: string;
|
||||
senderUsername?: string;
|
||||
senderE164?: string;
|
||||
sessionFile: string;
|
||||
workspaceDir: string;
|
||||
config: ClawdbotConfig;
|
||||
|
||||
@ -14,7 +14,13 @@ function enhanceBrowserFetchError(url: string, err: unknown, timeoutMs: number):
|
||||
? "If this is a sandboxed session, ensure the sandbox browser is running and try again."
|
||||
: `Start (or restart) the Clawdbot gateway (Clawdbot.app menubar, or \`${formatCliCommand("clawdbot gateway")}\`) and try again.`;
|
||||
const msg = String(err);
|
||||
if (msg.toLowerCase().includes("timed out") || msg.toLowerCase().includes("timeout")) {
|
||||
const lower = msg.toLowerCase();
|
||||
if (
|
||||
lower.includes("timed out") ||
|
||||
lower.includes("timeout") ||
|
||||
lower.includes("aborted") ||
|
||||
lower.includes("aborterror")
|
||||
) {
|
||||
return new Error(
|
||||
`Can't reach the clawd browser control service (timed out after ${timeoutMs}ms). ${hint}`,
|
||||
);
|
||||
|
||||
@ -309,6 +309,9 @@ describe("backward compatibility (profile parameter)", () => {
|
||||
testPort = await getFreePort();
|
||||
_cdpBaseUrl = `http://127.0.0.1:${testPort + 1}`;
|
||||
|
||||
prevGatewayPort = process.env.CLAWDBOT_GATEWAY_PORT;
|
||||
process.env.CLAWDBOT_GATEWAY_PORT = String(testPort - 2);
|
||||
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async (url: string) => {
|
||||
@ -344,6 +347,11 @@ describe("backward compatibility (profile parameter)", () => {
|
||||
afterEach(async () => {
|
||||
vi.unstubAllGlobals();
|
||||
vi.restoreAllMocks();
|
||||
if (prevGatewayPort === undefined) {
|
||||
delete process.env.CLAWDBOT_GATEWAY_PORT;
|
||||
} else {
|
||||
process.env.CLAWDBOT_GATEWAY_PORT = prevGatewayPort;
|
||||
}
|
||||
const { stopBrowserControlServer } = await import("./server.js");
|
||||
await stopBrowserControlServer();
|
||||
});
|
||||
|
||||
@ -286,6 +286,9 @@ describe("profile CRUD endpoints", () => {
|
||||
testPort = await getFreePort();
|
||||
_cdpBaseUrl = `http://127.0.0.1:${testPort + 1}`;
|
||||
|
||||
prevGatewayPort = process.env.CLAWDBOT_GATEWAY_PORT;
|
||||
process.env.CLAWDBOT_GATEWAY_PORT = String(testPort - 2);
|
||||
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async (url: string) => {
|
||||
@ -299,6 +302,11 @@ describe("profile CRUD endpoints", () => {
|
||||
afterEach(async () => {
|
||||
vi.unstubAllGlobals();
|
||||
vi.restoreAllMocks();
|
||||
if (prevGatewayPort === undefined) {
|
||||
delete process.env.CLAWDBOT_GATEWAY_PORT;
|
||||
} else {
|
||||
process.env.CLAWDBOT_GATEWAY_PORT = prevGatewayPort;
|
||||
}
|
||||
const { stopBrowserControlServer } = await import("./server.js");
|
||||
await stopBrowserControlServer();
|
||||
});
|
||||
|
||||
@ -1 +1 @@
|
||||
6d63b866aa0e917b278c6bef42229e8cd1f43c8ba31c845a96b4d9d5ce780265
|
||||
2567ca5bbc065b922d96717a488d5db3120b5b033c5d0508682d1aa8fbba470a
|
||||
|
||||
@ -2,9 +2,13 @@ import type { ClawdbotConfig } from "../../config/config.js";
|
||||
import {
|
||||
resolveChannelGroupRequireMention,
|
||||
resolveChannelGroupToolsPolicy,
|
||||
resolveToolsBySender,
|
||||
} from "../../config/group-policy.js";
|
||||
import type { DiscordConfig } from "../../config/types.js";
|
||||
import type { GroupToolPolicyConfig } from "../../config/types.tools.js";
|
||||
import type {
|
||||
GroupToolPolicyBySenderConfig,
|
||||
GroupToolPolicyConfig,
|
||||
} from "../../config/types.tools.js";
|
||||
import { resolveSlackAccount } from "../../slack/accounts.js";
|
||||
|
||||
type GroupMentionParams = {
|
||||
@ -13,6 +17,10 @@ type GroupMentionParams = {
|
||||
groupChannel?: string | null;
|
||||
groupSpace?: string | null;
|
||||
accountId?: string | null;
|
||||
senderId?: string | null;
|
||||
senderName?: string | null;
|
||||
senderUsername?: string | null;
|
||||
senderE164?: string | null;
|
||||
};
|
||||
|
||||
function normalizeDiscordSlug(value?: string | null) {
|
||||
@ -172,6 +180,10 @@ export function resolveGoogleChatGroupToolPolicy(
|
||||
channel: "googlechat",
|
||||
groupId: params.groupId,
|
||||
accountId: params.accountId,
|
||||
senderId: params.senderId,
|
||||
senderName: params.senderName,
|
||||
senderUsername: params.senderUsername,
|
||||
senderE164: params.senderE164,
|
||||
});
|
||||
}
|
||||
|
||||
@ -226,6 +238,10 @@ export function resolveTelegramGroupToolPolicy(
|
||||
channel: "telegram",
|
||||
groupId: chatId ?? params.groupId,
|
||||
accountId: params.accountId,
|
||||
senderId: params.senderId,
|
||||
senderName: params.senderName,
|
||||
senderUsername: params.senderUsername,
|
||||
senderE164: params.senderE164,
|
||||
});
|
||||
}
|
||||
|
||||
@ -237,6 +253,10 @@ export function resolveWhatsAppGroupToolPolicy(
|
||||
channel: "whatsapp",
|
||||
groupId: params.groupId,
|
||||
accountId: params.accountId,
|
||||
senderId: params.senderId,
|
||||
senderName: params.senderName,
|
||||
senderUsername: params.senderUsername,
|
||||
senderE164: params.senderE164,
|
||||
});
|
||||
}
|
||||
|
||||
@ -248,6 +268,10 @@ export function resolveIMessageGroupToolPolicy(
|
||||
channel: "imessage",
|
||||
groupId: params.groupId,
|
||||
accountId: params.accountId,
|
||||
senderId: params.senderId,
|
||||
senderName: params.senderName,
|
||||
senderUsername: params.senderUsername,
|
||||
senderE164: params.senderE164,
|
||||
});
|
||||
}
|
||||
|
||||
@ -268,8 +292,24 @@ export function resolveDiscordGroupToolPolicy(
|
||||
? (channelEntries[channelSlug] ?? channelEntries[`#${channelSlug}`])
|
||||
: undefined) ??
|
||||
(groupChannel ? channelEntries[normalizeDiscordSlug(groupChannel)] : undefined);
|
||||
const senderPolicy = resolveToolsBySender({
|
||||
toolsBySender: entry?.toolsBySender,
|
||||
senderId: params.senderId,
|
||||
senderName: params.senderName,
|
||||
senderUsername: params.senderUsername,
|
||||
senderE164: params.senderE164,
|
||||
});
|
||||
if (senderPolicy) return senderPolicy;
|
||||
if (entry?.tools) return entry.tools;
|
||||
}
|
||||
const guildSenderPolicy = resolveToolsBySender({
|
||||
toolsBySender: guildEntry?.toolsBySender,
|
||||
senderId: params.senderId,
|
||||
senderName: params.senderName,
|
||||
senderUsername: params.senderUsername,
|
||||
senderE164: params.senderE164,
|
||||
});
|
||||
if (guildSenderPolicy) return guildSenderPolicy;
|
||||
if (guildEntry?.tools) return guildEntry.tools;
|
||||
return undefined;
|
||||
}
|
||||
@ -294,7 +334,9 @@ export function resolveSlackGroupToolPolicy(
|
||||
channelName ?? "",
|
||||
normalizedName,
|
||||
].filter(Boolean);
|
||||
let matched: { tools?: GroupToolPolicyConfig } | undefined;
|
||||
let matched:
|
||||
| { tools?: GroupToolPolicyConfig; toolsBySender?: GroupToolPolicyBySenderConfig }
|
||||
| undefined;
|
||||
for (const candidate of candidates) {
|
||||
if (candidate && channels[candidate]) {
|
||||
matched = channels[candidate];
|
||||
@ -302,6 +344,14 @@ export function resolveSlackGroupToolPolicy(
|
||||
}
|
||||
}
|
||||
const resolved = matched ?? channels["*"];
|
||||
const senderPolicy = resolveToolsBySender({
|
||||
toolsBySender: resolved?.toolsBySender,
|
||||
senderId: params.senderId,
|
||||
senderName: params.senderName,
|
||||
senderUsername: params.senderUsername,
|
||||
senderE164: params.senderE164,
|
||||
});
|
||||
if (senderPolicy) return senderPolicy;
|
||||
if (resolved?.tools) return resolved.tools;
|
||||
return undefined;
|
||||
}
|
||||
@ -314,5 +364,9 @@ export function resolveBlueBubblesGroupToolPolicy(
|
||||
channel: "bluebubbles",
|
||||
groupId: params.groupId,
|
||||
accountId: params.accountId,
|
||||
senderId: params.senderId,
|
||||
senderName: params.senderName,
|
||||
senderUsername: params.senderUsername,
|
||||
senderE164: params.senderE164,
|
||||
});
|
||||
}
|
||||
|
||||
@ -155,6 +155,10 @@ export type ChannelGroupContext = {
|
||||
groupChannel?: string | null;
|
||||
groupSpace?: string | null;
|
||||
accountId?: string | null;
|
||||
senderId?: string | null;
|
||||
senderName?: string | null;
|
||||
senderUsername?: string | null;
|
||||
senderE164?: string | null;
|
||||
};
|
||||
|
||||
export type ChannelCapabilities = {
|
||||
|
||||
@ -1,17 +1,19 @@
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import { Command } from "commander";
|
||||
|
||||
const clientMocks = vi.hoisted(() => ({
|
||||
browserSnapshot: vi.fn(async () => ({
|
||||
const gatewayMocks = vi.hoisted(() => ({
|
||||
callGatewayFromCli: vi.fn(async () => ({
|
||||
ok: true,
|
||||
format: "ai",
|
||||
targetId: "t1",
|
||||
url: "https://example.com",
|
||||
snapshot: "ok",
|
||||
})),
|
||||
resolveBrowserControlUrl: vi.fn(() => "http://127.0.0.1:18791"),
|
||||
}));
|
||||
vi.mock("../browser/client.js", () => clientMocks);
|
||||
|
||||
vi.mock("./gateway-rpc.js", () => ({
|
||||
callGatewayFromCli: gatewayMocks.callGatewayFromCli,
|
||||
}));
|
||||
|
||||
const configMocks = vi.hoisted(() => ({
|
||||
loadConfig: vi.fn(() => ({ browser: {} })),
|
||||
@ -37,6 +39,7 @@ describe("browser cli snapshot defaults", () => {
|
||||
configMocks.loadConfig.mockReturnValue({
|
||||
browser: { snapshotDefaults: { mode: "efficient" } },
|
||||
});
|
||||
|
||||
const { registerBrowserInspectCommands } = await import("./browser-cli-inspect.js");
|
||||
const program = new Command();
|
||||
const browser = program.command("browser").option("--json", false);
|
||||
@ -44,12 +47,18 @@ describe("browser cli snapshot defaults", () => {
|
||||
|
||||
await program.parseAsync(["browser", "snapshot"], { from: "user" });
|
||||
|
||||
expect(clientMocks.browserSnapshot).toHaveBeenCalledWith(
|
||||
"http://127.0.0.1:18791",
|
||||
expect(gatewayMocks.callGatewayFromCli).toHaveBeenCalledWith(
|
||||
"browser.request",
|
||||
expect.any(Object),
|
||||
expect.objectContaining({
|
||||
format: "ai",
|
||||
mode: "efficient",
|
||||
method: "GET",
|
||||
path: "/snapshot",
|
||||
query: expect.objectContaining({
|
||||
format: "ai",
|
||||
mode: "efficient",
|
||||
}),
|
||||
}),
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
@ -57,13 +66,15 @@ describe("browser cli snapshot defaults", () => {
|
||||
configMocks.loadConfig.mockReturnValue({
|
||||
browser: { snapshotDefaults: { mode: "efficient" } },
|
||||
});
|
||||
clientMocks.browserSnapshot.mockResolvedValueOnce({
|
||||
|
||||
gatewayMocks.callGatewayFromCli.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
format: "aria",
|
||||
targetId: "t1",
|
||||
url: "https://example.com",
|
||||
nodes: [],
|
||||
});
|
||||
|
||||
const { registerBrowserInspectCommands } = await import("./browser-cli-inspect.js");
|
||||
const program = new Command();
|
||||
const browser = program.command("browser").option("--json", false);
|
||||
@ -71,8 +82,9 @@ describe("browser cli snapshot defaults", () => {
|
||||
|
||||
await program.parseAsync(["browser", "snapshot", "--format", "aria"], { from: "user" });
|
||||
|
||||
expect(clientMocks.browserSnapshot).toHaveBeenCalled();
|
||||
const [, opts] = clientMocks.browserSnapshot.mock.calls.at(-1) ?? [];
|
||||
expect(opts?.mode).toBeUndefined();
|
||||
expect(gatewayMocks.callGatewayFromCli).toHaveBeenCalled();
|
||||
const [, , params] = gatewayMocks.callGatewayFromCli.mock.calls.at(-1) ?? [];
|
||||
expect(params).toBeTruthy();
|
||||
expect((params as { query?: Record<string, string> }).query?.mode).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,4 +1,19 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||
|
||||
let previousProfile: string | undefined;
|
||||
|
||||
beforeAll(() => {
|
||||
previousProfile = process.env.CLAWDBOT_PROFILE;
|
||||
process.env.CLAWDBOT_PROFILE = "isolated";
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (previousProfile === undefined) {
|
||||
delete process.env.CLAWDBOT_PROFILE;
|
||||
} else {
|
||||
process.env.CLAWDBOT_PROFILE = previousProfile;
|
||||
}
|
||||
});
|
||||
|
||||
const mocks = vi.hoisted(() => ({
|
||||
loadSessionStore: vi.fn().mockReturnValue({
|
||||
|
||||
@ -1,13 +1,14 @@
|
||||
import type { ChannelId } from "../channels/plugins/types.js";
|
||||
import { normalizeAccountId } from "../routing/session-key.js";
|
||||
import type { ClawdbotConfig } from "./config.js";
|
||||
import type { GroupToolPolicyConfig } from "./types.tools.js";
|
||||
import type { GroupToolPolicyBySenderConfig, GroupToolPolicyConfig } from "./types.tools.js";
|
||||
|
||||
export type GroupPolicyChannel = ChannelId;
|
||||
|
||||
export type ChannelGroupConfig = {
|
||||
requireMention?: boolean;
|
||||
tools?: GroupToolPolicyConfig;
|
||||
toolsBySender?: GroupToolPolicyBySenderConfig;
|
||||
};
|
||||
|
||||
export type ChannelGroupPolicy = {
|
||||
@ -19,6 +20,65 @@ export type ChannelGroupPolicy = {
|
||||
|
||||
type ChannelGroups = Record<string, ChannelGroupConfig>;
|
||||
|
||||
export type GroupToolPolicySender = {
|
||||
senderId?: string | null;
|
||||
senderName?: string | null;
|
||||
senderUsername?: string | null;
|
||||
senderE164?: string | null;
|
||||
};
|
||||
|
||||
function normalizeSenderKey(value: string): string {
|
||||
const trimmed = value.trim();
|
||||
if (!trimmed) return "";
|
||||
const withoutAt = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed;
|
||||
return withoutAt.toLowerCase();
|
||||
}
|
||||
|
||||
export function resolveToolsBySender(
|
||||
params: {
|
||||
toolsBySender?: GroupToolPolicyBySenderConfig;
|
||||
} & GroupToolPolicySender,
|
||||
): GroupToolPolicyConfig | undefined {
|
||||
const toolsBySender = params.toolsBySender;
|
||||
if (!toolsBySender) return undefined;
|
||||
const entries = Object.entries(toolsBySender);
|
||||
if (entries.length === 0) return undefined;
|
||||
|
||||
const normalized = new Map<string, GroupToolPolicyConfig>();
|
||||
let wildcard: GroupToolPolicyConfig | undefined;
|
||||
for (const [rawKey, policy] of entries) {
|
||||
if (!policy) continue;
|
||||
const key = normalizeSenderKey(rawKey);
|
||||
if (!key) continue;
|
||||
if (key === "*") {
|
||||
wildcard = policy;
|
||||
continue;
|
||||
}
|
||||
if (!normalized.has(key)) {
|
||||
normalized.set(key, policy);
|
||||
}
|
||||
}
|
||||
|
||||
const candidates: string[] = [];
|
||||
const pushCandidate = (value?: string | null) => {
|
||||
const trimmed = value?.trim();
|
||||
if (!trimmed) return;
|
||||
candidates.push(trimmed);
|
||||
};
|
||||
pushCandidate(params.senderId);
|
||||
pushCandidate(params.senderE164);
|
||||
pushCandidate(params.senderUsername);
|
||||
pushCandidate(params.senderName);
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const key = normalizeSenderKey(candidate);
|
||||
if (!key) continue;
|
||||
const match = normalized.get(key);
|
||||
if (match) return match;
|
||||
}
|
||||
return wildcard;
|
||||
}
|
||||
|
||||
function resolveChannelGroups(
|
||||
cfg: ClawdbotConfig,
|
||||
channel: GroupPolicyChannel,
|
||||
@ -94,14 +154,32 @@ export function resolveChannelGroupRequireMention(params: {
|
||||
return true;
|
||||
}
|
||||
|
||||
export function resolveChannelGroupToolsPolicy(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
channel: GroupPolicyChannel;
|
||||
groupId?: string | null;
|
||||
accountId?: string | null;
|
||||
}): GroupToolPolicyConfig | undefined {
|
||||
export function resolveChannelGroupToolsPolicy(
|
||||
params: {
|
||||
cfg: ClawdbotConfig;
|
||||
channel: GroupPolicyChannel;
|
||||
groupId?: string | null;
|
||||
accountId?: string | null;
|
||||
} & GroupToolPolicySender,
|
||||
): GroupToolPolicyConfig | undefined {
|
||||
const { groupConfig, defaultConfig } = resolveChannelGroupPolicy(params);
|
||||
const groupSenderPolicy = resolveToolsBySender({
|
||||
toolsBySender: groupConfig?.toolsBySender,
|
||||
senderId: params.senderId,
|
||||
senderName: params.senderName,
|
||||
senderUsername: params.senderUsername,
|
||||
senderE164: params.senderE164,
|
||||
});
|
||||
if (groupSenderPolicy) return groupSenderPolicy;
|
||||
if (groupConfig?.tools) return groupConfig.tools;
|
||||
const defaultSenderPolicy = resolveToolsBySender({
|
||||
toolsBySender: defaultConfig?.toolsBySender,
|
||||
senderId: params.senderId,
|
||||
senderName: params.senderName,
|
||||
senderUsername: params.senderUsername,
|
||||
senderE164: params.senderE164,
|
||||
});
|
||||
if (defaultSenderPolicy) return defaultSenderPolicy;
|
||||
if (defaultConfig?.tools) return defaultConfig.tools;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -8,7 +8,7 @@ import type {
|
||||
} from "./types.base.js";
|
||||
import type { ChannelHeartbeatVisibilityConfig } from "./types.channels.js";
|
||||
import type { DmConfig, ProviderCommandsConfig } from "./types.messages.js";
|
||||
import type { GroupToolPolicyConfig } from "./types.tools.js";
|
||||
import type { GroupToolPolicyBySenderConfig, GroupToolPolicyConfig } from "./types.tools.js";
|
||||
|
||||
export type DiscordDmConfig = {
|
||||
/** If false, ignore all incoming Discord DMs. Default: true. */
|
||||
@ -28,6 +28,7 @@ export type DiscordGuildChannelConfig = {
|
||||
requireMention?: boolean;
|
||||
/** Optional tool policy overrides for this channel. */
|
||||
tools?: GroupToolPolicyConfig;
|
||||
toolsBySender?: GroupToolPolicyBySenderConfig;
|
||||
/** If specified, only load these skills for this channel. Omit = all skills; empty = no skills. */
|
||||
skills?: string[];
|
||||
/** If false, disable the bot for this channel. */
|
||||
@ -45,6 +46,7 @@ export type DiscordGuildEntry = {
|
||||
requireMention?: boolean;
|
||||
/** Optional tool policy overrides for this guild (used when channel override is missing). */
|
||||
tools?: GroupToolPolicyConfig;
|
||||
toolsBySender?: GroupToolPolicyBySenderConfig;
|
||||
/** Reaction notification mode (off|own|all|allowlist). Default: own. */
|
||||
reactionNotifications?: DiscordReactionNotificationMode;
|
||||
users?: Array<string | number>;
|
||||
|
||||
@ -6,7 +6,7 @@ import type {
|
||||
} from "./types.base.js";
|
||||
import type { ChannelHeartbeatVisibilityConfig } from "./types.channels.js";
|
||||
import type { DmConfig } from "./types.messages.js";
|
||||
import type { GroupToolPolicyConfig } from "./types.tools.js";
|
||||
import type { GroupToolPolicyBySenderConfig, GroupToolPolicyConfig } from "./types.tools.js";
|
||||
|
||||
export type IMessageAccountConfig = {
|
||||
/** Optional display name for this account (used in CLI/UI lists). */
|
||||
@ -64,6 +64,7 @@ export type IMessageAccountConfig = {
|
||||
{
|
||||
requireMention?: boolean;
|
||||
tools?: GroupToolPolicyConfig;
|
||||
toolsBySender?: GroupToolPolicyBySenderConfig;
|
||||
}
|
||||
>;
|
||||
/** Heartbeat visibility settings for this channel. */
|
||||
|
||||
@ -6,7 +6,7 @@ import type {
|
||||
} from "./types.base.js";
|
||||
import type { ChannelHeartbeatVisibilityConfig } from "./types.channels.js";
|
||||
import type { DmConfig } from "./types.messages.js";
|
||||
import type { GroupToolPolicyConfig } from "./types.tools.js";
|
||||
import type { GroupToolPolicyBySenderConfig, GroupToolPolicyConfig } from "./types.tools.js";
|
||||
|
||||
export type MSTeamsWebhookConfig = {
|
||||
/** Port for the webhook server. Default: 3978. */
|
||||
@ -24,6 +24,7 @@ export type MSTeamsChannelConfig = {
|
||||
requireMention?: boolean;
|
||||
/** Optional tool policy overrides for this channel. */
|
||||
tools?: GroupToolPolicyConfig;
|
||||
toolsBySender?: GroupToolPolicyBySenderConfig;
|
||||
/** Reply style: "thread" replies to the message, "top-level" posts a new message. */
|
||||
replyStyle?: MSTeamsReplyStyle;
|
||||
};
|
||||
@ -34,6 +35,7 @@ export type MSTeamsTeamConfig = {
|
||||
requireMention?: boolean;
|
||||
/** Default tool policy for channels in this team. */
|
||||
tools?: GroupToolPolicyConfig;
|
||||
toolsBySender?: GroupToolPolicyBySenderConfig;
|
||||
/** Default reply style for channels in this team. */
|
||||
replyStyle?: MSTeamsReplyStyle;
|
||||
/** Per-channel overrides. Key is conversation ID (e.g., "19:...@thread.tacv2"). */
|
||||
|
||||
@ -7,7 +7,7 @@ import type {
|
||||
} from "./types.base.js";
|
||||
import type { ChannelHeartbeatVisibilityConfig } from "./types.channels.js";
|
||||
import type { DmConfig, ProviderCommandsConfig } from "./types.messages.js";
|
||||
import type { GroupToolPolicyConfig } from "./types.tools.js";
|
||||
import type { GroupToolPolicyBySenderConfig, GroupToolPolicyConfig } from "./types.tools.js";
|
||||
|
||||
export type SlackDmConfig = {
|
||||
/** If false, ignore all incoming Slack DMs. Default: true. */
|
||||
@ -33,6 +33,7 @@ export type SlackChannelConfig = {
|
||||
requireMention?: boolean;
|
||||
/** Optional tool policy overrides for this channel. */
|
||||
tools?: GroupToolPolicyConfig;
|
||||
toolsBySender?: GroupToolPolicyBySenderConfig;
|
||||
/** Allow bot-authored messages to trigger replies (default: false). */
|
||||
allowBots?: boolean;
|
||||
/** Allowlist of users that can invoke the bot in this channel. */
|
||||
|
||||
@ -9,7 +9,7 @@ import type {
|
||||
} from "./types.base.js";
|
||||
import type { ChannelHeartbeatVisibilityConfig } from "./types.channels.js";
|
||||
import type { DmConfig, ProviderCommandsConfig } from "./types.messages.js";
|
||||
import type { GroupToolPolicyConfig } from "./types.tools.js";
|
||||
import type { GroupToolPolicyBySenderConfig, GroupToolPolicyConfig } from "./types.tools.js";
|
||||
|
||||
export type TelegramActionConfig = {
|
||||
reactions?: boolean;
|
||||
@ -146,6 +146,7 @@ export type TelegramGroupConfig = {
|
||||
requireMention?: boolean;
|
||||
/** Optional tool policy overrides for this group. */
|
||||
tools?: GroupToolPolicyConfig;
|
||||
toolsBySender?: GroupToolPolicyBySenderConfig;
|
||||
/** If specified, only load these skills for this group (when no topic). Omit = all skills; empty = no skills. */
|
||||
skills?: string[];
|
||||
/** Per-topic configuration (key is message_thread_id as string) */
|
||||
|
||||
@ -158,6 +158,8 @@ export type GroupToolPolicyConfig = {
|
||||
deny?: string[];
|
||||
};
|
||||
|
||||
export type GroupToolPolicyBySenderConfig = Record<string, GroupToolPolicyConfig>;
|
||||
|
||||
export type ExecToolConfig = {
|
||||
/** Exec host routing (default: sandbox). */
|
||||
host?: "sandbox" | "gateway" | "node";
|
||||
|
||||
@ -6,7 +6,7 @@ import type {
|
||||
} from "./types.base.js";
|
||||
import type { ChannelHeartbeatVisibilityConfig } from "./types.channels.js";
|
||||
import type { DmConfig } from "./types.messages.js";
|
||||
import type { GroupToolPolicyConfig } from "./types.tools.js";
|
||||
import type { GroupToolPolicyBySenderConfig, GroupToolPolicyConfig } from "./types.tools.js";
|
||||
|
||||
export type WhatsAppActionConfig = {
|
||||
reactions?: boolean;
|
||||
@ -70,6 +70,7 @@ export type WhatsAppConfig = {
|
||||
{
|
||||
requireMention?: boolean;
|
||||
tools?: GroupToolPolicyConfig;
|
||||
toolsBySender?: GroupToolPolicyBySenderConfig;
|
||||
}
|
||||
>;
|
||||
/** Acknowledgment reaction sent immediately upon message receipt. */
|
||||
@ -135,6 +136,7 @@ export type WhatsAppAccountConfig = {
|
||||
{
|
||||
requireMention?: boolean;
|
||||
tools?: GroupToolPolicyConfig;
|
||||
toolsBySender?: GroupToolPolicyBySenderConfig;
|
||||
}
|
||||
>;
|
||||
/** Acknowledgment reaction sent immediately upon message receipt. */
|
||||
|
||||
@ -22,6 +22,8 @@ import {
|
||||
resolveTelegramCustomCommands,
|
||||
} from "./telegram-custom-commands.js";
|
||||
|
||||
const ToolPolicyBySenderSchema = z.record(z.string(), ToolPolicySchema).optional();
|
||||
|
||||
const TelegramInlineButtonsScopeSchema = z.enum(["off", "dm", "group", "all", "allowlist"]);
|
||||
|
||||
const TelegramCapabilitiesSchema = z.union([
|
||||
@ -47,6 +49,7 @@ export const TelegramGroupSchema = z
|
||||
.object({
|
||||
requireMention: z.boolean().optional(),
|
||||
tools: ToolPolicySchema,
|
||||
toolsBySender: ToolPolicyBySenderSchema,
|
||||
skills: z.array(z.string()).optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
allowFrom: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
@ -186,6 +189,7 @@ export const DiscordGuildChannelSchema = z
|
||||
allow: z.boolean().optional(),
|
||||
requireMention: z.boolean().optional(),
|
||||
tools: ToolPolicySchema,
|
||||
toolsBySender: ToolPolicyBySenderSchema,
|
||||
skills: z.array(z.string()).optional(),
|
||||
enabled: z.boolean().optional(),
|
||||
users: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
@ -199,6 +203,7 @@ export const DiscordGuildSchema = z
|
||||
slug: z.string().optional(),
|
||||
requireMention: z.boolean().optional(),
|
||||
tools: ToolPolicySchema,
|
||||
toolsBySender: ToolPolicyBySenderSchema,
|
||||
reactionNotifications: z.enum(["off", "own", "all", "allowlist"]).optional(),
|
||||
users: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
channels: z.record(z.string(), DiscordGuildChannelSchema.optional()).optional(),
|
||||
@ -374,6 +379,7 @@ export const SlackChannelSchema = z
|
||||
allow: z.boolean().optional(),
|
||||
requireMention: z.boolean().optional(),
|
||||
tools: ToolPolicySchema,
|
||||
toolsBySender: ToolPolicyBySenderSchema,
|
||||
allowBots: z.boolean().optional(),
|
||||
users: z.array(z.union([z.string(), z.number()])).optional(),
|
||||
skills: z.array(z.string()).optional(),
|
||||
@ -584,6 +590,7 @@ export const IMessageAccountSchemaBase = z
|
||||
.object({
|
||||
requireMention: z.boolean().optional(),
|
||||
tools: ToolPolicySchema,
|
||||
toolsBySender: ToolPolicyBySenderSchema,
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
@ -640,6 +647,7 @@ const BlueBubblesGroupConfigSchema = z
|
||||
.object({
|
||||
requireMention: z.boolean().optional(),
|
||||
tools: ToolPolicySchema,
|
||||
toolsBySender: ToolPolicyBySenderSchema,
|
||||
})
|
||||
.strict();
|
||||
|
||||
@ -699,6 +707,7 @@ export const MSTeamsChannelSchema = z
|
||||
.object({
|
||||
requireMention: z.boolean().optional(),
|
||||
tools: ToolPolicySchema,
|
||||
toolsBySender: ToolPolicyBySenderSchema,
|
||||
replyStyle: MSTeamsReplyStyleSchema.optional(),
|
||||
})
|
||||
.strict();
|
||||
@ -707,6 +716,7 @@ export const MSTeamsTeamSchema = z
|
||||
.object({
|
||||
requireMention: z.boolean().optional(),
|
||||
tools: ToolPolicySchema,
|
||||
toolsBySender: ToolPolicyBySenderSchema,
|
||||
replyStyle: MSTeamsReplyStyleSchema.optional(),
|
||||
channels: z.record(z.string(), MSTeamsChannelSchema.optional()).optional(),
|
||||
})
|
||||
|
||||
@ -10,6 +10,8 @@ import {
|
||||
import { ToolPolicySchema } from "./zod-schema.agent-runtime.js";
|
||||
import { ChannelHeartbeatVisibilitySchema } from "./zod-schema.channels.js";
|
||||
|
||||
const ToolPolicyBySenderSchema = z.record(z.string(), ToolPolicySchema).optional();
|
||||
|
||||
export const WhatsAppAccountSchema = z
|
||||
.object({
|
||||
name: z.string().optional(),
|
||||
@ -41,6 +43,7 @@ export const WhatsAppAccountSchema = z
|
||||
.object({
|
||||
requireMention: z.boolean().optional(),
|
||||
tools: ToolPolicySchema,
|
||||
toolsBySender: ToolPolicyBySenderSchema,
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
@ -105,6 +108,7 @@ export const WhatsAppConfigSchema = z
|
||||
.object({
|
||||
requireMention: z.boolean().optional(),
|
||||
tools: ToolPolicySchema,
|
||||
toolsBySender: ToolPolicyBySenderSchema,
|
||||
})
|
||||
.strict()
|
||||
.optional(),
|
||||
|
||||
@ -1,8 +1,22 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
||||
|
||||
import { buildPairingReply } from "./pairing-messages.js";
|
||||
|
||||
describe("buildPairingReply", () => {
|
||||
let previousProfile: string | undefined;
|
||||
|
||||
beforeAll(() => {
|
||||
previousProfile = process.env.CLAWDBOT_PROFILE;
|
||||
process.env.CLAWDBOT_PROFILE = "isolated";
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
if (previousProfile === undefined) {
|
||||
delete process.env.CLAWDBOT_PROFILE;
|
||||
} else {
|
||||
process.env.CLAWDBOT_PROFILE = previousProfile;
|
||||
}
|
||||
});
|
||||
const cases = [
|
||||
{
|
||||
channel: "discord",
|
||||
|
||||
@ -81,6 +81,7 @@ export type {
|
||||
DmConfig,
|
||||
GroupPolicy,
|
||||
GroupToolPolicyConfig,
|
||||
GroupToolPolicyBySenderConfig,
|
||||
MarkdownConfig,
|
||||
MarkdownTableMode,
|
||||
GoogleChatAccountConfig,
|
||||
@ -121,6 +122,7 @@ export { resolveAckReaction } from "../agents/identity.js";
|
||||
export type { ReplyPayload } from "../auto-reply/types.js";
|
||||
export type { ChunkMode } from "../auto-reply/chunk.js";
|
||||
export { SILENT_REPLY_TOKEN, isSilentReplyText } from "../auto-reply/tokens.js";
|
||||
export { resolveToolsBySender } from "../config/group-policy.js";
|
||||
export {
|
||||
buildPendingHistoryContextFromMap,
|
||||
clearHistoryEntries,
|
||||
|
||||
Loading…
Reference in New Issue
Block a user