mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-19 02:17:26 +00:00
refactor(core): dedupe shared config and runtime helpers
This commit is contained in:
@@ -8,3 +8,57 @@ export function formatAllowFromLowercase(params: {
|
||||
.map((entry) => (params.stripPrefixRe ? entry.replace(params.stripPrefixRe, "") : entry))
|
||||
.map((entry) => entry.toLowerCase());
|
||||
}
|
||||
|
||||
type ParsedChatAllowTarget =
|
||||
| { kind: "chat_id"; chatId: number }
|
||||
| { kind: "chat_guid"; chatGuid: string }
|
||||
| { kind: "chat_identifier"; chatIdentifier: string }
|
||||
| { kind: "handle"; handle: string };
|
||||
|
||||
export function isAllowedParsedChatSender<TParsed extends ParsedChatAllowTarget>(params: {
|
||||
allowFrom: Array<string | number>;
|
||||
sender: string;
|
||||
chatId?: number | null;
|
||||
chatGuid?: string | null;
|
||||
chatIdentifier?: string | null;
|
||||
normalizeSender: (sender: string) => string;
|
||||
parseAllowTarget: (entry: string) => TParsed;
|
||||
}): boolean {
|
||||
const allowFrom = params.allowFrom.map((entry) => String(entry).trim());
|
||||
if (allowFrom.length === 0) {
|
||||
return true;
|
||||
}
|
||||
if (allowFrom.includes("*")) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const senderNormalized = params.normalizeSender(params.sender);
|
||||
const chatId = params.chatId ?? undefined;
|
||||
const chatGuid = params.chatGuid?.trim();
|
||||
const chatIdentifier = params.chatIdentifier?.trim();
|
||||
|
||||
for (const entry of allowFrom) {
|
||||
if (!entry) {
|
||||
continue;
|
||||
}
|
||||
const parsed = params.parseAllowTarget(entry);
|
||||
if (parsed.kind === "chat_id" && chatId !== undefined) {
|
||||
if (parsed.chatId === chatId) {
|
||||
return true;
|
||||
}
|
||||
} else if (parsed.kind === "chat_guid" && chatGuid) {
|
||||
if (parsed.chatGuid === chatGuid) {
|
||||
return true;
|
||||
}
|
||||
} else if (parsed.kind === "chat_identifier" && chatIdentifier) {
|
||||
if (parsed.chatIdentifier === chatIdentifier) {
|
||||
return true;
|
||||
}
|
||||
} else if (parsed.kind === "handle" && senderNormalized) {
|
||||
if (parsed.handle === senderNormalized) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
50
src/plugin-sdk/command-auth.ts
Normal file
50
src/plugin-sdk/command-auth.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
|
||||
export type ResolveSenderCommandAuthorizationParams = {
|
||||
cfg: OpenClawConfig;
|
||||
rawBody: string;
|
||||
isGroup: boolean;
|
||||
dmPolicy: string;
|
||||
configuredAllowFrom: string[];
|
||||
senderId: string;
|
||||
isSenderAllowed: (senderId: string, allowFrom: string[]) => boolean;
|
||||
readAllowFromStore: () => Promise<string[]>;
|
||||
shouldComputeCommandAuthorized: (rawBody: string, cfg: OpenClawConfig) => boolean;
|
||||
resolveCommandAuthorizedFromAuthorizers: (params: {
|
||||
useAccessGroups: boolean;
|
||||
authorizers: Array<{ configured: boolean; allowed: boolean }>;
|
||||
}) => boolean;
|
||||
};
|
||||
|
||||
export async function resolveSenderCommandAuthorization(
|
||||
params: ResolveSenderCommandAuthorizationParams,
|
||||
): Promise<{
|
||||
shouldComputeAuth: boolean;
|
||||
effectiveAllowFrom: string[];
|
||||
senderAllowedForCommands: boolean;
|
||||
commandAuthorized: boolean | undefined;
|
||||
}> {
|
||||
const shouldComputeAuth = params.shouldComputeCommandAuthorized(params.rawBody, params.cfg);
|
||||
const storeAllowFrom =
|
||||
!params.isGroup && (params.dmPolicy !== "open" || shouldComputeAuth)
|
||||
? await params.readAllowFromStore().catch(() => [])
|
||||
: [];
|
||||
const effectiveAllowFrom = [...params.configuredAllowFrom, ...storeAllowFrom];
|
||||
const useAccessGroups = params.cfg.commands?.useAccessGroups !== false;
|
||||
const senderAllowedForCommands = params.isSenderAllowed(params.senderId, effectiveAllowFrom);
|
||||
const commandAuthorized = shouldComputeAuth
|
||||
? params.resolveCommandAuthorizedFromAuthorizers({
|
||||
useAccessGroups,
|
||||
authorizers: [
|
||||
{ configured: effectiveAllowFrom.length > 0, allowed: senderAllowedForCommands },
|
||||
],
|
||||
})
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
shouldComputeAuth,
|
||||
effectiveAllowFrom,
|
||||
senderAllowedForCommands,
|
||||
commandAuthorized,
|
||||
};
|
||||
}
|
||||
@@ -84,6 +84,11 @@ export type { OpenClawConfig as ClawdbotConfig } from "../config/config.js";
|
||||
export type { FileLockHandle, FileLockOptions } from "./file-lock.js";
|
||||
export { acquireFileLock, withFileLock } from "./file-lock.js";
|
||||
export { normalizeWebhookPath, resolveWebhookPath } from "./webhook-path.js";
|
||||
export {
|
||||
registerWebhookTarget,
|
||||
rejectNonPostWebhookRequest,
|
||||
resolveWebhookTargets,
|
||||
} from "./webhook-targets.js";
|
||||
export type { AgentMediaPayload } from "./agent-media-payload.js";
|
||||
export { buildAgentMediaPayload } from "./agent-media-payload.js";
|
||||
export {
|
||||
@@ -141,9 +146,13 @@ export { ToolPolicySchema } from "../config/zod-schema.agent-runtime.js";
|
||||
export type { RuntimeEnv } from "../runtime.js";
|
||||
export type { WizardPrompter } from "../wizard/prompts.js";
|
||||
export { DEFAULT_ACCOUNT_ID, normalizeAccountId } from "../routing/session-key.js";
|
||||
export { formatAllowFromLowercase } from "./allow-from.js";
|
||||
export { formatAllowFromLowercase, isAllowedParsedChatSender } from "./allow-from.js";
|
||||
export { resolveSenderCommandAuthorization } from "./command-auth.js";
|
||||
export { handleSlackMessageAction } from "./slack-message-actions.js";
|
||||
export { extractToolSend } from "./tool-send.js";
|
||||
export { resolveChannelAccountConfigBasePath } from "./config-paths.js";
|
||||
export { chunkTextForOutbound } from "./text-chunking.js";
|
||||
export { readJsonFileWithFallback, writeJsonFileAtomically } from "./json-store.js";
|
||||
export type { ChatType } from "../channels/chat-type.js";
|
||||
/** @deprecated Use ChatType instead */
|
||||
export type { RoutePeerKind } from "../routing/resolve-route.js";
|
||||
@@ -173,6 +182,7 @@ export {
|
||||
export { fetchWithSsrFGuard } from "../infra/net/fetch-guard.js";
|
||||
export { SsrFBlockedError, isBlockedHostname, isPrivateIpAddress } from "../infra/net/ssrf.js";
|
||||
export type { LookupFn, SsrFPolicy } from "../infra/net/ssrf.js";
|
||||
export { rawDataToString } from "../infra/ws.js";
|
||||
export { isWSLSync, isWSL2Sync, isWSLEnv } from "../infra/wsl.js";
|
||||
export { isTruthyEnvValue } from "../infra/env.js";
|
||||
export { resolveToolsBySender } from "../config/group-policy.js";
|
||||
|
||||
35
src/plugin-sdk/json-store.ts
Normal file
35
src/plugin-sdk/json-store.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { safeParseJson } from "../utils.js";
|
||||
|
||||
export async function readJsonFileWithFallback<T>(
|
||||
filePath: string,
|
||||
fallback: T,
|
||||
): Promise<{ value: T; exists: boolean }> {
|
||||
try {
|
||||
const raw = await fs.promises.readFile(filePath, "utf-8");
|
||||
const parsed = safeParseJson<T>(raw);
|
||||
if (parsed == null) {
|
||||
return { value: fallback, exists: true };
|
||||
}
|
||||
return { value: parsed, exists: true };
|
||||
} catch (err) {
|
||||
const code = (err as { code?: string }).code;
|
||||
if (code === "ENOENT") {
|
||||
return { value: fallback, exists: false };
|
||||
}
|
||||
return { value: fallback, exists: false };
|
||||
}
|
||||
}
|
||||
|
||||
export async function writeJsonFileAtomically(filePath: string, value: unknown): Promise<void> {
|
||||
const dir = path.dirname(filePath);
|
||||
await fs.promises.mkdir(dir, { recursive: true, mode: 0o700 });
|
||||
const tmp = path.join(dir, `${path.basename(filePath)}.${crypto.randomUUID()}.tmp`);
|
||||
await fs.promises.writeFile(tmp, `${JSON.stringify(value, null, 2)}\n`, {
|
||||
encoding: "utf-8",
|
||||
});
|
||||
await fs.promises.chmod(tmp, 0o600);
|
||||
await fs.promises.rename(tmp, filePath);
|
||||
}
|
||||
162
src/plugin-sdk/slack-message-actions.ts
Normal file
162
src/plugin-sdk/slack-message-actions.ts
Normal file
@@ -0,0 +1,162 @@
|
||||
import type { AgentToolResult } from "@mariozechner/pi-agent-core";
|
||||
import type { ChannelMessageActionContext } from "../channels/plugins/types.js";
|
||||
import { readNumberParam, readStringParam } from "../agents/tools/common.js";
|
||||
|
||||
type SlackActionInvoke = (
|
||||
action: Record<string, unknown>,
|
||||
cfg: ChannelMessageActionContext["cfg"],
|
||||
toolContext?: ChannelMessageActionContext["toolContext"],
|
||||
) => Promise<AgentToolResult<unknown>>;
|
||||
|
||||
export async function handleSlackMessageAction(params: {
|
||||
providerId: string;
|
||||
ctx: ChannelMessageActionContext;
|
||||
invoke: SlackActionInvoke;
|
||||
normalizeChannelId?: (channelId: string) => string;
|
||||
includeReadThreadId?: boolean;
|
||||
}): Promise<AgentToolResult<unknown>> {
|
||||
const { providerId, ctx, invoke, normalizeChannelId, includeReadThreadId = false } = params;
|
||||
const { action, cfg, params: actionParams } = ctx;
|
||||
const accountId = ctx.accountId ?? undefined;
|
||||
const resolveChannelId = () => {
|
||||
const channelId =
|
||||
readStringParam(actionParams, "channelId") ??
|
||||
readStringParam(actionParams, "to", { required: true });
|
||||
return normalizeChannelId ? normalizeChannelId(channelId) : channelId;
|
||||
};
|
||||
|
||||
if (action === "send") {
|
||||
const to = readStringParam(actionParams, "to", { required: true });
|
||||
const content = readStringParam(actionParams, "message", {
|
||||
required: true,
|
||||
allowEmpty: true,
|
||||
});
|
||||
const mediaUrl = readStringParam(actionParams, "media", { trim: false });
|
||||
const threadId = readStringParam(actionParams, "threadId");
|
||||
const replyTo = readStringParam(actionParams, "replyTo");
|
||||
return await invoke(
|
||||
{
|
||||
action: "sendMessage",
|
||||
to,
|
||||
content,
|
||||
mediaUrl: mediaUrl ?? undefined,
|
||||
accountId,
|
||||
threadTs: threadId ?? replyTo ?? undefined,
|
||||
},
|
||||
cfg,
|
||||
ctx.toolContext,
|
||||
);
|
||||
}
|
||||
|
||||
if (action === "react") {
|
||||
const messageId = readStringParam(actionParams, "messageId", {
|
||||
required: true,
|
||||
});
|
||||
const emoji = readStringParam(actionParams, "emoji", { allowEmpty: true });
|
||||
const remove = typeof actionParams.remove === "boolean" ? actionParams.remove : undefined;
|
||||
return await invoke(
|
||||
{
|
||||
action: "react",
|
||||
channelId: resolveChannelId(),
|
||||
messageId,
|
||||
emoji,
|
||||
remove,
|
||||
accountId,
|
||||
},
|
||||
cfg,
|
||||
);
|
||||
}
|
||||
|
||||
if (action === "reactions") {
|
||||
const messageId = readStringParam(actionParams, "messageId", {
|
||||
required: true,
|
||||
});
|
||||
const limit = readNumberParam(actionParams, "limit", { integer: true });
|
||||
return await invoke(
|
||||
{
|
||||
action: "reactions",
|
||||
channelId: resolveChannelId(),
|
||||
messageId,
|
||||
limit,
|
||||
accountId,
|
||||
},
|
||||
cfg,
|
||||
);
|
||||
}
|
||||
|
||||
if (action === "read") {
|
||||
const limit = readNumberParam(actionParams, "limit", { integer: true });
|
||||
const readAction: Record<string, unknown> = {
|
||||
action: "readMessages",
|
||||
channelId: resolveChannelId(),
|
||||
limit,
|
||||
before: readStringParam(actionParams, "before"),
|
||||
after: readStringParam(actionParams, "after"),
|
||||
accountId,
|
||||
};
|
||||
if (includeReadThreadId) {
|
||||
readAction.threadId = readStringParam(actionParams, "threadId");
|
||||
}
|
||||
return await invoke(readAction, cfg);
|
||||
}
|
||||
|
||||
if (action === "edit") {
|
||||
const messageId = readStringParam(actionParams, "messageId", {
|
||||
required: true,
|
||||
});
|
||||
const content = readStringParam(actionParams, "message", { required: true });
|
||||
return await invoke(
|
||||
{
|
||||
action: "editMessage",
|
||||
channelId: resolveChannelId(),
|
||||
messageId,
|
||||
content,
|
||||
accountId,
|
||||
},
|
||||
cfg,
|
||||
);
|
||||
}
|
||||
|
||||
if (action === "delete") {
|
||||
const messageId = readStringParam(actionParams, "messageId", {
|
||||
required: true,
|
||||
});
|
||||
return await invoke(
|
||||
{
|
||||
action: "deleteMessage",
|
||||
channelId: resolveChannelId(),
|
||||
messageId,
|
||||
accountId,
|
||||
},
|
||||
cfg,
|
||||
);
|
||||
}
|
||||
|
||||
if (action === "pin" || action === "unpin" || action === "list-pins") {
|
||||
const messageId =
|
||||
action === "list-pins"
|
||||
? undefined
|
||||
: readStringParam(actionParams, "messageId", { required: true });
|
||||
return await invoke(
|
||||
{
|
||||
action: action === "pin" ? "pinMessage" : action === "unpin" ? "unpinMessage" : "listPins",
|
||||
channelId: resolveChannelId(),
|
||||
messageId,
|
||||
accountId,
|
||||
},
|
||||
cfg,
|
||||
);
|
||||
}
|
||||
|
||||
if (action === "member-info") {
|
||||
const userId = readStringParam(actionParams, "userId", { required: true });
|
||||
return await invoke({ action: "memberInfo", userId, accountId }, cfg);
|
||||
}
|
||||
|
||||
if (action === "emoji-list") {
|
||||
const limit = readNumberParam(actionParams, "limit", { integer: true });
|
||||
return await invoke({ action: "emojiList", limit, accountId }, cfg);
|
||||
}
|
||||
|
||||
throw new Error(`Action ${action} is not supported for provider ${providerId}.`);
|
||||
}
|
||||
15
src/plugin-sdk/tool-send.ts
Normal file
15
src/plugin-sdk/tool-send.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
export function extractToolSend(
|
||||
args: Record<string, unknown>,
|
||||
expectedAction = "sendMessage",
|
||||
): { to: string; accountId?: string } | null {
|
||||
const action = typeof args.action === "string" ? args.action.trim() : "";
|
||||
if (action !== expectedAction) {
|
||||
return null;
|
||||
}
|
||||
const to = typeof args.to === "string" ? args.to : undefined;
|
||||
if (!to) {
|
||||
return null;
|
||||
}
|
||||
const accountId = typeof args.accountId === "string" ? args.accountId.trim() : undefined;
|
||||
return { to, accountId };
|
||||
}
|
||||
49
src/plugin-sdk/webhook-targets.ts
Normal file
49
src/plugin-sdk/webhook-targets.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||
import { normalizeWebhookPath } from "./webhook-path.js";
|
||||
|
||||
export type RegisteredWebhookTarget<T> = {
|
||||
target: T;
|
||||
unregister: () => void;
|
||||
};
|
||||
|
||||
export function registerWebhookTarget<T extends { path: string }>(
|
||||
targetsByPath: Map<string, T[]>,
|
||||
target: T,
|
||||
): RegisteredWebhookTarget<T> {
|
||||
const key = normalizeWebhookPath(target.path);
|
||||
const normalizedTarget = { ...target, path: key };
|
||||
const existing = targetsByPath.get(key) ?? [];
|
||||
targetsByPath.set(key, [...existing, normalizedTarget]);
|
||||
const unregister = () => {
|
||||
const updated = (targetsByPath.get(key) ?? []).filter((entry) => entry !== normalizedTarget);
|
||||
if (updated.length > 0) {
|
||||
targetsByPath.set(key, updated);
|
||||
return;
|
||||
}
|
||||
targetsByPath.delete(key);
|
||||
};
|
||||
return { target: normalizedTarget, unregister };
|
||||
}
|
||||
|
||||
export function resolveWebhookTargets<T>(
|
||||
req: IncomingMessage,
|
||||
targetsByPath: Map<string, T[]>,
|
||||
): { path: string; targets: T[] } | null {
|
||||
const url = new URL(req.url ?? "/", "http://localhost");
|
||||
const path = normalizeWebhookPath(url.pathname);
|
||||
const targets = targetsByPath.get(path);
|
||||
if (!targets || targets.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return { path, targets };
|
||||
}
|
||||
|
||||
export function rejectNonPostWebhookRequest(req: IncomingMessage, res: ServerResponse): boolean {
|
||||
if (req.method === "POST") {
|
||||
return false;
|
||||
}
|
||||
res.statusCode = 405;
|
||||
res.setHeader("Allow", "POST");
|
||||
res.end("Method Not Allowed");
|
||||
return true;
|
||||
}
|
||||
Reference in New Issue
Block a user