From d6f908b813ccdc40b667552966466c00703fb6b7 Mon Sep 17 00:00:00 2001 From: orion Date: Thu, 26 Feb 2026 00:23:47 +0000 Subject: [PATCH] feat(policy): add per-channel channelPolicies with hot-reload list mode/lists --- docs/CONFIG.example.json | 7 ++++ docs/INTEGRATION.md | 1 + plugin/README.md | 1 + plugin/index.ts | 3 +- plugin/openclaw.plugin.json | 14 ++++++++ plugin/rules.ts | 44 ++++++++++++++++++++++--- scripts/install-whispergate-openclaw.sh | 4 ++- 7 files changed, 68 insertions(+), 6 deletions(-) diff --git a/docs/CONFIG.example.json b/docs/CONFIG.example.json index 0ed8f1e..c1f6109 100644 --- a/docs/CONFIG.example.json +++ b/docs/CONFIG.example.json @@ -13,6 +13,13 @@ "humanList": ["561921120408698910"], "agentList": [], "endSymbols": ["🔚"], + "channelPolicies": { + "1476369680632647721": { + "listMode": "agent-list", + "agentList": ["1474088632750047324"], + "endSymbols": ["🔚"] + } + }, "noReplyProvider": "whisper-gateway", "noReplyModel": "no-reply", "enableDiscordControlTool": true, diff --git a/docs/INTEGRATION.md b/docs/INTEGRATION.md index f1c8c12..07d13ae 100644 --- a/docs/INTEGRATION.md +++ b/docs/INTEGRATION.md @@ -54,6 +54,7 @@ Environment overrides: - `LIST_MODE` (`human-list` or `agent-list`) - `HUMAN_LIST_JSON` - `AGENT_LIST_JSON` +- `CHANNEL_POLICIES_JSON` - `END_SYMBOLS_JSON` The script: diff --git a/plugin/README.md b/plugin/README.md index 6e13cf2..1abc1f9 100644 --- a/plugin/README.md +++ b/plugin/README.md @@ -29,6 +29,7 @@ Optional: - `listMode` (`human-list` | `agent-list`, default `human-list`) - `humanList` (default []) - `agentList` (default []) +- `channelPolicies` (per-channel overrides by channelId) - `bypassUserIds` (deprecated alias of `humanList`) - `endSymbols` (default ["🔚"]) - `enableDiscordControlTool` (default true) diff --git a/plugin/index.ts b/plugin/index.ts index 208c0be..b3cffb7 100644 --- a/plugin/index.ts +++ b/plugin/index.ts @@ -178,9 +178,10 @@ export default { const senderId = normalizeSender(e, c); const content = typeof e.content === "string" ? e.content : ""; const channel = normalizeChannel(c); + const channelId = typeof c.channelId === "string" ? c.channelId : undefined; const live = getLivePluginConfig(api, baseConfig as WhisperGateConfig); - const decision = evaluateDecision({ config: live, channel, senderId, content }); + const decision = evaluateDecision({ config: live, channel, channelId, senderId, content }); sessionDecision.set(sessionKey, { decision, createdAt: Date.now() }); pruneDecisionMap(); api.logger.debug?.( diff --git a/plugin/openclaw.plugin.json b/plugin/openclaw.plugin.json index 968c0bf..3779378 100644 --- a/plugin/openclaw.plugin.json +++ b/plugin/openclaw.plugin.json @@ -13,6 +13,20 @@ "listMode": { "type": "string", "enum": ["human-list", "agent-list"], "default": "human-list" }, "humanList": { "type": "array", "items": { "type": "string" }, "default": [] }, "agentList": { "type": "array", "items": { "type": "string" }, "default": [] }, + "channelPolicies": { + "type": "object", + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "properties": { + "listMode": { "type": "string", "enum": ["human-list", "agent-list"] }, + "humanList": { "type": "array", "items": { "type": "string" } }, + "agentList": { "type": "array", "items": { "type": "string" } }, + "endSymbols": { "type": "array", "items": { "type": "string" } } + } + }, + "default": {} + }, "bypassUserIds": { "type": "array", "items": { "type": "string" }, "default": [] }, "endSymbols": { "type": "array", "items": { "type": "string" }, "default": ["🔚"] }, "noReplyProvider": { "type": "string" }, diff --git a/plugin/rules.ts b/plugin/rules.ts index fb00f3c..b4797f4 100644 --- a/plugin/rules.ts +++ b/plugin/rules.ts @@ -4,6 +4,15 @@ export type WhisperGateConfig = { listMode?: "human-list" | "agent-list"; humanList?: string[]; agentList?: string[]; + channelPolicies?: Record< + string, + { + listMode?: "human-list" | "agent-list"; + humanList?: string[]; + agentList?: string[]; + endSymbols?: string[]; + } + >; // backward compatibility bypassUserIds?: string[]; endSymbols?: string[]; @@ -21,9 +30,34 @@ function getLastChar(input: string): string { return t.length ? t[t.length - 1] : ""; } +function resolvePolicy(config: WhisperGateConfig, channelId?: string) { + const globalMode = config.listMode || "human-list"; + const globalHuman = config.humanList || config.bypassUserIds || []; + const globalAgent = config.agentList || []; + const globalEnd = config.endSymbols || ["🔚"]; + + if (!channelId) { + return { listMode: globalMode, humanList: globalHuman, agentList: globalAgent, endSymbols: globalEnd }; + } + + const cp = config.channelPolicies || {}; + const scoped = cp[channelId]; + if (!scoped) { + return { listMode: globalMode, humanList: globalHuman, agentList: globalAgent, endSymbols: globalEnd }; + } + + return { + listMode: scoped.listMode || globalMode, + humanList: scoped.humanList || globalHuman, + agentList: scoped.agentList || globalAgent, + endSymbols: scoped.endSymbols || globalEnd, + }; +} + export function evaluateDecision(params: { config: WhisperGateConfig; channel?: string; + channelId?: string; senderId?: string; content?: string; }): Decision { @@ -38,16 +72,18 @@ export function evaluateDecision(params: { return { shouldUseNoReply: false, reason: "non_discord" }; } - const mode = config.listMode || "human-list"; - const humanList = config.humanList || config.bypassUserIds || []; - const agentList = config.agentList || []; + const policy = resolvePolicy(config, params.channelId); + + const mode = policy.listMode; + const humanList = policy.humanList; + const agentList = policy.agentList; const senderId = params.senderId || ""; const inHumanList = !!senderId && humanList.includes(senderId); const inAgentList = !!senderId && agentList.includes(senderId); const lastChar = getLastChar(params.content || ""); - const hasEnd = !!lastChar && (config.endSymbols || []).includes(lastChar); + const hasEnd = !!lastChar && policy.endSymbols.includes(lastChar); if (mode === "human-list") { if (inHumanList) { diff --git a/scripts/install-whispergate-openclaw.sh b/scripts/install-whispergate-openclaw.sh index c91bcda..4686518 100755 --- a/scripts/install-whispergate-openclaw.sh +++ b/scripts/install-whispergate-openclaw.sh @@ -17,6 +17,7 @@ NO_REPLY_API_KEY="${NO_REPLY_API_KEY:-wg-local-test-token}" LIST_MODE="${LIST_MODE:-human-list}" HUMAN_LIST_JSON="${HUMAN_LIST_JSON:-[\"561921120408698910\",\"1474088632750047324\"]}" AGENT_LIST_JSON="${AGENT_LIST_JSON:-[]}" +CHANNEL_POLICIES_JSON="${CHANNEL_POLICIES_JSON:-{}}" END_SYMBOLS_JSON="${END_SYMBOLS_JSON:-[\"🔚\"]}" STATE_DIR="${STATE_DIR:-$HOME/.openclaw/whispergate-install-records}" @@ -138,7 +139,7 @@ PY current_plugins_json='{}' fi - new_plugins_json="$(CURRENT_PLUGINS_JSON="$current_plugins_json" PLUGIN_PATH="$PLUGIN_PATH" LIST_MODE="$LIST_MODE" HUMAN_LIST_JSON="$HUMAN_LIST_JSON" AGENT_LIST_JSON="$AGENT_LIST_JSON" END_SYMBOLS_JSON="$END_SYMBOLS_JSON" NO_REPLY_PROVIDER_ID="$NO_REPLY_PROVIDER_ID" NO_REPLY_MODEL_ID="$NO_REPLY_MODEL_ID" python3 - <<'PY' + new_plugins_json="$(CURRENT_PLUGINS_JSON="$current_plugins_json" PLUGIN_PATH="$PLUGIN_PATH" LIST_MODE="$LIST_MODE" HUMAN_LIST_JSON="$HUMAN_LIST_JSON" AGENT_LIST_JSON="$AGENT_LIST_JSON" CHANNEL_POLICIES_JSON="$CHANNEL_POLICIES_JSON" END_SYMBOLS_JSON="$END_SYMBOLS_JSON" NO_REPLY_PROVIDER_ID="$NO_REPLY_PROVIDER_ID" NO_REPLY_MODEL_ID="$NO_REPLY_MODEL_ID" python3 - <<'PY' import json, os plugins=json.loads(os.environ['CURRENT_PLUGINS_JSON']) if not isinstance(plugins,dict): @@ -162,6 +163,7 @@ entries['whispergate']={ 'listMode': os.environ['LIST_MODE'], 'humanList': json.loads(os.environ['HUMAN_LIST_JSON']), 'agentList': json.loads(os.environ['AGENT_LIST_JSON']), + 'channelPolicies': json.loads(os.environ['CHANNEL_POLICIES_JSON']), 'endSymbols': json.loads(os.environ['END_SYMBOLS_JSON']), 'noReplyProvider': os.environ['NO_REPLY_PROVIDER_ID'], 'noReplyModel': os.environ['NO_REPLY_MODEL_ID'],